fix(nav): the address keeps what the app rewrites around it (review round 3)

- The Feed press over a live search ends only the search: the feed's own view stays named.
- Leaving a search steps back only off an entry the app pushed (_ytOwn); one a link or a reload
  opened is cleared in place, also where the Navigation API is missing.
- useUrlMirror rewrites a changed view through replaceView, so a reload's overlay marks go; its
  first write and its landing writes keep them for an overlay that reopens.
- Files and Playlists: a box emptied and filled again settles at once, never naming the ended search.
- Playlists: a linked player is scoped to the playlist the store started with, and only the rail's
  own move to the first playlist is no pick.
- Contract: writer objects are scanned to their matching brace, a computed or shorthand key is
  refused, and every useHistorySubview call hands a named definition.
- Lint: playwright/no-skipped-test also refuses fixme (gate snapshot regenerated).
- FEED_DEFAULT_SORT at the three remaining sites; linkedNotListed says to search by name.
This commit is contained in:
2026-10-27 23:33:22 +01:00
parent 66e427dfa4
commit 02906d0acb
18 changed files with 282 additions and 53 deletions
+1 -1
View File
@@ -78,7 +78,7 @@
{ "allowShortCircuit": false, "allowTaggedTemplates": false, "allowTernary": false }
],
"playwright/no-focused-test": [2],
"playwright/no-skipped-test": [2]
"playwright/no-skipped-test": [2, { "allowConditional": false, "disallowFixme": true }]
},
"settings": null
},
@@ -0,0 +1,27 @@
// @vitest-environment jsdom
//
// A reload under an overlay that reopens (the player) loads on the overlay's entry, its mark on it,
// and the overlay takes that entry over as it opens again. A page's first mirror write on that load
// (the defaults its address did not name yet) is the same view, not another: the mark stays, and the
// reopened overlay still takes its entry over rather than pushing one more. Its own file: the entry
// is read as the history module loads, so it is set up before the import.
import { describe, expect, it } from "vitest";
import { act, renderHook } from "@testing-library/react";
describe("a page's first mirror write on the entry a reload under an overlay started from", () => {
it("leaves the entry to the overlay that reopens", async () => {
window.history.replaceState({ sfPage: "activity" }, "", "/?page=activity");
window.history.pushState({ sfPage: "activity", _ov: 1, _ovd: 1 }, "", "/?page=activity");
const { useUrlMirror } = await import("./useUrlMirror");
const { useBackToClose } = await import("./history");
renderHook(() => useUrlMirror({ at: "runs" }));
expect(new URLSearchParams(window.location.search).get("at")).toBe("runs");
const depth = window.history.length;
renderHook(() => useBackToClose(() => {}));
await act(async () => {
await Promise.resolve();
});
expect(window.history.length).toBe(depth);
});
});
@@ -0,0 +1,39 @@
// @vitest-environment jsdom
//
// A reload under a dialog that does not reopen (About) loads on the dialog's entry, its overlay mark
// still on it. A page's view mirrored onto that entry (another tab) is another view: the next overlay
// pushes its own entry over it rather than taking this one over — taken over, closing it stepped back
// onto the entry below, and the page read its old view back from that address. Its own file: the
// entry is read as the history module loads, so it is set up before the import.
import { describe, expect, it } from "vitest";
import { act, renderHook } from "@testing-library/react";
describe("a view mirrored onto the entry a reload under a dialog started from", () => {
it("is where an overlay over it closes to", async () => {
window.history.replaceState({ sfPage: "activity" }, "", "/?page=activity&at=runs");
window.history.pushState(
{ sfPage: "activity", _ov: 1, _ovd: 1 },
"",
"/?page=activity&at=runs",
);
const { useUrlMirror } = await import("./useUrlMirror");
const { useBackToClose } = await import("./history");
const mirror = renderHook(({ at }) => useUrlMirror({ at }), { initialProps: { at: "runs" } });
mirror.rerender({ at: "admin" });
expect(new URLSearchParams(window.location.search).get("at")).toBe("admin");
const depth = window.history.length;
const overlay = renderHook(() => useBackToClose(() => {}));
await act(async () => {
await Promise.resolve();
});
expect(window.history.length).toBe(depth + 1);
await act(async () => {
const landed = new Promise((res) => window.addEventListener("popstate", res, { once: true }));
overlay.unmount();
await landed;
});
expect(new URLSearchParams(window.location.search).get("at")).toBe("admin");
});
});
+12 -2
View File
@@ -1,4 +1,5 @@
import { useEffect, useEffectEvent, useState } from "react";
import { useEffect, useEffectEvent, useRef, useState } from "react";
import { replaceView } from "./history";
import { urlWithParams } from "./urlState";
// Mirror a set of nav params onto the CURRENT history entry (replaceState — mirroring is not a new
@@ -19,11 +20,20 @@ export function useUrlMirror(updates: Record<string, string | null>): void {
// The object itself is read through an effect event, so the effect's reactive inputs are the
// serialized value and the landing count — which is exactly what the dep list says: an effect event
// for the identity that churns, primitives for the triggers.
// A change of what this mirror writes, after its first write, is another view on the same entry
// (replaceView: it drops the marks a reload under an overlay left). Its first write on a mount and
// its write on a landing name the view the entry already shows, so the marks stay: an overlay that
// reopens after a reload still finds its entry.
const written = useRef<string | null>(null);
const mirror = useEffectEvent(() => {
const href = urlWithParams(updates);
const changed = written.current != null && written.current !== dep;
written.current = dep;
// Every mirror runs on every landing: one with nothing to change writes nothing (a browser
// throttles a page that rewrites its address too often).
if (href !== urlWithParams({})) window.history.replaceState(window.history.state, "", href);
if (href === urlWithParams({})) return;
if (changed) replaceView(window.history.state ?? {}, href);
else window.history.replaceState(window.history.state, "", href);
});
// Counted in state, on a task after the popstate dispatch: every listener of it has run by then —
// a store that reads its keys back from the landed address among them (Activity) — and so has the