mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-27 06:47:07 +00:00
a0c7d68aa9
* 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
187 lines
7.9 KiB
TypeScript
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()],
|
|
},
|
|
};
|
|
});
|