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:
@@ -86,19 +86,21 @@ function sources(dir: string): string[] {
|
||||
}
|
||||
|
||||
// The object literals handed to an address writer: `urlWithParams` / `useUrlMirror`, an overlay's
|
||||
// own keys (ui/Modal's `url`, `useBackToClose`'s).
|
||||
// own keys (ui/Modal's `url`, `useBackToClose`'s). Each pattern ends at the literal's opening brace;
|
||||
// the literal runs to its matching close (`writerObjects`), nested arguments and all.
|
||||
const WRITER_OBJECTS = [
|
||||
/(?:urlWithParams|useUrlMirror)\(\{([^}]*)\}/g,
|
||||
/\burl=\{\{([^}]*)\}/g,
|
||||
/(?:urlWithParams|useUrlMirror)\(\{/g,
|
||||
/\burl=\{\{/g,
|
||||
// Its first argument a name, a call or an arrow (`() => setOpen(false)`).
|
||||
/useBackToClose\((?:[^,()]|\([^()]*\))+,\s*\{([^}]*)\}/g,
|
||||
/useBackToClose\((?:[^,()]|\([^()]*\))+,\s*\{/g,
|
||||
];
|
||||
// One of each form: a regex that matched nothing in the tree would pass every scan.
|
||||
const WRITER_FIXTURE = `
|
||||
useUrlMirror({ fxa: x });
|
||||
useUrlMirror({ fxa: x, fxf });
|
||||
<Modal url={{ fxb: String(id) }} />
|
||||
useBackToClose(onClose, { fxc: "1" });
|
||||
useBackToClose(() => setOpen(false), { fxd: "1" });
|
||||
urlWithParams({ ...fxCodec({ ...fxState, job }), fxe: "1" });
|
||||
`;
|
||||
|
||||
// Where a writer outside lib/ is handed its value: the address writers' argument, and an
|
||||
@@ -132,6 +134,18 @@ function topLevel(text: string, sep: string): string[] {
|
||||
out.push(text.slice(from));
|
||||
return out.map((s) => s.trim());
|
||||
}
|
||||
/** The top-level pieces of every writer's object literal in `text` (`a: x`, `...f(y)`, `[K]: v`, `b`). */
|
||||
function writerObjects(text: string): string[][] {
|
||||
return WRITER_OBJECTS.flatMap((re) =>
|
||||
[...text.matchAll(re)].map((m) => {
|
||||
const open = m.index + m[0].length - 1;
|
||||
return topLevel(text.slice(open + 1, closing(text, open)), ",").filter(Boolean);
|
||||
}),
|
||||
);
|
||||
}
|
||||
/** The key a piece names as a literal: `key: value`, or the shorthand `key`. */
|
||||
const keyOf = (piece: string) => /^(\w+)\s*:/.exec(piece)?.[1] ?? /^(\w+)$/.exec(piece)?.[1];
|
||||
|
||||
const LIB_EXPORTS = new Set(
|
||||
sources(join(SRC, "lib")).flatMap((f) =>
|
||||
[...readFileSync(f, "utf8").matchAll(/export (?:function|const) (\w+)/g)].map((m) => m[1]!),
|
||||
@@ -143,10 +157,13 @@ function writerValueOk(value: string): boolean {
|
||||
const choice = topLevel(v, "?");
|
||||
if (choice.length === 2) return topLevel(choice[1]!, ":").every(writerValueOk);
|
||||
if (v.startsWith("{")) {
|
||||
// Its spreads are a lib/ function's result too.
|
||||
// Its spreads are a lib/ function's result too; its other pieces a literal key and its value —
|
||||
// a computed or a shorthand one is a key no literal scan reads.
|
||||
return topLevel(v.slice(1, closing(v, 0)), ",")
|
||||
.filter((piece) => piece.startsWith("..."))
|
||||
.every((piece) => writerValueOk(piece.slice(3)));
|
||||
.filter(Boolean)
|
||||
.every((piece) =>
|
||||
piece.startsWith("...") ? writerValueOk(piece.slice(3)) : /^\w+\s*:/.test(piece),
|
||||
);
|
||||
}
|
||||
const call = /^(\w+)\(/.exec(v);
|
||||
return (
|
||||
@@ -189,9 +206,10 @@ function queryKeyLiterals(): { key: string; where: string }[] {
|
||||
for (const file of sources(SRC)) {
|
||||
const text = readFileSync(file, "utf8");
|
||||
const where = relative(SRC, file).split("\\").join("/");
|
||||
for (const re of WRITER_OBJECTS) {
|
||||
for (const m of text.matchAll(re)) {
|
||||
for (const k of `{${m[1]!}`.matchAll(/[{,]\s*(\w+)\s*:/g)) out.push({ key: k[1]!, where });
|
||||
for (const pieces of writerObjects(text)) {
|
||||
for (const piece of pieces) {
|
||||
const key = keyOf(piece);
|
||||
if (key) out.push({ key, where });
|
||||
}
|
||||
}
|
||||
if (!text.includes("location.search")) continue;
|
||||
@@ -211,10 +229,9 @@ function computedWriterKeys(inLib = false): string[] {
|
||||
const where = relative(SRC, file).split("\\").join("/");
|
||||
if (where.startsWith("lib/") !== inLib) continue;
|
||||
const text = readFileSync(file, "utf8");
|
||||
for (const re of WRITER_OBJECTS) {
|
||||
for (const m of text.matchAll(re)) {
|
||||
if (/[{,]\s*\[/.test(`{${m[1]!}`)) out.push(`${where}: ${m[0]!.slice(0, 60)}`);
|
||||
}
|
||||
for (const pieces of writerObjects(text)) {
|
||||
const computed = pieces.find((piece) => piece.startsWith("["));
|
||||
if (computed) out.push(`${where}: ${computed.slice(0, 60)}`);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
@@ -352,11 +369,11 @@ describe("module URL keys", () => {
|
||||
});
|
||||
|
||||
it("the writer scans see each form they are meant to", () => {
|
||||
const keys = WRITER_OBJECTS.flatMap((re) =>
|
||||
[...WRITER_FIXTURE.matchAll(re)].map((m) => /(\w+)\s*:/.exec(m[1]!)![1]),
|
||||
const keys = writerObjects(WRITER_FIXTURE).flatMap((pieces) =>
|
||||
pieces.flatMap((piece) => (piece.startsWith("...") ? [] : (keyOf(piece) ?? []))),
|
||||
);
|
||||
expect(keys.sort()).toEqual(["fxa", "fxb", "fxc", "fxd"]);
|
||||
expect(writerValues(WRITER_FIXTURE)).toHaveLength(4);
|
||||
expect(keys.sort()).toEqual(["fxa", "fxb", "fxc", "fxd", "fxe", "fxf"]);
|
||||
expect(writerValues(WRITER_FIXTURE)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("are written outside lib/ only as a literal or a lib/ function's result", () => {
|
||||
@@ -368,6 +385,9 @@ describe("module URL keys", () => {
|
||||
expect(writerValueOk("upd")).toBe(false);
|
||||
expect(writerValueOk("myOwnParams(v)")).toBe(false);
|
||||
expect(writerValueOk("{ ...st, zz: 1 }")).toBe(false);
|
||||
// A key no scan reads as a literal: computed, or shorthand.
|
||||
expect(writerValueOk("{ page: p, [K]: v }")).toBe(false);
|
||||
expect(writerValueOk("{ page: p, zz }")).toBe(false);
|
||||
expect(writerValueOk("Object.fromEntries(rows)")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -380,6 +400,30 @@ describe("module URL keys", () => {
|
||||
expect(defined.sort()).toEqual(["MODE_URL", "PLEX_SUB_URL"]);
|
||||
});
|
||||
|
||||
// …and every sub-view is handed one of those, by name: a definition written any other way (inline,
|
||||
// `satisfies`) is one the census above does not count.
|
||||
it("hand each sub-view a definition the census names", () => {
|
||||
const handed = (text: string) =>
|
||||
[...text.matchAll(/\buseHistorySubview(?:<[^>()]*>)?\(/g)].flatMap((m) => {
|
||||
const open = m.index + m[0].length - 1;
|
||||
const args = topLevel(text.slice(open + 1, closing(text, open)), ",");
|
||||
return args.length > 1 ? [args[1]!] : [];
|
||||
});
|
||||
const named = new Set(["MODE_URL", "PLEX_SUB_URL"]);
|
||||
const unnamed = (text: string) => handed(text).filter((arg) => !named.has(arg));
|
||||
const files = sources(SRC).filter(
|
||||
(f) => !relative(SRC, f).split("\\").join("/").startsWith("lib/history"),
|
||||
);
|
||||
expect(files.flatMap((f) => unnamed(readFileSync(f, "utf8")))).toEqual([]);
|
||||
// The call sites are seen (a scan of none would pass)…
|
||||
expect(new Set(files.flatMap((f) => handed(readFileSync(f, "utf8"))))).toEqual(named);
|
||||
// …and the check tells an inline definition from a named one.
|
||||
expect(unnamed('useHistorySubview(sub, { key: "zz", encode, decode })')).toEqual([
|
||||
'{ key: "zz", encode, decode }',
|
||||
]);
|
||||
expect(unnamed("useHistorySubview<Sub>(sub, PLEX_SUB_URL)")).toEqual([]);
|
||||
});
|
||||
|
||||
it("the computed-key scan sees what it is meant to (lib's own writers compute theirs)", () => {
|
||||
expect(computedWriterKeys(true).some((w) => w.startsWith("lib/history.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user