Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/web/src/__tests__/__mocks__/server/mock.handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,22 @@ const createGoogleImportEvent: typeof createMockStandaloneEvent = (
dateDiff,
);

// Authenticated mounts that race past session auth may fetch /calendars.
// Do not register a global handler here: a default success changes event-list
// calendarIds and breaks suite-order-dependent hook/grid tests that expect
// the legacy undefined (all-calendars) read until calendars are seeded.
// Tests that need a default response can server.use(rest.get(...)) locally.

export const globalHandlers = [
rest.get("http://localhost/version.json", (_req, res, ctx) => {
return res(ctx.json({ version: "dev" }));
}),
rest.get(
`${ENV_WEB.API_BASEURL}/calendars/availability`,
(_req, res, ctx) => {
return res(ctx.status(Status.OK), ctx.json({ busyPeriods: [] }));
},
),
rest.get(`${ENV_WEB.API_BASEURL}/event`, (_req, res, ctx) => {
const events = [
createGoogleImportEvent(),
Expand Down Expand Up @@ -59,6 +71,12 @@ export const globalHandlers = [
rest.post(`${ENV_WEB.API_BASEURL}/user/metadata`, (req, res, ctx) => {
return res(ctx.status(Status.OK), ctx.json(req.json()));
}),
rest.get(`${ENV_WEB.API_BASEURL}/user/email-updates`, (_req, res, ctx) => {
return res(ctx.status(Status.OK), ctx.json({ status: "unavailable" }));
}),
rest.put(`${ENV_WEB.API_BASEURL}/user/email-updates`, (_req, res, ctx) => {
return res(ctx.status(Status.OK), ctx.json({ status: "subscribed" }));
}),
rest.post(`${ENV_WEB.API_BASEURL}/signinup`, (_req, res, ctx) => {
return res(ctx.json({ isNewUser: true }));
}),
Expand Down
125 changes: 121 additions & 4 deletions packages/web/src/__tests__/setup/jsdom-env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { JSDOM } from "jsdom";
import { inspect } from "node:util";

export const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
pretendToBeVisual: true,
Expand All @@ -23,10 +24,7 @@ Object.defineProperty(window, "HTMLIFrameElement", {
globalThis.HTMLIFrameElement = window.HTMLIFrameElement;
globalThis.HTMLAnchorElement = window.HTMLAnchorElement;
globalThis.Node = window.Node;
globalThis.Event = window.Event;
globalThis.CustomEvent = window.CustomEvent;
globalThis.MouseEvent = window.MouseEvent;
globalThis.KeyboardEvent = window.KeyboardEvent;

// Bun's native globalThis.dispatchEvent/addEventListener operate on Bun's own
// Event realm. Dexie constructs `new CustomEvent(...)` against the jsdom
// Event class above, so dispatching through Bun's native EventTarget throws
Expand All @@ -41,3 +39,122 @@ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
const noopAlert = () => {};
window.alert = noopAlert;
globalThis.alert = noopAlert;

// Bun/util.inspect walks jsdom Window/Event graphs by default (event
// listeners → document → SymbolTree → …), which can dump megabytes into
// failing-test diffs and console.error output. Keep a short label instead.
const inspectCustom = inspect.custom;

Object.defineProperty(window, inspectCustom, {
configurable: true,
value() {
return "Window [jsdom]";
},
});
Object.defineProperty(window.document, inspectCustom, {
configurable: true,
value() {
return "Document [jsdom]";
},
});
Object.defineProperty(window.Node.prototype, inspectCustom, {
configurable: true,
value(this: Node) {
const name = this.nodeName?.toLowerCase?.() ?? "node";
const id = this instanceof window.Element && this.id ? `#${this.id}` : "";
return `${name}${id} [jsdom]`;
},
});
Object.defineProperty(window.Event.prototype, inspectCustom, {
configurable: true,
value(this: Event) {
return `${this.constructor?.name ?? "Event"}(${this.type}) [jsdom]`;
},
});

// Bun's expect() diffs do not honor util.inspect.custom. They walk
// Event[Symbol(impl)]._globalObject into the full Window graph. Replace that
// field with a Proxy that still forwards gets for jsdom, but exposes no own
// keys for Bun's property enumerator.
function redactEventImplGlobalObject(event: Event) {
const implSym = Object.getOwnPropertySymbols(event).find(
(symbol) => String(symbol) === "Symbol(impl)",
);
if (!implSym) return;

const impl = (event as unknown as Record<symbol, Record<string, unknown>>)[
implSym
];
const globalObject = impl?._globalObject;
if (!globalObject || typeof globalObject !== "object") return;

const stub = new Proxy(globalObject, {
ownKeys() {
return [];
},
getOwnPropertyDescriptor() {
return undefined;
},
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
has(target, prop) {
return Reflect.has(target, prop);
},
});

Object.defineProperty(impl, "_globalObject", {
configurable: true,
enumerable: true,
writable: true,
value: stub,
});
}

const EVENT_CONSTRUCTOR_NAMES = [
"Event",
"CustomEvent",
"KeyboardEvent",
"MouseEvent",
"PointerEvent",
"FocusEvent",
"StorageEvent",
"InputEvent",
"WheelEvent",
"UIEvent",
"CompositionEvent",
"DragEvent",
"ClipboardEvent",
"SubmitEvent",
"MessageEvent",
"ErrorEvent",
"ProgressEvent",
] as const;

type EventConstructor = new (...args: never[]) => Event;

for (const name of EVENT_CONSTRUCTOR_NAMES) {
const Original = window[name as keyof Window];
if (typeof Original !== "function") continue;
const OriginalCtor = Original as EventConstructor;

const Redacted = function RedactedEvent(
this: unknown,
...args: unknown[]
): Event {
const event = Reflect.construct(OriginalCtor, args, new.target ?? Redacted);
redactEventImplGlobalObject(event as Event);
return event as Event;
};

Redacted.prototype = OriginalCtor.prototype;
Object.defineProperty(Redacted, "name", { value: name });
Object.setPrototypeOf(Redacted, OriginalCtor);

Object.defineProperty(window, name, {
configurable: true,
writable: true,
value: Redacted,
});
(globalThis as Record<string, unknown>)[name] = Redacted;
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,7 @@ const toStrictEvent = (event: CompassEvent): Event =>

function Provider({ children }: PropsWithChildren) {
// useState initializer: one client per mounted tree. Rebuilding an empty
// client on re-render makes the grid's calendars query really fetch
// /api/calendars (no handler here) - timing-dependent on slow CI runners.
// client on re-render drops seeded event/pending-mutation cache.
const [queryClient] = useState(() => {
const client = createCompassQueryClient();
seedPendingEventMutations(client, pendingEventIds);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,8 @@ const measurements = {

// useState initializer: exactly one client per mounted tree (matches
// eventReadOnlyInteraction.test.tsx's Provider) - seeding in the render body
// would rebuild an empty client on every re-render and the fresh cache would
// then really try to fetch /api/calendars and /api/calendars/availability
// (no handlers here), a timing-dependent failure on slow CI runners.
// would rebuild an empty client on every re-render and drop the fixture
// calendars/availability cache.
function Provider({ children }: PropsWithChildren) {
const [queryClient] = useState(() => {
const client = createCompassQueryClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ let seededCalendars: Calendar[] = [];
function Provider({ children }: PropsWithChildren) {
// useState initializer: exactly one client per mounted tree. Creating and
// seeding in the render body rebuilds an EMPTY client on every re-render,
// and the fresh cache then really fetches /api/calendars (no handler in
// this file) - a timing-dependent failure that only shows on slow (CI)
// runners.
// which drops the seeded calendars/events and races a network refetch.
const [queryClient] = useState(() => {
const client = createCompassQueryClient();
seedEventQueries(client, seededEvents);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ const renderShortcuts = (options?: {
initialData: toNormalizedEventQueryData(events),
});
// Always seed calendars so useWeekEventViewModel's visibility filter and
// useCalendarsQuery don't race a network fetch (MSW has no /api/calendars
// handler in this file). Default = writable calendar for the editable event.
// useCalendarsQuery see the fixture calendars instead of racing a fetch.
// Default = writable calendar for the editable event.
queryClient.setQueryData(
calendarQueryKeys.all,
options?.calendars ?? [writableCalendar],
Expand Down