From 378b7559e4b277e28443752842013d3f89175fee Mon Sep 17 00:00:00 2001 From: npeter83 Date: Mon, 21 Sep 2026 23:33:02 +0200 Subject: [PATCH] build(frontend): finish the React 19 move, and hold the versions that were left in prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit react-i18next was still on ^14.1.2. The E27 S1 plan named it as "the one peer that actually needs moving — React 19 support is the 15 line", and then it did not move. The 14 line's reads `element.ref`, removed in React 19, and its withTranslation types are built on the GLOBAL JSX namespace @types/react 19 no longer declares — both latent only because this app imports neither. 15.7.4 fixes both at the source (`c.props.ref ?? c.ref`, `React.JSX.LibraryManagedAttributes`). `reactEngine.contract.test.ts` joins its Node and Python twins and holds what was only a comment: the compiler's `target` against the installed React major, react and react-dom together, the types against the runtime, and react-i18next's floor. A stale target is not a corrupt bundle — the plugin validates the option — but it is a silent one, since tsc never sees Babel's output. pythonEngine gains the seventh spelling nobody held: `requirements.lock`'s own regeneration recipe names a python image, and wheels resolve against THAT interpreter, so a recipe left behind yields a lock that does not match the image installing it. The type lane now covers `alembic` too, for the reason the ruff lane was widened to it — migrations run on every container start, so one that raises crash-loops prod. It came back at zero new errors, so the baseline is untouched. `tests/` is deliberately still out, and the config says why. vitest.config's docblock claimed the React transform is "a no-op for the plain .ts node tests". It is not: the compiler rewrites src/lib's hooks in that lane too, which is the point of sharing one factory with the build — and worth knowing before ruling the compiler out of a divergence. Tests: useCardPager had none, in either the hook or its two callers, though its page clamp moved to render in S2 — the transformation where a non-convergent condition is a crash rather than a glitch. Four cases now, including termination on an empty list. Combobox's and Avatar's close/failed-URL cases are relabelled CHARACTERIZATION: both pass against the pre-sweep code, and the tests beside them are the ones that discriminate. --- backend/pyrightconfig.json | 9 +- frontend/package-lock.json | 20 +++-- frontend/package.json | 2 +- .../src/components/ui/Combobox.dom.test.tsx | 10 ++- .../src/lib/pythonEngine.contract.test.ts | 14 +++ frontend/src/lib/reactEngine.contract.test.ts | 69 +++++++++++++++ frontend/src/lib/useCardPager.dom.test.tsx | 86 +++++++++++++++++++ frontend/tsconfig.tsbuildinfo | 1 + frontend/vitest.config.ts | 6 +- 9 files changed, 202 insertions(+), 15 deletions(-) create mode 100644 frontend/src/lib/reactEngine.contract.test.ts create mode 100644 frontend/src/lib/useCardPager.dom.test.tsx create mode 100644 frontend/tsconfig.tsbuildinfo diff --git a/backend/pyrightconfig.json b/backend/pyrightconfig.json index 89837125..6e32135f 100644 --- a/backend/pyrightconfig.json +++ b/backend/pyrightconfig.json @@ -15,8 +15,15 @@ // `pythonVersion` is not free-standing: `/.python-version` is the single source of truth and // `tests/test_python_engine_contract.py` fails this gate if this file, the two Dockerfiles, the CI // container or ruff's target-version disagree with it. +// +// `include` covers `alembic` for the same reason the ruff lane was widened to it: migrations run on +// every container start, so one that raises there crash-loops prod, and "it is generated code" is +// not an argument when the generated code is what boots the system. `tests/` is deliberately OUT — +// a test's job is to fail loudly when it is wrong, which is the opposite of the silent runtime +// failure this lane exists to catch, and annotating fixtures to a checker's satisfaction is churn +// the ratchet exists to avoid. { - "include": ["app"], + "include": ["app", "alembic"], "extraPaths": ["."], "typeCheckingMode": "basic", "pythonVersion": "3.12", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9294d8cc..3fa176cc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,7 +20,7 @@ "lucide-react": "^0.408.0", "react": "19.3.0", "react-dom": "19.3.0", - "react-i18next": "^14.1.2" + "react-i18next": "^15.7.4" }, "devDependencies": { "@eslint/js": "9.39.1", @@ -5861,17 +5861,18 @@ } }, "node_modules/react-i18next": { - "version": "14.1.3", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.3.tgz", - "integrity": "sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw==", + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz", + "integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", + "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { - "i18next": ">= 23.2.3", - "react": ">= 16.8.0" + "i18next": ">= 23.4.0", + "react": ">= 16.8.0", + "typescript": "^5" }, "peerDependenciesMeta": { "react-dom": { @@ -5879,6 +5880,9 @@ }, "react-native": { "optional": true + }, + "typescript": { + "optional": true } } }, @@ -6377,7 +6381,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/frontend/package.json b/frontend/package.json index 904a0586..fc8a554e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,7 @@ "lucide-react": "^0.408.0", "react": "19.3.0", "react-dom": "19.3.0", - "react-i18next": "^14.1.2" + "react-i18next": "^15.7.4" }, "devDependencies": { "@eslint/js": "9.39.1", diff --git a/frontend/src/components/ui/Combobox.dom.test.tsx b/frontend/src/components/ui/Combobox.dom.test.tsx index fdc55403..ec5e62ff 100644 --- a/frontend/src/components/ui/Combobox.dom.test.tsx +++ b/frontend/src/components/ui/Combobox.dom.test.tsx @@ -247,10 +247,12 @@ describe("Combobox", () => { expect(screen.getByRole("combobox").getAttribute("aria-expanded")).toBe("false"); // closed }); - // The typed query is discarded when the list closes, and it happens IN the close — the clear used - // to be an effect on `open`, which fired a render later and, in controlled mode, reported the - // empty query outside any event (E27 S2). The list is for "find and pick": a leftover filter would - // hide everything worth seeing the next time it opens. + // The typed query is discarded when the list closes. CHARACTERIZATION, not the regression guard: + // the old effect-based clear satisfies these assertions too, because RTL wraps fireEvent in act() + // and the passive effect has flushed by the time the value is read. What the move into the close + // funnel actually fixed is measured by the next test, which the old shape fails. This one is here + // because the behaviour is worth pinning: the list is for "find and pick", and a leftover filter + // would hide everything worth seeing the next time it opens. it("discards the typed query when the list closes, so the next open shows everything", () => { wrap(); const input = screen.getByRole("combobox"); diff --git a/frontend/src/lib/pythonEngine.contract.test.ts b/frontend/src/lib/pythonEngine.contract.test.ts index a0eb6829..b2637f03 100644 --- a/frontend/src/lib/pythonEngine.contract.test.ts +++ b/frontend/src/lib/pythonEngine.contract.test.ts @@ -84,3 +84,17 @@ it("lints against the pinned version", () => { versionIn(readRoot("backend/ruff.toml"), /^target-version = "py(\d)(\d+)"$/m, "ruff.toml"), ).toBe(pinned); }); + +it("resolves the lock on the pinned version", () => { + // The seventh spelling, and the sneakiest: `requirements.lock`'s own regeneration recipe names an + // image. Wheels and environment markers are resolved by THAT interpreter, so a recipe left behind + // produces a lock that does not match the image installing it — the same split as the others, one + // indirection further out, and with nothing red to show for it. + expect( + versionIn( + readRoot("backend/requirements.lock"), + /docker run [^\n]*python:(\d+)\.(\d+)-slim/, + "requirements.lock", + ), + ).toBe(pinned); +}); diff --git a/frontend/src/lib/reactEngine.contract.test.ts b/frontend/src/lib/reactEngine.contract.test.ts new file mode 100644 index 00000000..61bfd1d2 --- /dev/null +++ b/frontend/src/lib/reactEngine.contract.test.ts @@ -0,0 +1,69 @@ +import { readFileSync } from "node:fs"; +import { expect, it } from "vitest"; + +// The third contract test, after `nodeEngine` and `pythonEngine`, and for the same reason: a version +// that is spelled in two places drifts, and the drift is silent until something downstream breaks. +// +// Here the two places are `package.json`'s React pin and `reactPlugin.ts`'s `target`, which tells +// babel-plugin-react-compiler which React runtime to emit memoisation against. E27 S1 wrote the rule +// as a comment — "`target` follows the installed React major and must move with it" — and the house +// rule this codebase was built on says a rule stated in a comment must also be enforced in code. So +// it is enforced here. +// +// The compiler does validate the option itself (an unknown target throws at build time), which +// bounds the damage: the failure mode is not a corrupt bundle but a STALE target — React moves to a +// new major and the compiler keeps emitting for the old one, with no lane saying so, because tsc +// never sees Babel's output. +// +// To move React: bump `react`, `react-dom` and `@types/react` in package.json, then follow this +// test's failures. +const readFrontend = (rel: string) => + readFileSync(new URL(`../../${rel}`, import.meta.url), "utf8"); + +const pkg = JSON.parse(readFrontend("package.json")) as { + dependencies: Record; + devDependencies: Record; +}; + +/** The major in a version range, whether it is exact (`19.3.0`) or a caret (`^19.3.0`). */ +function major(range: string, where: string): number { + const m = /^\D*(\d+)\./.exec(range); + expect(m, `could not read a major from ${where}: ${range}`).not.toBeNull(); + return Number(m![1]); +} + +const pinned = major(pkg.dependencies["react"]!, "react"); + +it("pins a plausible React major", () => { + expect(pinned).toBeGreaterThanOrEqual(19); +}); + +it("keeps react-dom on the same major as react", () => { + expect(major(pkg.dependencies["react-dom"]!, "react-dom")).toBe(pinned); +}); + +it("keeps the types on the same major as the runtime", () => { + // Types a major ahead of the runtime make tsc green over APIs the shipped React does not have. + expect(major(pkg.devDependencies["@types/react"]!, "@types/react")).toBe(pinned); + expect(major(pkg.devDependencies["@types/react-dom"]!, "@types/react-dom")).toBe(pinned); +}); + +it("compiles for the React that is actually installed", () => { + const src = readFrontend("reactPlugin.ts"); + const m = /target:\s*"(\d+)"/.exec(src); + // A rename or a moved line lands here. Re-point the test — the contract did not go away with it. + expect(m, 'no `target: ""` in reactPlugin.ts').not.toBeNull(); + expect( + Number(m![1]), + `reactPlugin.ts compiles for React ${m![1]} while package.json pins ${pinned}. The React ` + + `Compiler emits against the runtime named by \`target\`; move it with the pin.`, + ).toBe(pinned); +}); + +it("keeps react-i18next on a line that supports the pinned React", () => { + // The 14 line predates React 19: its read `element.ref` (removed in 19) and its + // `withTranslation` types were built on the GLOBAL JSX namespace that @types/react 19 no longer + // declares — both latent only because this app happens to use neither export. 15 is the first + // line that handles both, so the floor is a real contract, not a tidiness preference. + expect(major(pkg.dependencies["react-i18next"]!, "react-i18next")).toBeGreaterThanOrEqual(15); +}); diff --git a/frontend/src/lib/useCardPager.dom.test.tsx b/frontend/src/lib/useCardPager.dom.test.tsx new file mode 100644 index 00000000..0a1095fa --- /dev/null +++ b/frontend/src/lib/useCardPager.dom.test.tsx @@ -0,0 +1,86 @@ +// @vitest-environment jsdom +// +// The card layout's pager clamps an out-of-range page DURING RENDER (E27 S2), which is the right +// shape — React discards that pass, so nothing is committed on a page that no longer exists — but +// it is also the shape that turns a non-convergent condition into "Too many re-renders" rather than +// a visual glitch. Neither the hook nor either of its two callers (ChannelDiscovery, Channels) had +// a test, so nothing held either half. This file holds both: the clamp happens, and it terminates. +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, act } from "@testing-library/react"; + +vi.mock("react-i18next", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, useTranslation: () => ({ t: (k: string) => k }) }; +}); + +import { useCardPager } from "./useCardPager"; + +const rows = (n: number) => Array.from({ length: n }, (_, i) => i); +const KEY = "siftlode.test.cardSize"; + +beforeEach(() => { + localStorage.clear(); + sessionStorage.setItem("siftlode.activeAccount", "1"); +}); + +describe("useCardPager — an out-of-range page", () => { + it("clamps to the last page that still exists when the list shrinks", () => { + const { result, rerender } = renderHook(({ n }) => useCardPager(rows(n), KEY, true), { + initialProps: { n: 30 }, + }); + act(() => result.current.pagerProps.onSizeChange(10)); + act(() => result.current.pagerProps.onPageChange(2)); + expect(result.current.pagerProps.page).toBe(2); + expect(result.current.paged).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]); + + rerender({ n: 5 }); // an unsubscribe, a refetch — page 2 no longer exists + + expect(result.current.pagerProps.page).toBe(0); + expect(result.current.paged).toEqual([0, 1, 2, 3, 4]); + }); + + it("does not spring back to the stale page when the list grows again", () => { + const { result, rerender } = renderHook(({ n }) => useCardPager(rows(n), KEY, true), { + initialProps: { n: 30 }, + }); + act(() => result.current.pagerProps.onSizeChange(10)); + act(() => result.current.pagerProps.onPageChange(2)); + + rerender({ n: 5 }); + rerender({ n: 30 }); + + // The clamp was COMMITTED, not merely displayed — `safePage` alone would have let 2 come back. + expect(result.current.pagerProps.page).toBe(0); + }); + + it("terminates on an empty list — the write falsifies its own condition", () => { + const { result, rerender } = renderHook(({ n }) => useCardPager(rows(n), KEY, true), { + initialProps: { n: 30 }, + }); + act(() => result.current.pagerProps.onSizeChange(10)); + act(() => result.current.pagerProps.onPageChange(2)); + + // totalPages floors at 1, so the clamp target is 0 and the condition is false next pass. If it + // were not, this line would throw "Too many re-renders" instead of returning. + rerender({ n: 0 }); + + expect(result.current.pagerProps.page).toBe(0); + expect(result.current.paged).toEqual([]); + }); + + it("holds still in table mode, where DataTable does its own paging", () => { + const { result, rerender } = renderHook(({ n, on }) => useCardPager(rows(n), KEY, on), { + initialProps: { n: 30, on: false }, + }); + act(() => result.current.pagerProps.onSizeChange(10)); + act(() => result.current.pagerProps.onPageChange(2)); + + rerender({ n: 5, on: false }); // the same shrink — but nothing is committed here + rerender({ n: 30, on: false }); + + // `safePage` still clamps for DISPLAY while the list is short; what must not happen is the + // clamp being written, which would lose the page for real. Growing back proves it was not. + expect(result.current.pagerProps.page).toBe(2); + expect(result.current.paged).toEqual(rows(30)); // and the rows are never sliced in table mode + }); +}); diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo new file mode 100644 index 00000000..4f1dfb39 --- /dev/null +++ b/frontend/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/about.tsx","./src/components/aboutcell.tsx","./src/components/accountpanel.tsx","./src/components/activefilterchips.tsx","./src/components/addtoplaylist.tsx","./src/components/adminlogs.dom.test.tsx","./src/components/adminlogs.tsx","./src/components/adminusers.dom.test.tsx","./src/components/adminusers.tsx","./src/components/appcommandpalette.dom.test.tsx","./src/components/appcommandpalette.tsx","./src/components/auditlog.tsx","./src/components/bootgate.dom.test.tsx","./src/components/bootgate.tsx","./src/components/channelabout.tsx","./src/components/channeldiscovery.tsx","./src/components/channellayoutgrid.dom.test.tsx","./src/components/channellayoutgrid.tsx","./src/components/channellink.tsx","./src/components/channelnamelink.dom.test.tsx","./src/components/channelnamelink.tsx","./src/components/channelpage.backfill.dom.test.tsx","./src/components/channelpage.busyfocus.dom.test.tsx","./src/components/channelpage.tsx","./src/components/channelpicker.tsx","./src/components/channelstatspopover.dom.test.tsx","./src/components/channelstatspopover.tsx","./src/components/channeltags.suggest.dom.test.tsx","./src/components/channeltags.tsx","./src/components/channels.tsx","./src/components/channelsprovider.dom.test.tsx","./src/components/channelsprovider.tsx","./src/components/chatdock.tsx","./src/components/chatthread.tsx","./src/components/comfortpinmenu.tsx","./src/components/configpanel.choice.dom.test.tsx","./src/components/configpanel.field.dom.test.tsx","./src/components/configpanel.tsx","./src/components/confirmprovider.tsx","./src/components/datefilterselect.tsx","./src/components/debugrecorder.dom.test.tsx","./src/components/debugrecorder.tsx","./src/components/demobanner.tsx","./src/components/diagnosticssettings.dom.test.tsx","./src/components/diagnosticssettings.tsx","./src/components/downloadbutton.tsx","./src/components/downloadcenter.confirm.dom.test.tsx","./src/components/downloadcenter.tsx","./src/components/downloaddialog.tsx","./src/components/errorboundary.stalebuild.dom.test.tsx","./src/components/errorboundary.tsx","./src/components/errordialog.tsx","./src/components/excludedchannellist.dom.test.tsx","./src/components/excludedchannellist.tsx","./src/components/feed.tsx","./src/components/feed.ytsort.dom.test.tsx","./src/components/feedfiltersprovider.dom.test.ts","./src/components/feedfiltersprovider.seed.dom.test.tsx","./src/components/feedfiltersprovider.tsx","./src/components/feedscopetoggle.tsx","./src/components/feedvideomenu.dom.test.tsx","./src/components/feedviewprovider.tsx","./src/components/filterrail.tsx","./src/components/glasstuner.tsx","./src/components/header.tsx","./src/components/home.dom.test.tsx","./src/components/home.pin.dom.test.tsx","./src/components/home.tsx","./src/components/issuecomposer.dom.test.tsx","./src/components/issuecomposer.tsx","./src/components/keygate.tsx","./src/components/languageswitcher.tsx","./src/components/libraryplayer.dom.test.tsx","./src/components/libraryplayer.tsx","./src/components/logline.tsx","./src/components/messages.keyboard.dom.test.tsx","./src/components/messages.tsx","./src/components/mylogviewer.dom.test.tsx","./src/components/mylogviewer.tsx","./src/components/navsidebar.tsx","./src/components/navigationprovider.dom.test.tsx","./src/components/navigationprovider.tsx","./src/components/notificationspanel.tsx","./src/components/onboardingwizard.dialog.dom.test.tsx","./src/components/onboardingwizard.escape.dom.test.tsx","./src/components/onboardingwizard.tsx","./src/components/palettelauncher.dom.test.tsx","./src/components/palettelauncher.tsx","./src/components/personalviewsmodal.dom.test.tsx","./src/components/personalviewsmodal.tsx","./src/components/playermodal.dom.test.tsx","./src/components/playermodal.tsx","./src/components/playlisteditfailures.dom.test.tsx","./src/components/playlistlimitshint.dom.test.tsx","./src/components/playlistlimitshint.tsx","./src/components/playlists.dom.test.tsx","./src/components/playlists.tsx","./src/components/playlistsprovider.tsx","./src/components/playlistsrail.rename.dom.test.tsx","./src/components/playlistsrail.tsx","./src/components/plexbrowse.tsx","./src/components/plexcollectioneditor.tsx","./src/components/plexcollectionview.dom.test.tsx","./src/components/plexcollectionview.tsx","./src/components/plexdiscover.dom.test.tsx","./src/components/plexdiscover.tsx","./src/components/plexinfo.dom.test.tsx","./src/components/plexinfo.tsx","./src/components/plexlibrarypicker.dom.test.tsx","./src/components/plexlibrarypicker.tsx","./src/components/plexpersonview.dom.test.tsx","./src/components/plexplayer.backspace.dom.test.tsx","./src/components/plexplayer.hls.dom.test.tsx","./src/components/plexplayer.session.dom.test.tsx","./src/components/plexplayer.strictmode.dom.test.tsx","./src/components/plexplayer.tabtrap.dom.test.tsx","./src/components/plexplayer.tsx","./src/components/plexplayersettings.tsx","./src/components/plexplaylistadd.tsx","./src/components/plexplaylistreorder.dom.test.tsx","./src/components/plexplaylistview.header.dom.test.tsx","./src/components/plexplaylistview.tsx","./src/components/plexprovider.dom.test.tsx","./src/components/plexprovider.tsx","./src/components/plexscopetoggle.dom.test.tsx","./src/components/plexscopetoggle.tsx","./src/components/plexsearchbox.dom.test.tsx","./src/components/plexsearchbox.tsx","./src/components/plexsidebar.tsx","./src/components/plexstatsoverlay.tsx","./src/components/plexviewbridge.tsx","./src/components/plexviewtoggle.dom.test.tsx","./src/components/plexviewtoggle.tsx","./src/components/postertile.tsx","./src/components/prefsprovider.dom.test.tsx","./src/components/prefsprovider.tsx","./src/components/profileeditor.tsx","./src/components/releasenotes.tsx","./src/components/savedviewspopover.dom.test.tsx","./src/components/savedviewspopover.tsx","./src/components/savedviewsprovider.dom.test.tsx","./src/components/savedviewsprovider.tsx","./src/components/scheduler.dom.test.tsx","./src/components/scheduler.tsx","./src/components/searchbar.dom.test.tsx","./src/components/searchbar.keys.dom.test.tsx","./src/components/searchbar.tsx","./src/components/selectedviewchip.tsx","./src/components/settingspanel.account.dom.test.tsx","./src/components/settingspanel.downloadauth.dom.test.tsx","./src/components/settingspanel.tsx","./src/components/setupwizard.tsx","./src/components/sharedialog.dom.test.tsx","./src/components/sharedialog.tsx","./src/components/sidebar.tsx","./src/components/stats.tsx","./src/components/syncstatus.tsx","./src/components/tagfilterchips.dom.test.tsx","./src/components/tagfilterchips.tsx","./src/components/tagmanager.tsx","./src/components/upnextcard.dom.test.tsx","./src/components/upnextcard.tsx","./src/components/uploaddialog.dom.test.tsx","./src/components/uploaddialog.tsx","./src/components/verifybanner.tsx","./src/components/versionbanner.tsx","./src/components/videocard.channel.dom.test.tsx","./src/components/videocard.nav.dom.test.tsx","./src/components/videocard.tsx","./src/components/videoeditor.partial.dom.test.tsx","./src/components/videoeditor.tsx","./src/components/videoinfocard.dom.test.tsx","./src/components/videoinfocard.tsx","./src/components/viewswitcher.tsx","./src/components/virtualfeed.tsx","./src/components/watchpage.dom.test.tsx","./src/components/watchpage.tsx","./src/components/welcome.tsx","./src/components/wizardprovider.tsx","./src/components/writefailurereporting.dom.test.tsx","./src/components/yearfilter.tsx","./src/components/accountpanel.dom.test.tsx","./src/components/admininvites.dom.test.tsx","./src/components/adminpendingaccess.dom.test.tsx","./src/components/auditcolumns.tsx","./src/components/authcardstates.dom.test.tsx","./src/components/channelcells.tsx","./src/components/channelcolumns.dom.test.tsx","./src/components/channelcolumns.tsx","./src/components/channelsync.ts","./src/components/channelscontext.ts","./src/components/clientlogwiring.dom.test.tsx","./src/components/comfortpin.dom.test.ts","./src/components/comfortpin.tsx","./src/components/confirmcontext.ts","./src/components/downloadadmin.tsx","./src/components/downloadmodals.tsx","./src/components/downloadrow.dom.test.tsx","./src/components/downloadrow.tsx","./src/components/downloadtitle.ts","./src/components/feedfilterscontext.ts","./src/components/feedfiltersinit.ts","./src/components/feedviewcontext.ts","./src/components/formalerts.dom.test.tsx","./src/components/listnav.wiring.contract.test.ts","./src/components/loglineparts.ts","./src/components/modulepages.tsx","./src/components/moduleregistry.tsx","./src/components/navigation.dom.test.ts","./src/components/navigationcontext.ts","./src/components/navigationinit.ts","./src/components/playermodalcontrols.tsx","./src/components/playlistcolumns.tsx","./src/components/playlistscontext.ts","./src/components/playlistsinit.test.ts","./src/components/playlistsinit.ts","./src/components/plexbrowselists.tsx","./src/components/plexcards.dom.test.tsx","./src/components/plexcards.tsx","./src/components/plexcontext.ts","./src/components/plexdetailui.tsx","./src/components/plexplayable.ts","./src/components/plexplayercontrols.tsx","./src/components/plexsuburl.test.ts","./src/components/plexsuburl.ts","./src/components/plexviewcontext.ts","./src/components/plexviews.tsx","./src/components/prefscontext.ts","./src/components/savedviewscontext.ts","./src/components/usedebugrecording.ts","./src/components/useplaylistactions.dom.test.tsx","./src/components/useplaylistactions.ts","./src/components/useplexactivefilters.dom.test.tsx","./src/components/useplexactivefilters.ts","./src/components/useplexplaylistedits.dom.test.tsx","./src/components/useplexplaylistedits.ts","./src/components/usetagsuggestions.dom.test.tsx","./src/components/usetagsuggestions.ts","./src/components/welcomemode.dom.test.ts","./src/components/welcomemode.render.dom.test.tsx","./src/components/welcomemode.ts","./src/components/wizardcontext.ts","./src/components/legal/legallayout.tsx","./src/components/legal/privacypolicy.tsx","./src/components/legal/terms.tsx","./src/components/legal/legalbodies.contract.dom.test.tsx","./src/components/legal/usehungarian.ts","./src/components/ui/actionmenu.dom.test.tsx","./src/components/ui/actionmenu.tsx","./src/components/ui/anchoredpopover.dom.test.tsx","./src/components/ui/anchoredpopover.returnfocus.dom.test.tsx","./src/components/ui/anchoredpopover.tsx","./src/components/ui/avatar.dom.test.tsx","./src/components/ui/avatar.tsx","./src/components/ui/backtotop.tsx","./src/components/ui/banner.dom.test.tsx","./src/components/ui/banner.tsx","./src/components/ui/button.dom.test.tsx","./src/components/ui/button.tsx","./src/components/ui/carousel.dom.test.tsx","./src/components/ui/carousel.tsx","./src/components/ui/columnsortcontrol.tsx","./src/components/ui/combobox.dom.test.tsx","./src/components/ui/combobox.tsx","./src/components/ui/commandpalette.dom.test.tsx","./src/components/ui/commandpalette.ordering.dom.test.tsx","./src/components/ui/commandpalette.tsx","./src/components/ui/contextmenu.dom.test.tsx","./src/components/ui/contextmenu.tsx","./src/components/ui/countbadge.tsx","./src/components/ui/datatable.fixedlayout.dom.test.tsx","./src/components/ui/datatable.keyboard.dom.test.tsx","./src/components/ui/datatable.tsx","./src/components/ui/draftsavebar.tsx","./src/components/ui/filterchipgroup.dom.test.tsx","./src/components/ui/filterchipgroup.tsx","./src/components/ui/filterselect.dom.test.tsx","./src/components/ui/filterselect.tsx","./src/components/ui/focussafebutton.tsx","./src/components/ui/inlinenotice.tsx","./src/components/ui/itemcount.dom.test.tsx","./src/components/ui/itemcount.tsx","./src/components/ui/keyboardhelp.dom.test.tsx","./src/components/ui/keyboardhelp.tsx","./src/components/ui/modal.dom.test.tsx","./src/components/ui/modal.fullscreen.dom.test.tsx","./src/components/ui/modal.strictmode.dom.test.tsx","./src/components/ui/modal.tsx","./src/components/ui/navgrid.tsx","./src/components/ui/overlay.tsx","./src/components/ui/pagescroller.dom.test.tsx","./src/components/ui/pagescroller.tsx","./src/components/ui/pageshell.dom.test.tsx","./src/components/ui/pageshell.tsx","./src/components/ui/pager.dom.test.tsx","./src/components/ui/pager.tsx","./src/components/ui/panelgroup.tsx","./src/components/ui/panelgroups.tsx","./src/components/ui/prioritystepper.dom.test.tsx","./src/components/ui/prioritystepper.tsx","./src/components/ui/querystate.dom.test.tsx","./src/components/ui/querystate.tsx","./src/components/ui/segmentedtoggle.dom.test.tsx","./src/components/ui/segmentedtoggle.tsx","./src/components/ui/sidepanel.tsx","./src/components/ui/sortcontrol.dom.test.tsx","./src/components/ui/sortcontrol.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toaster.dom.test.tsx","./src/components/ui/toaster.hosts.test.ts","./src/components/ui/toaster.strictmode.dom.test.tsx","./src/components/ui/toaster.tsx","./src/components/ui/tooltip.layer.dom.test.tsx","./src/components/ui/tooltip.tsx","./src/components/ui/undotoolbar.tsx","./src/components/ui/virtualgrid.tsx","./src/components/ui/anchoredpopoverhooks.ts","./src/components/ui/dialogfocus.dom.test.tsx","./src/components/ui/dialogfocus.ts","./src/components/ui/form.tsx","./src/components/ui/gridcolumns.test.ts","./src/components/ui/gridcolumns.ts","./src/components/ui/overlayorder.ts","./src/components/ui/pagetoolbarslot.ts","./src/components/ui/pagercontent.ts","./src/components/ui/readahead.test.ts","./src/components/ui/readahead.ts","./src/components/ui/sortablea11y.dom.test.tsx","./src/components/ui/sortablea11y.ts","./src/components/ui/stickytop.dom.test.tsx","./src/components/ui/stickytop.ts","./src/components/ui/tablealign.ts","./src/components/ui/tableheader.tsx","./src/components/ui/toastlevels.ts","./src/components/ui/uii18n.contract.test.ts","./src/components/ui/usecontextmenu.ts","./src/components/ui/useitemtriggers.dom.test.ts","./src/i18n/appcopy.dom.test.ts","./src/i18n/appcopy.ts","./src/i18n/backendids.contract.test.ts","./src/i18n/index.ts","./src/i18n/parity.test.ts","./src/i18n/unusedkeys.contract.test.ts","./src/lib/accountpinnedstores.dom.test.ts","./src/lib/actionerrors.test.ts","./src/lib/actionerrors.ts","./src/lib/activeaccount.dom.test.ts","./src/lib/activeaccount.ts","./src/lib/adminusers.test.ts","./src/lib/adminusers.ts","./src/lib/adminuserstab.ts","./src/lib/api.ts","./src/lib/browserbackgroundplay.dom.test.ts","./src/lib/browserbackgroundplay.ts","./src/lib/capturetab.dom.test.ts","./src/lib/capturetab.ts","./src/lib/channeldetailpoll.backfill.dom.test.tsx","./src/lib/channeldetailpoll.test.ts","./src/lib/channeldetailpoll.ts","./src/lib/channellayout.ts","./src/lib/channelwritefailures.dom.test.tsx","./src/lib/channelsurlstate.dom.test.ts","./src/lib/channelsurlstate.ts","./src/lib/chatdock.ts","./src/lib/chiplane.contract.test.ts","./src/lib/chipoverflow.test.ts","./src/lib/chipoverflow.ts","./src/lib/clipboard.ts","./src/lib/columnsort.test.ts","./src/lib/columnsort.ts","./src/lib/controlrules.contract.test.ts","./src/lib/cropgeometry.test.ts","./src/lib/cropgeometry.ts","./src/lib/datefilter.test.ts","./src/lib/datefilter.ts","./src/lib/debugrecorder.ts","./src/lib/descriptionlinks.tsx","./src/lib/detaillog.dom.test.ts","./src/lib/diagnostics.dom.test.ts","./src/lib/diagnostics.session.dom.test.ts","./src/lib/diagnostics.ts","./src/lib/downloadactions.test.ts","./src/lib/downloadactions.ts","./src/lib/downloadfailure.test.ts","./src/lib/downloadfailure.ts","./src/lib/downloads.test.ts","./src/lib/downloads.ts","./src/lib/e2ee.ts","./src/lib/entryurl.ts","./src/lib/errordialog.ts","./src/lib/feedsort.ts","./src/lib/feedsort.yt.test.ts","./src/lib/feedview.ts","./src/lib/filterlog.ts","./src/lib/focussafe.contract.test.ts","./src/lib/format.test.ts","./src/lib/format.ts","./src/lib/fullscreen.ts","./src/lib/globalkeymap.dom.test.tsx","./src/lib/hints.ts","./src/lib/history.dom.test.ts","./src/lib/history.nav.dom.test.ts","./src/lib/history.navposition.dom.test.tsx","./src/lib/history.navrestore.reload.dom.test.tsx","./src/lib/history.subview.dom.test.ts","./src/lib/history.ts","./src/lib/homeintent.ts","./src/lib/http.test.ts","./src/lib/http.ts","./src/lib/huetile.ts","./src/lib/issuedraft.test.ts","./src/lib/issuedraft.ts","./src/lib/jsxcommenttext.contract.test.ts","./src/lib/keymap.sequence.test.ts","./src/lib/keymap.test.ts","./src/lib/keymap.ts","./src/lib/layerstack.dom.test.ts","./src/lib/layerstack.integration.dom.test.tsx","./src/lib/layerstack.ts","./src/lib/lazywithpreload.dom.test.tsx","./src/lib/lazywithpreload.ts","./src/lib/linkify.test.ts","./src/lib/linkify.tsx","./src/lib/listnav.dom.test.tsx","./src/lib/listnav.scope.dom.test.tsx","./src/lib/listnav.ts","./src/lib/listpaging.test.ts","./src/lib/listpaging.ts","./src/lib/literala11ytext.contract.test.ts","./src/lib/log.contract.test.ts","./src/lib/log.test.ts","./src/lib/log.ts","./src/lib/loghooks.dom.test.ts","./src/lib/loghooks.ts","./src/lib/logsurlstate.test.ts","./src/lib/logsurlstate.ts","./src/lib/medialog.ts","./src/lib/mediasource.ts","./src/lib/messagessocket.test.ts","./src/lib/messagessocket.ts","./src/lib/messaging.ts","./src/lib/modules.ts","./src/lib/mutationfailures.contract.test.ts","./src/lib/nav.ts","./src/lib/navposition.wiring.contract.test.ts","./src/lib/networkboundary.contract.test.ts","./src/lib/nodeengine.contract.test.ts","./src/lib/notifications.ts","./src/lib/onboarding.ts","./src/lib/pagekeys.dom.test.tsx","./src/lib/pagemeta.test.ts","./src/lib/pagemeta.ts","./src/lib/panellayout.dom.test.ts","./src/lib/panellayout.ts","./src/lib/parseid.test.ts","./src/lib/parseid.ts","./src/lib/playermodalhooks.progress.dom.test.tsx","./src/lib/playermodalhooks.ts","./src/lib/playermodes.ts","./src/lib/playerqueue.test.ts","./src/lib/playerqueue.ts","./src/lib/playervolume.test.ts","./src/lib/playervolume.ts","./src/lib/playlistname.ts","./src/lib/playlistview.test.ts","./src/lib/playlistview.ts","./src/lib/plexbackdrop.dom.test.tsx","./src/lib/plexdetail.ts","./src/lib/plexfinishmargin.contract.test.ts","./src/lib/plexplayerhooks.subtitle.dom.test.tsx","./src/lib/plexplayerhooks.ts","./src/lib/plexplayerprefs.ts","./src/lib/plexqueue.test.ts","./src/lib/plexqueue.ts","./src/lib/plexrating.test.ts","./src/lib/plexrating.ts","./src/lib/plexsearch.ts","./src/lib/plexsort.ts","./src/lib/plexstats.test.ts","./src/lib/plexstats.ts","./src/lib/plexurlstate.dom.test.ts","./src/lib/plexurlstate.ts","./src/lib/plexwatch.ts","./src/lib/progresscache.ts","./src/lib/pxbox.contract.test.ts","./src/lib/pythonengine.contract.test.ts","./src/lib/queryclient.dom.test.ts","./src/lib/queryclient.ts","./src/lib/querykeys.test.ts","./src/lib/querykeys.ts","./src/lib/releasenotes.test.ts","./src/lib/releasenotes.ts","./src/lib/savedviews.test.ts","./src/lib/savedviews.ts","./src/lib/sortoptions.ts","./src/lib/sortablea11y.contract.test.ts","./src/lib/stalebuild.dom.test.ts","./src/lib/stalebuild.ts","./src/lib/storage.ts","./src/lib/store.ts","./src/lib/tablecolumns.ts","./src/lib/theme.ts","./src/lib/toastdock.ts","./src/lib/uiboundary.contract.test.ts","./src/lib/urlstate.dom.test.ts","./src/lib/urlstate.ts","./src/lib/useactivefilters.ts","./src/lib/usecardpager.dom.test.tsx","./src/lib/usecardpager.ts","./src/lib/usechannelpriority.ts","./src/lib/usechanneltagactions.ts","./src/lib/usechipoverflowfit.dom.test.tsx","./src/lib/usechipoverflowfit.ts","./src/lib/usedebounced.dom.test.ts","./src/lib/usedebounced.ts","./src/lib/usedismiss.dom.test.tsx","./src/lib/usedismiss.ts","./src/lib/usefilterstate.dom.test.ts","./src/lib/usefilterstate.ts","./src/lib/usefocuskeeper.dom.test.tsx","./src/lib/usefocuskeeper.restore.dom.test.tsx","./src/lib/usefocuskeeper.ts","./src/lib/usehoverfocuswithin.dom.test.tsx","./src/lib/usehoverfocuswithin.ts","./src/lib/uselivequery.ts","./src/lib/useme.ts","./src/lib/usemediaquery.ts","./src/lib/useplexcollectiontitles.dom.test.tsx","./src/lib/useplexcollectiontitles.ts","./src/lib/usepref.dom.test.ts","./src/lib/usepref.savefailure.dom.test.ts","./src/lib/usepref.ts","./src/lib/usescrolledges.ts","./src/lib/usescrollfade.ts","./src/lib/usesortstate.dom.test.ts","./src/lib/usesortstate.ts","./src/lib/useundoable.ts","./src/lib/useurlmirror.dom.test.ts","./src/lib/useurlmirror.ts","./src/lib/useverifyentry.ts","./src/lib/verifyentry.dom.test.tsx","./src/lib/version.ts","./src/lib/youtube.ts","./src/lib/youtubeerrors.ts","./src/lib/api/account.ts","./src/lib/api/admin.ts","./src/lib/api/adminlogs.dom.test.ts","./src/lib/api/auth.ts","./src/lib/api/channels.ts","./src/lib/api/core.test.ts","./src/lib/api/core.ts","./src/lib/api/diagnostics.dom.test.ts","./src/lib/api/diagnostics.ts","./src/lib/api/discover.dom.test.ts","./src/lib/api/discover.ts","./src/lib/api/downloads.ts","./src/lib/api/facade.test.ts","./src/lib/api/feed.ts","./src/lib/api/messaging.ts","./src/lib/api/notifications.ts","./src/lib/api/playlists.ts","./src/lib/api/plex.dom.test.ts","./src/lib/api/plex.ts","./src/lib/api/quota.ts","./src/lib/api/savedviews.ts","./src/lib/api/setup.ts","./src/lib/api/sync.ts","./src/lib/api/tagmodes.test.ts","./src/lib/api/tagmodes.ts","./src/lib/api/tags.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 3545f74a..c8ce1502 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -6,7 +6,11 @@ import { reactWithCompiler } from "./reactPlugin"; // DOM). A test that needs the DOM (component render or renderHook) opts INTO jsdom per-file with a // // @vitest-environment jsdom // docblock on its first line. plugin-react is wired here so those tests get JSX + a Fast-Refresh- -// free React transform; it is a no-op for the plain .ts node tests (they carry no JSX). jsdom is a +// free React transform. It is NOT a no-op for the plain .ts node tests: the plugin's filter is +// /\.[tj]sx?$/, so the React Compiler rewrites `src/lib`'s hooks in that lane too (usePref, +// useCardPager, plexPlayerHooks all come out with `react/compiler-runtime` memo slots). That is the +// point of sharing one factory with vite.config.ts — the gate compiles the code the way the build +// does — and it is worth knowing before ruling the compiler out of a divergence. jsdom is a // devDependency referenced only via that docblock env string — knip's vitest plugin still sees it. export default defineConfig({ plugins: [reactWithCompiler()],