Navigation up/back split, faster startup, and CI versionCode fix
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m57s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
Build & Release / Run Tests (push) Successful in 5m13s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m30s
Build & Release / Build Linux (push) Successful in 17m52s
Build & Release / Build Android (push) Failing after 58s
Build & Release / Create Release (push) Has been skipped

Navigation:
- Split conflated "back" into navigateUp (deterministic route parent) and a
  history-safe navigateBack that tracks in-app depth via afterNavigate instead
  of history.length. Fixes the resume-from-background trap where a stale WebView
  stack left the header arrow stuck on the current page.
- /library self-corrects for music/tv/movies (which have dedicated landing
  pages): a leftover currentLibrary no longer forces the inline content-list
  view, so "up"/back shows the libraries overview. Live TV / channels / other
  types still render inline.

Startup (unblock first paint):
- auth.initialize() no longer awaits security-status, player-config, or session
  verification before flipping isInitialized. These run fire-and-forget after the
  session is restored, so the library overview paints without waiting on several
  serial IPC round-trips.

Versioning / CI:
- tauri.conf.json + package.json aligned to 0.0.15 (the tag series had drifted to
  0.1.0, whose formula-derived versionCode 1000 outran the v0.0.x tags).
- Release workflow now pins a monotonic Android versionCode
  (1000 + major*10000 + minor*100 + patch) so tagged builds never downgrade
  below prior installs and always increase in semver order.

Tests: navigation (4), auth (29), playbackMode (23) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 21:12:36 +02:00
co-authored by Claude Opus 4.8
parent 1992a8187d
commit 2e479d05b3
13 changed files with 278 additions and 94 deletions
+76 -16
View File
@@ -1,35 +1,95 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { navigateBack } from "./navigation";
const goto = vi.fn();
// Capture the afterNavigate callback so tests can simulate navigations and thus
// drive the in-app depth counter that canGoBack/navigateBack rely on.
let afterNavigateCb: ((nav: { from: unknown; to: unknown; delta?: number }) => void) | null =
null;
vi.mock("$app/navigation", () => ({
goto: (...args: unknown[]) => goto(...args),
afterNavigate: (cb: (nav: any) => void) => {
afterNavigateCb = cb;
},
}));
describe("navigateBack", () => {
import {
navigateUp,
navigateBack,
canGoBack,
registerNavigationTracking,
__resetNavigationDepthForTest,
} from "./navigation";
/** Simulate a SvelteKit navigation to move the depth counter. */
function nav(opts: { from?: boolean; delta?: number }) {
afterNavigateCb?.({
from: opts.from === false ? null : {},
to: {},
delta: opts.delta,
});
}
describe("navigation", () => {
beforeEach(() => {
goto.mockClear();
// registerNavigationTracking is idempotent; the first call in the suite wins
// and wires afterNavigateCb. Ensure it is registered, then reset depth so
// each case starts from the entry page (module state persists otherwise).
registerNavigationTracking();
__resetNavigationDepthForTest();
});
it("pops real history when there is in-app history to go back to", () => {
const back = vi.fn();
vi.spyOn(history, "back").mockImplementation(back);
vi.spyOn(history, "length", "get").mockReturnValue(3);
describe("navigateUp", () => {
it("always goes to the given parent path, never touching history", () => {
const back = vi.fn();
vi.spyOn(history, "back").mockImplementation(back);
navigateBack("/library");
navigateUp("/library/music");
expect(back).toHaveBeenCalledOnce();
expect(goto).not.toHaveBeenCalled();
expect(goto).toHaveBeenCalledWith("/library/music");
expect(back).not.toHaveBeenCalled();
});
});
it("falls back to the given path on a fresh deep-link (no history)", () => {
const back = vi.fn();
vi.spyOn(history, "back").mockImplementation(back);
vi.spyOn(history, "length", "get").mockReturnValue(1);
describe("navigateBack / canGoBack", () => {
it("falls back to the path when there is no in-app history yet", () => {
// Fresh session: only the initial load happened (from == null), so depth
// stays at 0 and there is nothing to pop.
nav({ from: false });
expect(canGoBack()).toBe(false);
navigateBack("/library/music");
const back = vi.fn();
vi.spyOn(history, "back").mockImplementation(back);
expect(goto).toHaveBeenCalledWith("/library/music");
expect(back).not.toHaveBeenCalled();
navigateBack("/library");
expect(goto).toHaveBeenCalledWith("/library");
expect(back).not.toHaveBeenCalled();
});
it("pops history after a real in-app forward navigation", () => {
nav({ from: false }); // initial load
nav({}); // navigated deeper within the app
expect(canGoBack()).toBe(true);
const back = vi.fn();
vi.spyOn(history, "back").mockImplementation(back);
navigateBack("/library");
expect(back).toHaveBeenCalledOnce();
expect(goto).not.toHaveBeenCalled();
});
it("does not go below zero when the user backs out to the entry page", () => {
nav({ from: false }); // load
nav({}); // forward → depth 1
nav({ delta: -1 }); // back → depth 0
nav({ delta: -1 }); // extra back (e.g. stale delta) must not underflow
expect(canGoBack()).toBe(false);
navigateBack("/library/music");
expect(goto).toHaveBeenCalledWith("/library/music");
});
});
});