fix navigation flow

This commit is contained in:
2026-08-07 00:28:24 -04:00
parent 9e7a44309a
commit c8d29ed59d
14 changed files with 924 additions and 484 deletions

View File

@@ -1,27 +1,33 @@
/**
* dispatch-keybinds.test.ts — yazi remake task 06 unit + integration tests.
* dispatch-keybinds.test.ts — yazi remake task 06/07 unit + integration tests.
*
* Exercises the rewired unified keybind router (`createDispatcher`) directly,
* without the OpenTUI render tree, mirroring how task 01 made the nav store a
* plain factory for `bun test`. Covers the task 06 acceptance + integration
* cases:
* cases, plus the tab-root routing:
*
* • Unit: `dispatch("move-down")` on a depth-tab current pane emits
* `nav.action { action: "move-down" }` pointing at the current pane only.
* • Integration: `swipe-next` (l) on a depth-tab at depth 0 emits `open`
* (drill); `swipe-prev` (h) at depth 1 pops to depth 0; `swipe-prev` at
* depth 0 is an inert noop (no emit, no pane/depth change, no error).
* • Acceptance: digit keys (`tab-goto-N`) switch tabs and focus lands on
* `DEPTH_CENTER_PANE`; `tab-next`/`tab-prev` cycle; `h` at depth 0 is
* inert; `j`/`k` (move-down/up) flow to `nav.action` for the current pane.
* • Tab root: j/k (`move-down`/`move-up`) move a tab cursor without touching
* the active tab; `open`/`swipe-next` (l/Enter) open the hovered tab and
* enter its content; `swipe-prev` (h) stays inert (out of the panes).
* • Depth-tab content: `swipe-next` (l) at depth 0 emits `open` (drill);
* `swipe-prev` (h) pops depth 1→0 and, at depth 0, returns to the tab root.
* • Fixed-pane tabs (Search/Player, special): `h`/`l` swipe [1, paneCount];
* `h` on the first pane stays — never overflows to the tab root.
* • Digit keys (`tab-goto-N`), `tab-next` (`]`), `tab-prev` (`[`) switch
* tabs and preserve focus context (root stays root for depth-tabs, content
* stays content).
* • `j`/`k` (move-down/up) in content flow to `nav.action` for the current
* pane.
*
* The dispatcher is built with fake audio/k/help deps (the drills/moves under
* test never reach the audio or advanceEpisode paths) and a real nav store
* the shared contract depth/l movement routes through.
* The dispatcher is built with fake audio/k/help deps (the paths under test
* never reach the audio or advanceEpisode branches) and a real nav store.
*/
import { test, expect, mock } from "bun:test";
import { createRoot } from "solid-js";
import { createNavigation, DEPTH_CENTER_PANE } from "../src/context/navigation-store";
import {
createNavigation,
DEPTH_CENTER_PANE,
} from "../src/context/navigation-store";
import { TABS } from "../src/utils/navigation";
import { createDispatcher, type DispatcherDeps } from "../src/utils/dispatch";
import { on } from "../src/utils/event-bus";
@@ -30,16 +36,17 @@ import type { KeybindActionName } from "../src/context/KeybindContext";
/** Build a real nav store + a dispatcher wired to fake audio/k/help deps, all
* inside a reactive root (disposed after). Returns both so a test can read
* nav state and call dispatch. */
function withHarness(fn: (api: {
nav: ReturnType<typeof createNavigation>;
dispatch: (action: KeybindActionName) => void;
toggleHelp: () => boolean;
helpOpen: () => boolean;
}) => void) {
function withHarness(
fn: (api: {
nav: ReturnType<typeof createNavigation>;
dispatch: (action: KeybindActionName) => void;
toggleHelp: () => boolean;
helpOpen: () => boolean;
}) => void,
) {
createRoot((dispose) => {
const nav = createNavigation();
let help = false;
const toggleHelp = () => (help = !help);
const deps: DispatcherDeps = {
nav,
audio: {
@@ -57,7 +64,7 @@ function withHarness(fn: (api: {
fn({
nav,
dispatch: (action) => dispatch(action, evt() as any),
toggleHelp,
toggleHelp: () => (help = !help),
helpOpen: () => help,
});
dispose();
@@ -78,10 +85,66 @@ function captureNavActions(fn: () => void) {
return captured;
}
// ── Unit: move-down emits nav.action on the current pane only ─────────────────
// ── Tab root: j/k move the cursor; l/Enter open the hovered tab ──────────────
test("dispatch('move-down') on the tab root moves the cursor (no emit, active tab untouched)", () => {
withHarness(({ nav, dispatch }) => {
expect(nav.atRootTab()).toBe(true);
expect(nav.tabCursor()).toBe(TABS.FEED);
const events = captureNavActions(() => dispatch("move-down"));
expect(events).toHaveLength(0); // j on the root moves the cursor only
expect(nav.tabCursor()).toBe(TABS.MYSHOWS);
expect(nav.activeTab()).toBe(TABS.FEED); // active tab untouched until opened
expect(nav.atRootTab()).toBe(true);
});
});
test("dispatch('move-up') on the tab root moves the cursor up (clamped, no wrap)", () => {
withHarness(({ nav, dispatch }) => {
expect(nav.tabCursor()).toBe(TABS.FEED);
dispatch("move-up");
expect(nav.tabCursor()).toBe(TABS.FEED); // clamped at the top
expect(nav.activeTab()).toBe(TABS.FEED);
expect(nav.atRootTab()).toBe(true);
});
});
test("dispatch('open') on the tab root opens the hovered tab and enters its content", () => {
withHarness(({ nav, dispatch }) => {
dispatch("move-down"); // cursor -> MYSHOWS
dispatch("move-down"); // cursor -> DISCOVER
expect(nav.tabCursor()).toBe(TABS.DISCOVER);
dispatch("open");
expect(nav.activeTab()).toBe(TABS.DISCOVER); // the hovered tab is opened
expect(nav.atRootTab()).toBe(false);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
});
});
test("dispatch('swipe-next') on the tab root opens the hovered tab and enters its content (no emit)", () => {
withHarness(({ nav, dispatch }) => {
dispatch("move-down"); // cursor -> MYSHOWS
const events = captureNavActions(() => dispatch("swipe-next"));
expect(events).toHaveLength(0);
expect(nav.activeTab()).toBe(TABS.MYSHOWS);
expect(nav.atRootTab()).toBe(false);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
});
});
test("dispatch('swipe-prev') on the tab root is inert (stays, no emit)", () => {
withHarness(({ nav, dispatch }) => {
const events = captureNavActions(() => dispatch("swipe-prev"));
expect(events).toHaveLength(0);
expect(nav.atRootTab()).toBe(true);
});
});
// ── Unit: move-down emits nav.action on the current pane only ────────────────
test("dispatch('move-down') on a depth-tab current pane emits nav.action {action:'move-down'} on the current pane", () => {
withHarness(({ nav, dispatch }) => {
nav.setActiveTab(TABS.FEED); // depth-tab → focus is the center pane
nav.setActiveTab(TABS.FEED); // depth-tab → enter content to test the list move
nav.enterTabContent();
expect(nav.isDepthTab()).toBe(true);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
@@ -96,21 +159,22 @@ test("dispatch('move-down') on a depth-tab current pane emits nav.action {action
test("dispatch('move-up') emits nav.action on the current pane only (j/k never change depth)", () => {
withHarness(({ nav, dispatch }) => {
nav.setActiveTab(TABS.MYSHOWS);
nav.enterTabContent();
const beforeDepth = nav.currentDepth();
const beforePane = nav.activePane();
const events = captureNavActions(() => dispatch("move-up"));
expect(events).toHaveLength(1);
expect(events[0].action).toBe("move-up");
expect(events[0].pane).toBe(beforePane);
expect(events[0].pane).toBe(DEPTH_CENTER_PANE);
// depth is untouched by j/k (only h/l and the page's open() touch it).
expect(nav.currentDepth()).toBe(beforeDepth);
});
});
// ── Integration: l drills (open emit), h pops, h@0 noop ──────────────────────
// ── Integration: l drills (open emit), h pops, h@0 → tab root ────────────────
test("dispatch('swipe-next') on a depth-tab at depth 0 emits 'open' (drill)", () => {
withHarness(({ nav, dispatch }) => {
nav.setActiveTab(TABS.DISCOVER);
nav.enterTabContent();
expect(nav.isDepthTab()).toBe(true);
expect(nav.currentDepth()).toBe(0);
@@ -126,6 +190,7 @@ test("dispatch('swipe-next') on a depth-tab at depth 0 emits 'open' (drill)", ()
test("dispatch('swipe-prev') at depth 1 pops to depth 0", () => {
withHarness(({ nav, dispatch }) => {
nav.setActiveTab(TABS.FEED);
nav.enterTabContent();
// simulate the page's open() having drilled one level.
nav.pushDepth({ kind: "episodes:f1", ctx: "f1", focus: 0 });
expect(nav.currentDepth()).toBe(1);
@@ -133,54 +198,105 @@ test("dispatch('swipe-prev') at depth 1 pops to depth 0", () => {
const events = captureNavActions(() => dispatch("swipe-prev"));
expect(events).toHaveLength(0); // a pop emits nothing — it just pops
expect(nav.currentDepth()).toBe(0);
// focus stays in content (depth > 0 pop does not return to the root).
expect(nav.atRootTab()).toBe(false);
});
});
test("dispatch('swipe-prev') at depth 0 is an inert noop (no emit, no pane/depth change, no error)", () => {
test("dispatch('swipe-prev') at depth 0 returns focus to the tab root", () => {
withHarness(({ nav, dispatch }) => {
nav.setActiveTab(TABS.SETTINGS);
nav.enterTabContent();
expect(nav.currentDepth()).toBe(0);
const paneBefore = nav.activePane();
expect(nav.atRootTab()).toBe(false);
const events = captureNavActions(() => dispatch("swipe-prev"));
expect(events).toHaveLength(0);
expect(nav.currentDepth()).toBe(0);
expect(nav.activePane()).toBe(paneBefore);
expect(nav.atRootTab()).toBe(true);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
});
});
// ── Acceptance: digit keys switch tabs and land focus on the current pane ──
test("tab-goto-N switches tabs; focus always lands on DEPTH_CENTER_PANE", () => {
test("dispatch('swipe-prev') on a fixed-pane tab at pane 1 stays (no tab overflow)", () => {
withHarness(({ nav, dispatch }) => {
// start on FEED (depth-tab, depth 0).
expect(nav.activeTab()).toBe(TABS.FEED);
nav.setActiveTab(TABS.SEARCH); // fixed-pane
nav.enterTabContent();
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
dispatch("swipe-prev");
// special tab: h on the first content pane does not return to the root.
expect(nav.atRootTab()).toBe(false);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
});
});
test("dispatch('swipe-prev') on a fixed-pane tab at pane > 1 swipes leftwards", () => {
withHarness(({ nav, dispatch }) => {
nav.setActiveTab(TABS.SEARCH); // 3 panes
nav.enterTabContent();
nav.swipe(1, 3);
nav.swipe(1, 3);
expect(nav.activePane()).toBe(3);
dispatch("swipe-prev");
expect(nav.activePane()).toBe(2);
});
});
// ── Acceptance: digit keys switch tabs and keep focus context ────────────────
test("tab-goto-N from the root keeps depth-tabs at the root; special tabs open", () => {
withHarness(({ nav, dispatch }) => {
// focus starts on the tab root.
expect(nav.atRootTab()).toBe(true);
dispatch("tab-goto-3"); // → Discover
expect(nav.activeTab()).toBe(TABS.DISCOVER);
expect(nav.tabCursor()).toBe(TABS.DISCOVER); // cursor re-synced
expect(nav.atRootTab()).toBe(true);
dispatch("tab-goto-2"); // → MyShows
expect(nav.activeTab()).toBe(TABS.MYSHOWS);
expect(nav.tabCursor()).toBe(TABS.MYSHOWS);
expect(nav.atRootTab()).toBe(true);
// fixed-pane tab is special: switching from the root opens its content.
dispatch("tab-goto-4"); // → Search
expect(nav.activeTab()).toBe(TABS.SEARCH);
expect(nav.tabCursor()).toBe(TABS.SEARCH);
expect(nav.atRootTab()).toBe(false);
});
});
test("tab-goto-N from content keeps focus in the active tab's content", () => {
withHarness(({ nav, dispatch }) => {
nav.enterTabContent();
expect(nav.atRootTab()).toBe(false);
dispatch("tab-goto-3"); // → Discover
expect(nav.activeTab()).toBe(TABS.DISCOVER);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
dispatch("tab-goto-2"); // → MyShows
expect(nav.activeTab()).toBe(TABS.MYSHOWS);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
// fixed-pane tab also lands on pane 0.
dispatch("tab-goto-4"); // → Search
dispatch("tab-goto-4"); // → Search (fixed-pane) lands its current pane
expect(nav.activeTab()).toBe(TABS.SEARCH);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
expect(nav.atRootTab()).toBe(false);
});
});
test("tab-next (]) / tab-prev ([) cycle tabs and reset focus to the current pane", () => {
test("tab-next (]) / tab-prev ([) cycle tabs and keep focus context", () => {
withHarness(({ nav, dispatch }) => {
nav.setActiveTab(TABS.FEED);
expect(nav.activeTab()).toBe(TABS.FEED);
expect(nav.atRootTab()).toBe(true);
dispatch("tab-next");
expect(nav.activeTab()).toBe(TABS.MYSHOWS);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
expect(nav.tabCursor()).toBe(TABS.MYSHOWS);
expect(nav.atRootTab()).toBe(true);
dispatch("tab-prev");
expect(nav.activeTab()).toBe(TABS.FEED);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
expect(nav.tabCursor()).toBe(TABS.FEED);
expect(nav.atRootTab()).toBe(true);
});
});

View File

@@ -1,12 +1,20 @@
/**
* nav-model.test.ts — yazi remake task 01 unit/integration tests.
* nav-model.test.ts — yazi remake task 01/07 unit/integration tests.
*
* Covers the removal of the SIDEBAR_PANE concept:
* Covers the tab-list-as-root navigation model:
* • createNavigation() exposes the nav factory directly (no Solid render
* needed), wrapped in a createRoot so effects register/dispose.
* • focus starts on the tab list — the app root. `atRootTab()` is true while
* the tab list is the CURRENT pane (nothing above it). `enterTabContent()`
* slides the tab into UP and puts focus on the content; `backToTabRoot()`
* returns to the root. Only depth-tabs participate (`atRootTab()` is false
* for the fixed-pane Search/Player tabs).
* • the root tab list is a normal list: `tabCursor` is independent of
* `activeTab`; moveTabCursor moves it (clamped), activateTabCursor opens
* the hovered tab + enters content, and direct tab switches re-sync it.
* • depth-tab focusedIndex depth-current read/writes the top frame's focus.
* • the tab-switch effect resets activePane to DEPTH_CENTER_PANE (0), not -1.
* • swipe() clamps to [0, paneCount-1] (no -1 sidebar slot).
* • swipe() is clamped to [1, paneCount] (content panes only; there is no
* pane-0 tab slot — the tab root is a flag, not a pane).
*/
import { test, expect } from "bun:test";
import { createRoot } from "solid-js";
@@ -27,17 +35,18 @@ function withNav(fn: (nav: ReturnType<typeof createNavigation>) => void) {
});
}
test("createNavigation initial activePane is DEPTH_CENTER_PANE (0), not -1", () => {
test("createNavigation starts on the tab root (atRootTab true)", () => {
withNav((nav) => {
expect(nav.atRootTab()).toBe(true);
expect(nav.activeTab()).toBe(TABS.FEED);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
expect(nav.activePane()).toBe(0);
});
});
// ── depth-tab focus: reads/writes the top frame's focus ───────────────────────
test("depth-tab focusedIndex(DEPTH_CENTER_PANE) returns top frame's focus", () => {
withNav((nav) => {
// FEED is a depth-tab; its root frame is { kind: "feeds", focus: 0 }.
// FeeD is a depth-tab; its root frame is a the top frame on the stack.
nav.setActiveTab(TABS.FEED);
expect(nav.isDepthTab()).toBe(true);
expect(nav.focusedIndex(DEPTH_CENTER_PANE)).toBe(0);
@@ -52,6 +61,7 @@ test("depth-tab focusedIndex(DEPTH_CENTER_PANE) returns top frame's focus", () =
test("setFocusedIndex on a 2-frame stack writes only the top frame", () => {
withNav((nav) => {
nav.setActiveTab(TABS.FEED);
nav.enterTabContent();
// root frame focus 3, then push a child frame whose focus is 5.
nav.setFocusedIndex(DEPTH_CENTER_PANE, 3);
nav.pushDepth({ kind: "episodes:feedId", ctx: "f1", focus: 5 });
@@ -79,19 +89,120 @@ test("popDepth at depth 0 is a noop (returns false, no frame lost)", () => {
});
});
// ── tab-switch resets focus to DEPTH_CENTER_PANE, not a sidebar ──────────────
test("tab-switch effect resets activePane to DEPTH_CENTER_PANE", () => {
// ── tab switching keeps focus context ────────────────────────────────────────
test("tab switch keeps focus context: at the root it stays at the root", () => {
withNav((nav) => {
// start on a depth-tab, land on the current pane.
// focus starts on the tab root.
expect(nav.atRootTab()).toBe(true);
// switching depth-tabs from the root must not drop focus into content.
nav.setActiveTab(TABS.FEED);
expect(nav.atRootTab()).toBe(true);
expect(nav.activeTab()).toBe(TABS.FEED);
nav.setActiveTab(TABS.MYSHOWS);
expect(nav.atRootTab()).toBe(true);
});
});
test("tab switch keeps focus context: in content it stays in content", () => {
withNav((nav) => {
nav.enterTabContent();
expect(nav.atRootTab()).toBe(false);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
// move pane focus away (swipe is a noop for depth-tabs count=1, so
// instead prove the effect resets on tab change).
nav.setActiveTab(TABS.SEARCH);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
// switch to another tab; the effect must reset to 0, never -1.
// switching tabs from content keeps the content context.
nav.setActiveTab(TABS.SETTINGS);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
expect(nav.activePane()).toBeGreaterThanOrEqual(0);
expect(nav.atRootTab()).toBe(false);
});
});
test("switching to a Search/Player tab leaves the root (special content)", () => {
withNav((nav) => {
// at root, opening Search is special: atRootTab() reports false because
// Search has its own content and never renders the tab-list root view.
nav.setActiveTab(TABS.SEARCH);
expect(nav.atRootTab()).toBe(false);
nav.enterTabContent();
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
expect(nav.atRootTab()).toBe(false);
});
});
// ── tab root <-> content transitions ─────────────────────────────────────────
test("enterTabContent/backToTabRoot round-trip between root and content", () => {
withNav((nav) => {
nav.setActiveTab(TABS.FEED);
expect(nav.atRootTab()).toBe(true);
nav.enterTabContent();
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
expect(nav.atRootTab()).toBe(false);
nav.backToTabRoot();
expect(nav.atRootTab()).toBe(true);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
});
});
test("enterTabContent preserves the active tab's depth", () => {
withNav((nav) => {
nav.setActiveTab(TABS.FEED);
nav.pushDepth({ kind: "episodes:feedId", ctx: "f1", focus: 0 });
expect(nav.currentDepth()).toBe(1);
// moving between the root and content never touches the depth stack.
nav.backToTabRoot();
nav.enterTabContent();
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
expect(nav.currentDepth()).toBe(1);
expect(nav.popDepth()).toBe(true);
expect(nav.currentDepth()).toBe(0);
});
});
test("tabCursor starts on the active tab", () => {
withNav((nav) => {
expect(nav.tabCursor()).toBe(TABS.FEED);
expect(nav.activeTab()).toBe(TABS.FEED);
});
});
test("moveTabCursor moves the cursor without changing the active tab, clamped at the ends", () => {
withNav((nav) => {
nav.setActiveTab(TABS.MYSHOWS); // cursor syncs to the active tab
expect(nav.tabCursor()).toBe(TABS.MYSHOWS);
nav.moveTabCursor(1);
expect(nav.tabCursor()).toBe(TABS.DISCOVER);
expect(nav.activeTab()).toBe(TABS.MYSHOWS); // active tab untouched
nav.moveTabCursor(-1);
expect(nav.tabCursor()).toBe(TABS.MYSHOWS);
// clamp: from FEED, up stays FEED; from SETTINGS, down stays SETTINGS.
nav.moveTabCursor(-1); // MYSHOWS -> FEED
nav.moveTabCursor(-1); // FEED -> FEED (clamped)
expect(nav.tabCursor()).toBe(TABS.FEED);
nav.setActiveTab(TABS.SETTINGS);
nav.moveTabCursor(1); // SETTINGS -> SETTINGS (clamped)
expect(nav.tabCursor()).toBe(TABS.SETTINGS);
expect(nav.activeTab()).toBe(TABS.SETTINGS);
});
});
test("activateTabCursor switches to the hovered tab and enters its content", () => {
withNav((nav) => {
nav.moveTabCursor(1); // cursor -> MYSHOWS
nav.moveTabCursor(1); // cursor -> DISCOVER
expect(nav.tabCursor()).toBe(TABS.DISCOVER);
expect(nav.activeTab()).toBe(TABS.FEED);
nav.activateTabCursor();
expect(nav.activeTab()).toBe(TABS.DISCOVER); // hovered tab opened
expect(nav.tabCursor()).toBe(TABS.DISCOVER);
expect(nav.atRootTab()).toBe(false);
expect(nav.activePane()).toBe(DEPTH_CENTER_PANE);
});
});
test("direct tab switches re-sync the tab cursor", () => {
withNav((nav) => {
nav.moveTabCursor(1); // cursor -> MYSHOWS
expect(nav.activeTab()).toBe(TABS.FEED);
nav.setActiveTab(TABS.SETTINGS);
expect(nav.tabCursor()).toBe(TABS.SETTINGS);
});
});
@@ -105,37 +216,44 @@ test("tab-switch resets mode/visual/command state", () => {
});
});
// ── swipe clamps to [0, paneCount-1] (no sidebar slot) ────────────────────────
test("swipe(-1, 3) on a fixed-pane tab clamps to 0, not -1", () => {
// ── swipe clamps to [1, paneCount] (no pane-0 tab slot) ──────────────────────
test("swipe on a fixed-pane tab stays within [1, paneCount]", () => {
withNav((nav) => {
nav.setActiveTab(TABS.SEARCH); // fixed-pane, TabPaneCount = 3
expect(TabPaneCount[TABS.SEARCH]).toBe(3);
// tab-switch effect lands us on pane 0 (DEPTH_CENTER_PANE).
expect(nav.activePane()).toBe(0);
nav.swipe(-1, TabPaneCount[TABS.SEARCH]);
expect(nav.activePane()).toBe(0); // lower bound, never -1
// swipe right twice then back: clamps to [0, 2].
nav.swipe(1, 3);
nav.swipe(1, 3);
expect(nav.activePane()).toBe(2); // upper bound
nav.swipe(1, 3);
expect(nav.activePane()).toBe(2); // never exceeds count-1
nav.swipe(-1, 3);
nav.enterTabContent();
expect(nav.activePane()).toBe(1);
// swipe left stays at 1 (no pane 0).
nav.swipe(-1, TabPaneCount[TABS.SEARCH]);
expect(nav.activePane()).toBe(1);
nav.swipe(-1, TabPaneCount[TABS.SEARCH]);
expect(nav.activePane()).toBe(1);
// swipe right up through the columns, then hold the upper bound.
nav.swipe(1, TabPaneCount[TABS.SEARCH]);
expect(nav.activePane()).toBe(2);
nav.swipe(1, TabPaneCount[TABS.SEARCH]);
expect(nav.activePane()).toBe(3);
nav.swipe(1, TabPaneCount[TABS.SEARCH]);
expect(nav.activePane()).toBe(3); // never exceeds paneCount
nav.swipe(-1, TabPaneCount[TABS.SEARCH]);
expect(nav.activePane()).toBe(2);
nav.swipe(-1, TabPaneCount[TABS.SEARCH]);
expect(nav.activePane()).toBe(1);
nav.swipe(-1, TabPaneCount[TABS.SEARCH]);
expect(nav.activePane()).toBe(1);
nav.swipe(-1, 3);
expect(nav.activePane()).toBe(0);
});
});
test("swipe on a single-pane fixed tab stays at 0", () => {
test("swipe on a single-pane fixed tab stays at its one content pane", () => {
withNav((nav) => {
nav.setActiveTab(TABS.PLAYER); // single-pane
expect(TabPaneCount[TABS.PLAYER]).toBe(1);
expect(nav.activePane()).toBe(0);
nav.enterTabContent(); // lands on its one content pane (1)
expect(nav.activePane()).toBe(1);
nav.swipe(1, TabPaneCount[TABS.PLAYER]);
expect(nav.activePane()).toBe(0);
expect(nav.activePane()).toBe(1); // upper bound
nav.swipe(-1, TabPaneCount[TABS.PLAYER]);
expect(nav.activePane()).toBe(0);
expect(nav.activePane()).toBe(1); // lower bound — never drops to a tab 0
});
});