Skip to content

Commit 0be5c93

Browse files
tyler-daneclaudecursoragent
authored
test(web): run isolated web suite directly (#1848)
* test(web): batch interim test runner * test(web): isolate direct bun suite * test(web): fix useGridLayout test stability in CI - Override getBoundingClientRect on BOTH HTMLElement.prototype and window.HTMLElement.prototype since JSDOM may use a different HTMLElement internally than globalThis - Use hardcoded test rects based on testid for consistent values - Remove intermediate ref callbacks, use hook refs directly - Move ResizeObserver mock setup to beforeEach for consistent timing - Replace waitFor with direct assertions after explicit timeout - Reset motion state in beforeEach/afterEach The tests were failing in CI because: 1. JSDOM creates its own HTMLElement class assigned to window.HTMLElement 2. Elements created by JSDOM use window.HTMLElement.prototype 3. Only overriding globalThis.HTMLElement.prototype wasn't sufficient 4. Must override both prototypes to ensure the mock is used Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(web): use renderHook for useGridLayout tests Use the canonical test pattern with renderHook and mock elements rather than rendering full components with prototype-level getBoundingClientRect overrides. This approach is consistent with useCalendarGridLayout tests and avoids CI-specific prototype override issues. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(web): remove redundant useGridLayout test The useGridLayout hook is a thin wrapper around useCalendarGridLayout that passes DAYS_IN_VIEW=7 and isWeekInteractionMotionActive. All functionality is already covered by useCalendarGridLayout.test.tsx which tests the same 7-day layout measurements. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: replace run.ts with native bun cmds * fix(backend): address base api network scenario * chore(web): re-add PlannerSidebar tests * fix(web): restore planner sidebar shortcuts overlay The createPlannerSidebar refactor imported the fixed-position shortcut hint overlay instead of the sidebar modal, which blocked sidebar clicks in e2e tests and ignored isShortcutsOpen. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4b9ac43 commit 0be5c93

70 files changed

Lines changed: 1324 additions & 1635 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/development/testing-playbook.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ E2E workflow (`test-e2e.yml`) is separate and runs on pull requests to `main` vi
4545
## Current Test Strategy
4646

4747
- `bun run test:core` uses `bun test` with a small compatibility preload for the core BSON mock setup.
48-
- `bun run test:web`, `bun run test:backend`, and `bun run test:scripts` intentionally retain the existing Jest harness while their hoist-heavy module-mocking patterns are migrated.
49-
- `bun run test:<project>` is the stable CI-facing entrypoint for every package; the root dispatcher chooses the correct runner per project.
48+
- `bun run test:web` runs `bun test --cwd packages/web` directly. Web tests should be isolated enough to run in one Bun process without batching.
49+
- `bun run test:backend` and `bun run test:scripts` intentionally retain the existing Jest harness while their hoist-heavy module-mocking patterns are migrated.
5050

5151
## Retained Jest Layout
5252

@@ -116,6 +116,14 @@ Avoid:
116116
- implementation-detail assertions
117117
- unnecessary module-wide mocks
118118

119+
Isolation rules:
120+
121+
- Do not use top-level `mock.module` for shared production modules unless the test imports the subject through a local factory and the mock cannot affect later files. Prefer provider wrappers, real stores, explicit dependency factories, or `spyOn` with teardown.
122+
- Avoid mocking shared UI primitives such as `TooltipWrapper`, `@floating-ui/react`, or session hooks in broad component tests. A mock that only helps one file can change unrelated tests later in the same Bun process.
123+
- If a test replaces globals (`fetch`, `document.getElementById`, storage, timers, console methods), restore the original value in teardown.
124+
- Prefer `renderWithStore`, `createStoreWrapper`, or a focused provider harness over mocking `@web/store` or `store.hooks`.
125+
- `bun test --cwd packages/web` is the acceptance check for web test isolation. A focused test can pass while still leaking into the direct suite.
126+
119127
### Web Jest Harness Defaults (MSW + Globals)
120128

121129
Primary setup files:

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@
2626
"lint:fix": "biome check --write .",
2727
"format": "biome format --write .",
2828
"format:check": "biome format .",
29-
"test": "bun packages/scripts/src/testing/run.ts",
29+
"test": "bun test:core && bun test:web && bun test:backend && bun test:scripts",
3030
"test:e2e": "bunx playwright test",
31-
"test:backend": "bun packages/scripts/src/testing/run.ts backend",
32-
"test:core": "bun packages/scripts/src/testing/run.ts core",
33-
"test:web": "bun packages/scripts/src/testing/run.ts web",
34-
"test:scripts": "bun packages/scripts/src/testing/run.ts scripts",
31+
"test:backend": "./node_modules/.bin/jest --selectProjects backend",
32+
"test:core": "bun test packages/core/src --preload packages/scripts/src/testing/core.preload.ts",
33+
"test:web": "bun test --cwd packages/web",
34+
"test:scripts": "./node_modules/.bin/jest scripts",
3535
"type-check": "bunx typescript@6.0.3 --noEmit && bunx typescript@6.0.3 -p packages/web/tsconfig.app.json --noEmit && bun run type-check:web-tests",
3636
"type-check:web-tests": "bunx typescript@6.0.3 -p packages/web/tsconfig.test.json --noEmit",
3737
"verify": "bun packages/scripts/src/testing/verify.ts",

packages/scripts/src/testing/run.ts

Lines changed: 0 additions & 128 deletions
This file was deleted.

packages/scripts/src/testing/verify.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { execSync } from "node:child_process";
55
* Detects which packages changed via git diff and runs the minimum
66
* necessary test suites plus type-check.
77
*
8-
* All test execution is delegated to run.ts — no commands are duplicated here.
8+
* Test execution is delegated to the root `test:<project>` package.json scripts.
99
*
1010
* Usage:
1111
* bun run verify — auto-detect from git diff
@@ -79,7 +79,7 @@ function mapFilesToPackages(files: string[]): Package[] {
7979
function runPackage(pkg: Package): boolean {
8080
console.log(`\n→ test:${pkg}`);
8181
const result = bunRuntime.spawnSync({
82-
cmd: ["bun", "packages/scripts/src/testing/run.ts", pkg],
82+
cmd: ["bun", "run", `test:${pkg}`],
8383
cwd: process.cwd(),
8484
env: {
8585
...process.env,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { configureStore, type PreloadedState } from "@reduxjs/toolkit";
2+
import {
3+
type RenderHookOptions,
4+
render,
5+
renderHook,
6+
} from "@testing-library/react";
7+
import { type PropsWithChildren, type ReactElement } from "react";
8+
import { Provider } from "react-redux";
9+
import { sagaMiddleware } from "@web/common/store/middlewares";
10+
import { type RootState } from "@web/store";
11+
import { reducers } from "@web/store/reducers";
12+
13+
export function createTestStore(preloadedState?: PreloadedState<RootState>) {
14+
return configureStore({
15+
reducer: reducers,
16+
preloadedState,
17+
middleware: (getDefaultMiddleware) =>
18+
getDefaultMiddleware({
19+
thunk: false,
20+
serializableCheck: false,
21+
immutableCheck: false,
22+
}).concat(sagaMiddleware),
23+
});
24+
}
25+
26+
export function createStoreWrapper(preloadedState?: PreloadedState<RootState>) {
27+
const store = createTestStore(preloadedState);
28+
29+
function StoreWrapper({ children }: PropsWithChildren) {
30+
return <Provider store={store}>{children}</Provider>;
31+
}
32+
33+
return { store, wrapper: StoreWrapper };
34+
}
35+
36+
export function renderWithStore(
37+
ui: ReactElement,
38+
preloadedState?: PreloadedState<RootState>,
39+
) {
40+
const { store, wrapper } = createStoreWrapper(preloadedState);
41+
42+
return {
43+
store,
44+
...render(ui, { wrapper }),
45+
};
46+
}
47+
48+
export function renderHookWithStore<Result, Props>(
49+
hook: (initialProps: Props) => Result,
50+
preloadedState?: PreloadedState<RootState>,
51+
options?: Omit<RenderHookOptions<Props>, "wrapper">,
52+
) {
53+
const { store, wrapper } = createStoreWrapper(preloadedState);
54+
55+
return {
56+
store,
57+
...renderHook(hook, { ...options, wrapper }),
58+
};
59+
}

packages/web/src/__tests__/web.preload.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,32 @@ mockNodeModules();
274274
const sessionModule = await import("supertokens-web-js/recipe/session");
275275
const { cleanup } = await import("@testing-library/react");
276276

277+
function resetDocument() {
278+
document.body.innerHTML = "";
279+
document.body.removeAttribute("style");
280+
document.body.removeAttribute("class");
281+
document.body.removeAttribute("data-app-locked");
282+
document.documentElement.removeAttribute("style");
283+
for (const style of document.head.querySelectorAll("style")) {
284+
if (
285+
!style.textContent?.includes(":has(.react-datepicker__day--selected)")
286+
) {
287+
continue;
288+
}
289+
290+
style.textContent = style.textContent.replaceAll(
291+
/[^{}]+:has\(\.react-datepicker__day--selected\)[^{]*\{[^{}]*\}/g,
292+
"",
293+
);
294+
}
295+
}
296+
297+
function resetBrowserState() {
298+
dom.reconfigure({ url: "http://localhost/" });
299+
localStorage.clear();
300+
sessionStorage.clear();
301+
}
302+
277303
beforeEach(() => {
278304
sessionModule.doesSessionExist?.mockResolvedValue(true);
279305
});
@@ -282,6 +308,8 @@ beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
282308
afterEach(async () => {
283309
await Promise.resolve();
284310
cleanup();
311+
resetDocument();
312+
resetBrowserState();
285313
server.resetHandlers();
286314
});
287315
afterAll(() => server.close());
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
type Dispatch = (action: unknown) => unknown;
2+
3+
export type CompleteAuthenticationDependencies = {
4+
authSuccess: () => unknown;
5+
clearAnonymousCalendarChangeSignUpPrompt: () => void;
6+
markUserAsAuthenticated: (email?: string) => void;
7+
refreshUserMetadata: () => Promise<unknown> | unknown;
8+
syncPendingLocalEvents: () => Promise<unknown>;
9+
triggerFetch: () => unknown;
10+
useAppDispatch: () => Dispatch;
11+
useSession: () => {
12+
setAuthenticated: (isAuthenticated: boolean) => void;
13+
};
14+
};
15+
16+
export function createUseCompleteAuthentication(
17+
dependencies: CompleteAuthenticationDependencies,
18+
) {
19+
return function useCompleteAuthenticationWithDependencies() {
20+
const dispatch = dependencies.useAppDispatch();
21+
const { setAuthenticated } = dependencies.useSession();
22+
23+
return async ({
24+
email,
25+
onComplete,
26+
}: {
27+
email?: string;
28+
onComplete?: () => void;
29+
}) => {
30+
dependencies.clearAnonymousCalendarChangeSignUpPrompt();
31+
dependencies.markUserAsAuthenticated(email);
32+
setAuthenticated(true);
33+
dispatch(dependencies.authSuccess());
34+
35+
void dependencies.refreshUserMetadata();
36+
37+
await dependencies.syncPendingLocalEvents();
38+
39+
dispatch(dependencies.triggerFetch());
40+
onComplete?.();
41+
};
42+
};
43+
}

0 commit comments

Comments
 (0)