import { readdirSync, readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; // The whole app source: a state write can be made from any page. const root = new URL("../../", import.meta.url); const sources = () => readdirSync(root, { recursive: true, encoding: "utf8" }) .map((f) => f.replace(/\\/g, "/")) .filter((f) => /\.tsx?$/.test(f) && !f.includes(".test.")) .map((f) => ({ f, src: readFileSync(new URL(f, root), "utf8") })); // The api's own definitions of these writes, and the one module that may call them. const ALLOWED = new Set([ "lib/api/feed.ts", "lib/api/playlists.ts", "features/video/videoWrites.ts", ]); // A related list and a live search are never refetched, so a state, Watch-later or playlist-item // write that does not reach them (`markVideo`) leaves them showing the old mark for the session — // the built-in Watch later list's items ARE the mark. videoWrites makes those writes and the mark // together; a direct api call skips the mark. const WRITES = /\bapi\s*\.\s*(setState|clearState|watchLaterAdd|watchLaterRemove|addToPlaylist|removeFromPlaylist|moveToPlaylist)\b/; describe("a video's state, Watch-later and playlist-item writes", () => { it("go through features/video/videoWrites, never the api directly", () => { const offenders = sources() .filter(({ f }) => !ALLOWED.has(f)) // Only the api object's members count (React's own `setState` is not ours) — named, not only // called: `(saved ? api.watchLaterAdd : api.watchLaterRemove)(id)` makes the write too. The // dot may sit on the next line of a chain. .filter(({ src }) => WRITES.test(src)) .map(({ f }) => f); expect(offenders).toEqual([]); }); it("catches a write named without being called on the spot, and a playlist item's", () => { expect(WRITES.test("(saved ? api.watchLaterAdd : api.watchLaterRemove)(id)")).toBe(true); expect(WRITES.test("await api\n .removeFromPlaylist(listId, id)")).toBe(true); expect(WRITES.test("api.moveToPlaylist(a, id, b)")).toBe(true); expect(WRITES.test("this.setState(next); api.playlists()")).toBe(false); }); });