revert(test): abandon shared-mock dedup — infeasible in vitest browser mode
CI proved cross-file sharing of a virtual-module mock body cannot work in
@vitest/browser-playwright 4.1.6: the static-import spread fails the hoist
("no top level variables"), and the await-vi.hoisted-import form fails to
parse ("Unexpected identifier 'vi'"). vi.hoisted has the same hoist
constraint as vi.mock, so there is no way to thread an external module's
body into the factory here.
Reverts Phase 1: restores the 4 $app/forms/$app/navigation specs to their
inline factories, inlines NotificationBell.spec's forms stub, deletes the
src/__mocks__/$app/* modules and the $mocks alias (vite, vitest-coverage,
kit). The no-factory-ban meta-test stays (no-factory vi.mock is still
banned). ADR-012 amended to record the infeasibility. Everything else
($app/state migration, confirm context-inject, notification refactor, the
pin, the meta-test) is unaffected. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,36 @@ import { page, userEvent } from 'vitest/browser';
|
||||
|
||||
import ChronikFuerDichBox from './ChronikFuerDichBox.svelte';
|
||||
import type { NotificationItem } from '$lib/notification/notifications.svelte';
|
||||
const formsMock = await vi.hoisted(() => import('$mocks/$app/forms'));
|
||||
|
||||
vi.mock('$app/forms', () => ({ ...formsMock }));
|
||||
const mockFormResult = vi.hoisted(() => ({ type: 'success' as string }));
|
||||
|
||||
vi.mock('$app/forms', () => ({
|
||||
enhance(
|
||||
node: HTMLFormElement,
|
||||
submit?: (opts: {
|
||||
formData: FormData;
|
||||
}) => (opts: {
|
||||
result: { type: string; data?: Record<string, unknown> };
|
||||
update: () => Promise<void>;
|
||||
}) => Promise<void>
|
||||
) {
|
||||
const handler = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const cb = submit?.({ formData: new FormData(node) } as never);
|
||||
if (typeof cb === 'function') {
|
||||
await (
|
||||
cb as (o: { result: typeof mockFormResult; update: () => Promise<void> }) => Promise<void>
|
||||
)({ result: mockFormResult, update: async () => {} });
|
||||
}
|
||||
};
|
||||
node.addEventListener('submit', handler);
|
||||
return { destroy: () => node.removeEventListener('submit', handler) };
|
||||
}
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mockFormResult.type = 'success';
|
||||
});
|
||||
|
||||
function notif(partial: Partial<NotificationItem>): NotificationItem {
|
||||
@@ -152,7 +176,7 @@ describe('ChronikFuerDichBox', () => {
|
||||
});
|
||||
|
||||
it('shows an accessible error banner when the dismiss action returns a failure', async () => {
|
||||
formsMock.setFormResult({ type: 'failure' });
|
||||
mockFormResult.type = 'failure';
|
||||
render(ChronikFuerDichBox, {
|
||||
unread: [notif({ id: 'err-1' })],
|
||||
optimisticMarkRead: vi.fn(),
|
||||
|
||||
@@ -3,12 +3,36 @@ import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import ChronikFuerDichBox from './ChronikFuerDichBox.svelte';
|
||||
import type { NotificationItem } from '$lib/notification/notifications';
|
||||
const formsMock = await vi.hoisted(() => import('$mocks/$app/forms'));
|
||||
|
||||
vi.mock('$app/forms', () => ({ ...formsMock }));
|
||||
const mockFormResult = vi.hoisted(() => ({ type: 'success' as string }));
|
||||
|
||||
vi.mock('$app/forms', () => ({
|
||||
enhance(
|
||||
node: HTMLFormElement,
|
||||
submit?: (opts: {
|
||||
formData: FormData;
|
||||
}) => (opts: {
|
||||
result: { type: string; data?: Record<string, unknown> };
|
||||
update: () => Promise<void>;
|
||||
}) => Promise<void>
|
||||
) {
|
||||
const handler = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const cb = submit?.({ formData: new FormData(node) } as never);
|
||||
if (typeof cb === 'function') {
|
||||
await (
|
||||
cb as (o: { result: typeof mockFormResult; update: () => Promise<void> }) => Promise<void>
|
||||
)({ result: mockFormResult, update: async () => {} });
|
||||
}
|
||||
};
|
||||
node.addEventListener('submit', handler);
|
||||
return { destroy: () => node.removeEventListener('submit', handler) };
|
||||
}
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mockFormResult.type = 'success';
|
||||
});
|
||||
|
||||
const mention = (overrides: Partial<NotificationItem> = {}): NotificationItem => ({
|
||||
@@ -136,7 +160,7 @@ describe('ChronikFuerDichBox', () => {
|
||||
});
|
||||
|
||||
it('shows an accessible error banner when the dismiss action returns a failure', async () => {
|
||||
formsMock.setFormResult({ type: 'failure' });
|
||||
mockFormResult.type = 'failure';
|
||||
render(ChronikFuerDichBox, {
|
||||
props: {
|
||||
unread: [mention({ id: 'err-1' })],
|
||||
|
||||
@@ -4,10 +4,17 @@ import { m } from '$lib/paraglide/messages.js';
|
||||
import type { NotificationItem } from '$lib/notification/notifications';
|
||||
import NotificationFixture from './notification.test-fixture.svelte';
|
||||
|
||||
const formsMock = await vi.hoisted(() => import('$mocks/$app/forms'));
|
||||
|
||||
vi.mock('$app/navigation', () => ({ goto: vi.fn(), beforeNavigate: vi.fn() }));
|
||||
vi.mock('$app/forms', () => ({ ...formsMock }));
|
||||
vi.mock('$app/forms', () => ({
|
||||
enhance(node: HTMLFormElement, submit?: (opts: { formData: FormData }) => unknown) {
|
||||
const handler = (e: Event) => {
|
||||
e.preventDefault();
|
||||
submit?.({ formData: new FormData(node) } as never);
|
||||
};
|
||||
node.addEventListener('submit', handler);
|
||||
return { destroy: () => node.removeEventListener('submit', handler) };
|
||||
}
|
||||
}));
|
||||
|
||||
// NotificationBell.onMount calls store.init(), which opens an EventSource and
|
||||
// fetches the unread count. Stub both so no real network or 401 → /login
|
||||
|
||||
@@ -3,15 +3,41 @@ import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import { goto } from '$app/navigation';
|
||||
import NotificationDropdown from './NotificationDropdown.svelte';
|
||||
const formsMock = await vi.hoisted(() => import('$mocks/$app/forms'));
|
||||
|
||||
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
||||
|
||||
vi.mock('$app/forms', () => ({ ...formsMock }));
|
||||
// Configurable result for the enhance mock — tests that need failure set
|
||||
// mockFormResult.type = 'failure' before clicking.
|
||||
const mockFormResult = vi.hoisted(() => ({ type: 'success' as string }));
|
||||
|
||||
// Invoke the SubmitFunction and always call the returned result callback with
|
||||
// mockFormResult so tests can exercise both success and failure branches.
|
||||
vi.mock('$app/forms', () => ({
|
||||
enhance(
|
||||
node: HTMLFormElement,
|
||||
submit?: (opts: {
|
||||
formData: FormData;
|
||||
}) => (opts: {
|
||||
result: { type: string; data?: Record<string, unknown> };
|
||||
update: () => Promise<void>;
|
||||
}) => Promise<void>
|
||||
) {
|
||||
const handler = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const cb = submit?.({ formData: new FormData(node) } as never);
|
||||
if (typeof cb === 'function') {
|
||||
await cb({ result: mockFormResult, update: async () => {} } as never);
|
||||
}
|
||||
};
|
||||
node.addEventListener('submit', handler);
|
||||
return { destroy: () => node.removeEventListener('submit', handler) };
|
||||
}
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
mockFormResult.type = 'success'; // reset to default after each test
|
||||
});
|
||||
|
||||
const makeNotification = (overrides: Record<string, unknown> = {}) => ({
|
||||
@@ -209,7 +235,7 @@ describe('NotificationDropdown', () => {
|
||||
});
|
||||
|
||||
it('shows a role=alert error banner when mark-all-read returns a failure', async () => {
|
||||
formsMock.setFormResult({ type: 'failure' });
|
||||
mockFormResult.type = 'failure';
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [makeNotification()],
|
||||
@@ -346,7 +372,7 @@ describe('NotificationDropdown', () => {
|
||||
});
|
||||
|
||||
it('does NOT call onClose or goto when the dismiss action returns a failure', async () => {
|
||||
formsMock.setFormResult({ type: 'failure' });
|
||||
mockFormResult.type = 'failure';
|
||||
const onClose = vi.fn();
|
||||
const n = makeNotification({ id: 'n99', actorName: 'Bob' });
|
||||
render(NotificationDropdown, {
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
const navMock = await vi.hoisted(() => import('$mocks/$app/navigation'));
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('$app/navigation', () => ({ ...navMock }));
|
||||
// Capture the beforeNavigate callback so tests can simulate navigation events
|
||||
let registeredBeforeNavigate:
|
||||
| ((nav: { cancel: () => void; to: { url: { href: string } } | null }) => void)
|
||||
| null = null;
|
||||
|
||||
const { simulateNavigate, goto: mockGoto } = navMock;
|
||||
const mockGoto = vi.fn();
|
||||
|
||||
vi.mock('$app/navigation', () => ({
|
||||
beforeNavigate: vi.fn((fn: typeof registeredBeforeNavigate) => {
|
||||
registeredBeforeNavigate = fn;
|
||||
}),
|
||||
goto: mockGoto
|
||||
}));
|
||||
|
||||
const { createUnsavedWarning } = await import('./useUnsavedWarning.svelte');
|
||||
|
||||
function simulateNavigate(href: string | null = '/somewhere') {
|
||||
const cancel = vi.fn();
|
||||
registeredBeforeNavigate?.({
|
||||
cancel,
|
||||
to: href ? { url: { href } } : null
|
||||
});
|
||||
return cancel;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
registeredBeforeNavigate = null;
|
||||
mockGoto.mockClear();
|
||||
});
|
||||
|
||||
describe('createUnsavedWarning', () => {
|
||||
it('isDirty starts false', () => {
|
||||
const w = createUnsavedWarning();
|
||||
|
||||
Reference in New Issue
Block a user