Files
pad/web/vitest.config.ts
T
xarmian a0c7d68aa9 build(web): SvelteKit 3 + adapter-static 4 (TASK-3423 PR B) (#1835)
* wip(web): SvelteKit 3.0.1 + adapter-static 4 with the $lib alias (TASK-3423 PR B, WIP)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* wip(web): kit 3 migration fixes - goto reset, page.url.href, $app/env, $app/state, cookie v2 (TASK-3423 PR B)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): pane Back restores scroll through kit 3's snapshot() (TASK-3423 PR B)

PaneHost read page.state one task after popstate; kit 3 applies a popped
entry after an await, so the saved paneScrollTop was never seen. Kit's own
snapshot() keeps the position per history entry, captures it when the entry
is left, drops it on a replace, and restores only on a traversal after the
entry is applied. The controller's replaceState(paneScrollTop) write, the
getPaneScrollTop dep and readPaneScrollTop are gone. The 9 bug-2182 legs pass.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): the back-settle latch opens at the Back's popstate, where kit 3 arms it (TASK-3423 PR B)

Kit 3's popstate handler is async, so afterNavigate (where the settle is
armed) now runs after every popstate listener; measured 2-30ms after the
helper's listener. Not yet run green: the box is held.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): the pane back-settle waits for an in-flight drill instead of minting the popped-to item (TASK-3423 PR B)

Kit 3 resolves a goto to afterNavigate across tasks, so a drill fired inside
the 140ms window could still be in flight when the settle fired; it minted
the popped-to item first (measured: URL still ?item=A at fire time, 2 of 4
runs). The settle now re-arms while navigating.current is set. Unit tests
cover the defer and the cancelled-navigation fallback; both die without
the defer. e2e not yet re-run on this commit.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): defer the back-settle on navigating.type, not a nonexistent navigating.current (TASK-3423 PR B)

79175a8bc read navigating.current, which kit 3's $app/state navigating does
not have: undefined !== null held always, so the settle deferred forever.
The R14 drill legs passed for that wrong reason and 432 (a drill AFTER the
window still sees A minted) failed 5/5. navigating.type is null exactly
when no navigation is in flight. Measured at this commit: pane-async-race
373 and 432 and capstone 645, 5 repeats each, 15/15 pass.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): the list's restore-skip is released by the next navigation, not a microtask (TASK-3423 PR B)

Closing the pane from a scrolled column jumped the list (bug-3165 e2e):
the handoff wrote the anchored position at 52ms, then the scroll restore
applied the list entry's saved offset (top 600) from 82ms on (measured with
a scroll-write probe after Escape). Kit 3 calls snapshot.restore after an
await in its navigate, so the queueMicrotask release ran before the
restore it was meant to skip. The skip now belongs to the navigation that
armed it: its own restore consumes it and the next beforeNavigate releases
any remainder. bug-3165 spec: 6/6 desktop legs pass.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): editor menus unsubscribe from the editor they subscribed to, not the live prop (TASK-3423 PR B)

version-restore-collab (BUG-2264's e2e) blanked the item page after a
restore under live collab, 3/3 on kit 3: the force-refresh remount ran
EditorBubbleMenu's and EditorLinkPopover's effect cleanups, which read the
live editor prop. That read re-entered Svelte's flush and destroyed the
editor mid-transaction (state_unsafe_mutation from ProseMirror's blur
handler), and a null prop then threw on .off, killing the page (stacks
captured with Error.stackTraceLimit=100). The cleanups now close over the
editor their run subscribed to.

Evidence is the e2e: red 3/3 at 347942e17, green 3/3 (2 tests x 3) with
this change alone. A jsdom unit test could not reproduce it: there Svelte
hands a teardown the pre-teardown prop value, so the old cleanup passed
too, and a test that passes either way was not kept.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): kit 3 in code main added since PR B: $app/env, goto reset in the library tabs (TASK-3423)

* test(web): re-review the identity-gate rows the rebase onto main re-hashed (TASK-3423)

collection page: quickCreateInColumn, the two pane-follow continuations,
installPaneTestHook, loadUrlFilters and updateUrlFilters. Only kit 3 API swaps
reach them (page.url.href, goto reset:false); no await or fence moved.
library page: the tab-switch goto now restores focus to the tab after the
navigation (kit 3 has no keepFocus); its continuation writes no state and is
tabled.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): quick capture's Open lands in the title editor under kit 3 (TASK-3423)

The captured item's URL carries ?new=1, which opens it with its title in
the editor. Under kit 2 the follow-up goto's focus reset closed the editor
at once, so the title read as text; kit 3's reset:false keeps it open
(site 3 of the day-87 ruling). Assert the editor's value instead.

Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF

* test(e2e): login honours ?redirect= and still refuses off-site targets (TASK-3423)

The login page now reads ?redirect= through $app/state's page, and that
value is validateRedirect's input. Nothing exercised the read: the share
and invite specs check only the link that points at /login. Covers the
form path from a signed-out context, the signed-in forward, and three
off-site shapes (absolute, protocol-relative, encoded slash).

Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF

* test(e2e): name the encoded-slash case by what the guard receives; off-site on the form path too (TASK-3423 r2)

Codex r2: the encoded-slash case is a value that still holds %2f after
searchParams decodes it once; the label and a comment now say so. Hunter
r2 L1: the form sign-in also gets an off-site target. Hunter r2 L2: the
off-site cases wait on one poll for "this origin, past /login", so an
intermediate URL cannot satisfy half of it.

Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF

* fix(web): the activity page's URL clear uses kit 3's goto options (TASK-3423)

#2226 landed dropFromUrl with goto's noScroll/keepFocus, which kit 3 removed
(they throw in dev and are ignored in prod), and new URL(page.url) on the
$app/state proxy. reset: false keeps scroll and focus as both options did;
page.url.href gives URL a string.

Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF
2026-10-18 19:03:13 -04:00

187 lines
7.9 KiB
TypeScript

import { defineConfig } from 'vitest/config';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
import { realpathSync } from 'node:fs';
import {
BROWSER_TEST_GLOB,
IDB_TEST_GLOB,
NODE_TEST_GLOB,
PROJECT_REQUIREMENTS,
createProjectCountReporter,
findUnsatisfiedProjects,
formatUnsatisfiedProjectsError,
} from './src/test/vitestProjects.ts';
import { dynamicCompileOptions } from './svelteCompileOptions.ts';
// Multi-project vitest setup (TASK-2081 / PLAN-1984, plus `idb` from
// PLAN-2636 unit 1):
//
// - `node` — the existing pure-TS unit suite. Plain node environment, no
// Svelte plugin (fast; matches the pre-TASK-2081 behavior).
// - `jsdom` — `.svelte` component + `.svelte.ts` rune-module tests. Runs in a
// browser-like DOM with the Svelte plugin so runes/components
// compile, and aliases `$app/env` to a browser=true mock.
//
// Split by filename: `*.svelte.test.ts` routes to jsdom, everything else
// (`*.test.ts`) stays on node. Keeping the node suite out of jsdom avoids
// slowing/altering the pure-logic tests.
//
// The non-`node` projects need devDependencies (`jsdom`,
// `@testing-library/svelte`, `@testing-library/jest-dom`,
// `@sveltejs/vite-plugin-svelte`, `fake-indexeddb`). They used to be probed with
// `require.resolve` and the project SKIPPED when they did not resolve, which made
// an incomplete install narrow the run instead of failing it — `npm test` exited
// 0 having executed none of the `*.svelte.test.ts` suites. Since BUG-3045 an
// unresolvable dependency REFUSES the run instead; see
// `src/test/vitestProjects.ts` for the reasoning and the messages.
const require = createRequire(import.meta.url);
function canResolve(id: string): boolean {
try {
require.resolve(id);
return true;
} catch {
return false;
}
}
const projectRoot = fileURLToPath(new URL('.', import.meta.url));
const $lib = fileURLToPath(new URL('./src/lib', import.meta.url));
const appEnvironmentMock = fileURLToPath(new URL('./src/test/mocks/app-environment.ts', import.meta.url));
const appStateMock = fileURLToPath(new URL('./src/test/mocks/app-state.ts', import.meta.url));
const appNavigationMock = fileURLToPath(new URL('./src/test/mocks/app-navigation.ts', import.meta.url));
// Agent worktrees symlink `web/node_modules` to the main checkout's
// node_modules rather than `npm ci`-ing a copy (running npm ci THROUGH the
// symlink deletes the shared tree — see CLAUDE.md's "Working in a git
// worktree" section, which also covers the svelte-kit sync prerequisite).
// Vite's dev-server fs-access guard checks a
// requested file's REALPATH against `server.fs.allow`, which defaults to the
// project root and its ancestors — a node_modules symlink that resolves
// outside that root (the worktree lives under a sibling directory tree) gets
// denied, breaking every jsdom-project test that imports a real package
// (e.g. `@testing-library/svelte/vitest`) with a confusing "does the file
// exist?" error even though it does. Explicitly allowing the resolved
// realpath fixes worktrees without changing behavior for a normal checkout,
// where the realpath is just `<project>/node_modules` — already inside the
// default allow-list. `projectRoot` MUST stay in the allow list alongside
// it — setting `server.fs.allow` REPLACES Vite's default (project root +
// ancestors), so omitting the root here would deny access to ordinary
// project source files (Codex review).
const nodeModulesRealPath = (() => {
try {
return realpathSync(fileURLToPath(new URL('./node_modules', import.meta.url)));
} catch {
return undefined;
}
})();
// Admission check for every project that has dependencies of its own — the
// `jsdom` project (TASK-2081) and the `idb` project (PLAN-2636 unit 1). Both
// used to self-disable when their deps did not resolve; both now refuse, at
// config-load time, before a single file is collected (BUG-3045).
//
// Module scope, so the refusal lands while the config is still being LOADED —
// before any project is constructed and before a single test file is collected.
// Measured: vitest reports it as a startup error ("failed to load config") and
// exits 1; it does not fall back to a default config (BUG-3045 trail).
const unsatisfiedProjects = findUnsatisfiedProjects(PROJECT_REQUIREMENTS, canResolve);
if (unsatisfiedProjects.length > 0) {
throw new Error(formatUnsatisfiedProjectsError(unsatisfiedProjects));
}
// The three globs are declared in src/test/vitestProjects.ts alongside
// `projectForTestFile`, so the config and the guard test that checks no test
// file falls through every project read one declaration rather than two copies
// (BUG-3045, codex round 1).
//
// IDB-backed persistence tests carry the `.idb.test.ts` suffix and must be
// excluded from the node project (which has no indexedDB — the persistence layer
// would silently no-op there and the test would pass vacuously) the same way the
// svelte glob is.
const nodeProject = {
resolve: { alias: { $lib } },
test: {
name: 'node',
environment: 'node',
include: [NODE_TEST_GLOB],
// The jsdom / idb projects own these; they'd blow up or no-op in the
// plain node env.
exclude: [BROWSER_TEST_GLOB, IDB_TEST_GLOB],
},
};
const idbProject = {
resolve: { alias: { $lib } },
server: nodeModulesRealPath
? { fs: { allow: [projectRoot, nodeModulesRealPath] } }
: undefined,
test: {
name: 'idb',
// fake-indexeddb runs in plain node — no DOM needed. Its setup installs
// a fresh in-memory IndexedDB per test.
environment: 'node',
include: [IDB_TEST_GLOB],
setupFiles: ['./src/test/setup-idb.ts'],
},
};
export default defineConfig(async () => {
// Dynamic import because these are heavy and only the jsdom project uses them.
// They are guaranteed resolvable: the admission check above refused the run
// otherwise.
const { svelte } = await import('@sveltejs/vite-plugin-svelte');
const { svelteTesting } = await import('@testing-library/svelte/vite');
// Every project is registered unconditionally (BUG-3045). A project that
// cannot run does not get dropped from this list — it stops the run.
const projects: Record<string, unknown>[] = [
nodeProject,
idbProject,
{
// The compile options come from the module vite.config.ts uses too,
// and configFile: false stops vite-plugin-svelte looking for a
// svelte.config.js of its own (TASK-3423).
plugins: [svelte({ configFile: false, dynamicCompileOptions }), svelteTesting()],
resolve: {
alias: {
$lib,
// No SvelteKit plugin in this project, so provide `$app/env`
// and `$app/state` — without a provider these don't just come back
// undefined, they fail to RESOLVE, which is a load-time error for
// any component that imports them (and one `vi.mock` can't rescue,
// since resolution happens first).
'$app/env': appEnvironmentMock,
'$app/state': appStateMock,
// `$app/navigation` for the same reason (TASK-2430) — without it
// Sidebar / TopBar / PaneHost can't even be IMPORTED under jsdom.
'$app/navigation': appNavigationMock,
},
},
server: nodeModulesRealPath
? { fs: { allow: [projectRoot, nodeModulesRealPath] } }
: undefined,
test: {
name: 'jsdom',
environment: 'jsdom',
include: [BROWSER_TEST_GLOB],
setupFiles: ['./src/test/setup-jsdom.ts'],
},
},
];
return {
test: {
projects,
// `default` is vitest's own reporter, and naming any reporter REPLACES
// the default list rather than adding to it, so it has to be restated
// here to keep the ordinary output. Measured, not assumed: with
// `default` dropped, a run prints this line and nothing else. The added one prints how many projects actually ran files, so
// a narrowed run (`--project`, or a file filter) says so in its own
// output rather than looking identical to a full one (BUG-3045).
reporters: ['default', createProjectCountReporter()],
},
};
});