Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 59 additions & 18 deletions Tests/StackNudgePanelCoreTests/ExtensionCatalogTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,21 @@ final class ExtensionCatalogTests: XCTestCase {
refused: [.init(id: "broken", reason: "nope")])
}

func testArrowsWalkTheRowsAndStopAtTheEnds() {
// The chevron is drawn above the list and is a target like anything else, so
// it is what ↓ reaches first from nothing and what ↑ stops against.
func testArrowsWalkEveryTargetAndStopAtTheEnds() {
let c = catalog()
let rows = threeRows
XCTAssertEqual(rows.map(\.id), ["broken", "installed", "available"])
XCTAssertEqual(c.targets(among: rows),
[.back, .row("broken"), .row("installed"), .row("available")])

c.moveSelection(among: rows, by: 1)
XCTAssertEqual(c.selectedID, "broken")
c.moveSelection(among: rows, by: 1)
c.moveSelection(among: rows, by: 1)
XCTAssertEqual(c.selectedID, "available")
c.moveSelection(among: rows, by: 1)
XCTAssertEqual(c.selectedID, "available", "stops rather than wrapping")
XCTAssertEqual(c.selection, .back)
c.moveSelection(among: rows, by: -1)
XCTAssertEqual(c.selection, .back, "stops rather than wrapping")
for _ in 0..<4 { c.moveSelection(among: rows, by: 1) }
XCTAssertEqual(c.selectedID, "available", "and stops at the far end too")
}

func testUpFromNoSelectionTakesTheLastRow() {
Expand All @@ -179,6 +182,38 @@ final class ExtensionCatalogTests: XCTestCase {
XCTAssertEqual(c.selectedID, "available")
}

// Only while the fetch has failed, because that is the only time the button
// is drawn. Offering a ring to a button nobody can see is the same defect as
// drawing a button nothing can reach.
func testTryAgainIsATargetOnlyWhileTheFetchHasFailed() {
let loaded = catalog(fetch: { .success([]) })
loaded.reload()
XCTAssertEqual(loaded.targets(among: threeRows).contains(.retry), false)

let failed = catalog(fetch: { .failure(.downloadFailed("x")) })
failed.reload()
XCTAssertEqual(failed.targets(among: threeRows)[1], .retry,
"drawn between the chevron and the rows, which is where it sits")
}

// A failed fetch does not hide what is installed, so the page still has rows
// to seed onto; Try again is reached by walking up to it.
func testAFailedFetchStillSeedsOntoARow() {
let c = catalog(fetch: { .failure(.downloadFailed("x")) })
c.reload()
c.reconcileSelection(among: threeRows)
XCTAssertEqual(c.selectedID, "broken")
}

// With nothing installed and nothing published there is no row to land on,
// and Try again is the only thing on the page worth pressing.
func testAFailedFetchWithNoRowsSeedsOntoTryAgain() {
let c = catalog(fetch: { .failure(.downloadFailed("x")) })
c.reload()
c.reconcileSelection(among: [])
XCTAssertEqual(c.selection, .retry)
}

// Enter does whatever the row's own button would: install an uninstalled
// one, remove an installed one.
func testActivatingAnUninstalledRowInstallsIt() {
Expand All @@ -188,7 +223,7 @@ final class ExtensionCatalogTests: XCTestCase {
let c = catalog(fetch: { .success([self.entry("available")]) },
install: { installed.append($0.id); return .success($0.id) })
c.reload()
c.selectedID = "available"
c.selection = .row("available")
c.activateSelection(among: threeRows)
XCTAssertEqual(installed, ["available"])
}
Expand All @@ -201,7 +236,7 @@ final class ExtensionCatalogTests: XCTestCase {
func testActivatingAnInstalledRowDoesNotRemoveIt() {
var removed: [String] = []
let c = catalog(remove: { removed.append($0); return .success($0) })
c.selectedID = "installed"
c.selection = .row("installed")
c.activateSelection(among: threeRows)
XCTAssertTrue(removed.isEmpty)
XCTAssertNil(c.work["installed"])
Expand Down Expand Up @@ -232,7 +267,7 @@ final class ExtensionCatalogTests: XCTestCase {
let c = catalog(fetch: { .success([self.entry("available")]) },
install: { _ in attempts += 1; return .failure(.installFailed("no")) })
c.reload()
c.selectedID = "available"
c.selection = .row("available")
c.activateSelection(among: threeRows)
XCTAssertEqual(attempts, 1)
XCTAssertNotNil(c.failure(for: "available"))
Expand All @@ -253,18 +288,21 @@ final class ExtensionCatalogTests: XCTestCase {
// make Enter a no-op.
func testTheSelectionMovesToTheFirstRowWhenItsOwnRowDisappears() {
let c = catalog()
c.selectedID = "installed"
c.selection = .row("installed")
c.reconcileSelection(among: threeRows)
XCTAssertEqual(c.selectedID, "installed")

c.reconcileSelection(among: threeRows.filter { $0.id != "installed" })
XCTAssertEqual(c.selectedID, "broken", "lands somewhere rather than nowhere")
}

func testWithNoRowsAtAllThereIsNothingToSelect() {
// The row is gone, so the selection falls back to what the page still draws.
// Nothing is not an option: it leaves ⏎ advertised against no target.
func testWithNoRowsTheSelectionFallsBackToTheChevron() {
let c = catalog()
c.selectedID = "installed"
c.selection = .row("installed")
c.reconcileSelection(among: [])
XCTAssertEqual(c.selection, .back)
XCTAssertNil(c.selectedID)
}

Expand All @@ -288,17 +326,20 @@ final class ExtensionCatalogTests: XCTestCase {
XCTAssertEqual(seeded?.isInstalled, true)
}

func testCommandArrowsJumpToTheFirstAndLastRow() {
func testCommandArrowsJumpToTheFirstAndLastTarget() {
let c = catalog()
c.selectEdge(among: threeRows, top: false)
XCTAssertEqual(c.selectedID, "available")
c.selectEdge(among: threeRows, top: true)
XCTAssertEqual(c.selectedID, "broken")
XCTAssertEqual(c.selection, .back)
}

func testJumpingIsANoOpWithNoRows() {
// An empty catalogue still has its chevron, so ⌘↑↓ land somewhere rather
// than leaving the page with no ring at all.
func testJumpingWithNoRowsLandsOnTheChevron() {
let c = catalog()
c.selectEdge(among: [], top: true)
XCTAssertEqual(c.selection, .back)
XCTAssertNil(c.selectedID)
}

Expand Down Expand Up @@ -569,14 +610,14 @@ final class ExtensionCatalogTests: XCTestCase {
// written about, under a footer still advertising it.
func testAQueryThatHidesTheSelectedRowMovesItToWhatIsLeft() {
let c = catalog()
c.selectedID = "derby"
c.selection = .row("derby")
c.reconcileSelection(among: ExtensionCatalog.matching(sample, query: "system"))
XCTAssertEqual(c.selectedID, "system")
}

func testAQueryThatStillShowsTheSelectedRowKeepsIt() {
let c = catalog()
c.selectedID = "derby"
c.selection = .row("derby")
c.reconcileSelection(among: ExtensionCatalog.matching(sample, query: "derby"))
XCTAssertEqual(c.selectedID, "derby")
}
Expand Down
76 changes: 76 additions & 0 deletions Tests/StackNudgePanelCoreTests/FooterHintTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -623,3 +623,79 @@ final class ExtensionConfigFooterTests: XCTestCase {
}
}
}

// The extensions browser's bar. Its chevron and its Try again are targets now,
// so ⏎ means different things depending on where the ring is, and the bar has to
// say which. One hint per action, with ⏎ added to the selected one.
final class ExtensionsBrowserFooterTests: XCTestCase {

private func hints(selection: ExtensionCatalog.Target?,
activation: String = "Install",
updateSelected: Bool = false,
queryIsEmpty: Bool = true,
hasRows: Bool = true) -> [FooterHintSpec] {
ExtensionsView.footerHints(selection: selection, activation: activation,
updateSelected: updateSelected,
queryIsEmpty: queryIsEmpty, hasRows: hasRows)
}

private func labels(_ specs: [FooterHintSpec]) -> [String] { specs.map(\.label) }

private func keys(_ specs: [FooterHintSpec], _ label: String) -> [String]? {
specs.first { $0.label == label }?.keys
}

func test_onARow_theRowVerbTakesReturn() {
let specs = hints(selection: .row("derby"), activation: "Settings")
XCTAssertEqual(labels(specs), ["Back", "Search", "Select", "Settings", "Reload"])
XCTAssertEqual(keys(specs, "Settings"), ["⏎"])
XCTAssertEqual(keys(specs, "Reload"), ["⌘R"])
}

// The row verb names something the chevron will not do, so it goes rather
// than sitting there wrong.
func test_onTheChevron_returnRidesOnBackAndTheRowVerbGoes() {
let specs = hints(selection: .back, activation: "Install")
XCTAssertEqual(labels(specs), ["Back", "Search", "Select", "Reload"])
XCTAssertEqual(keys(specs, "Back"), ["⏎", "Esc"])
}

// Try again and Reload are one action, so the button being selected adds ⏎
// to the hint that is already there rather than printing a second one.
func test_onTryAgain_returnRidesOnReload() {
let specs = hints(selection: .retry)
XCTAssertEqual(labels(specs), ["Back", "Search", "Select", "Reload"])
XCTAssertEqual(keys(specs, "Reload"), ["⏎", "⌘R"])
}

// With a query typed, Esc clears it rather than leaving, so ⏎ on the chevron
// must not be advertised on a hint that now means something else.
func test_withAQueryTypedEscapeClearsAndKeepsReturnOffIt() {
let specs = hints(selection: .back, queryIsEmpty: false)
XCTAssertEqual(keys(specs, "Clear"), ["Esc"])
XCTAssertFalse(labels(specs).contains("Back"))
}

func test_noLabelIsAdvertisedTwice() {
for selection: ExtensionCatalog.Target? in [nil, .back, .retry, .row("derby")] {
let names = labels(hints(selection: selection))
XCTAssertEqual(Set(names).count, names.count, "duplicate in \(names)")
}
}

// An update pending takes ⏎ for the update, so the row's page needs ⌘⏎ or
// there is no keyboard route to it at all.
func test_anUpdatePendingAdvertisesTheRouteToTheRowsPage() {
let specs = hints(selection: .row("derby"), activation: "Update", updateSelected: true)
XCTAssertEqual(keys(specs, "Settings"), ["⌘⏎"])
}

// Nothing to walk, so the hint dims rather than disappearing: the bar must
// not reflow as a query filters the list down to nothing.
func test_selectDimsWhenThereIsNothingToWalk() {
XCTAssertEqual(hints(selection: .back, hasRows: false)
.first { $0.label == "Select" }?.dimmed, true)
XCTAssertEqual(hints(selection: .row("derby"), hasRows: true)
.first { $0.label == "Select" }?.dimmed, false)
}
}
Loading
Loading