Five test files mocked $lib/shared/services/confirm.svelte under BOTH spellings (.svelte and .svelte.js) within the same file; two more mocked only the .svelte.js form. Both resolve to the same module URL but register two distinct Playwright route handlers in @vitest/browser-playwright. The cleanup logic only removes one, leaving an orphan that fires when the next session loads the module — crashing the run with "[birpc] rpc is closed, cannot call resolveManualMock". This is the exact trigger fixed upstream by vitest PR #10267 (issue #9957). Normalise every confirm.svelte mock to the no-extension form, matching production imports and the source file basename (confirm.svelte.ts). After this commit: 8 confirm.svelte mocks across 8 spec files, all under one canonical ID. A meta-test (next commit) prevents the duplicate-id pattern from reappearing. Refs: #553 · vitest-dev/vitest#9957 · vitest-dev/vitest#10267 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
130 lines
4.4 KiB
TypeScript
130 lines
4.4 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { cleanup, render } from 'vitest-browser-svelte';
|
|
import { page } from 'vitest/browser';
|
|
|
|
vi.mock('$lib/shared/services/confirm.svelte', () => ({
|
|
getConfirmService: () => ({ confirm: async () => false })
|
|
}));
|
|
|
|
const { default: AdminUserEditPage } = await import('./+page.svelte');
|
|
|
|
afterEach(cleanup);
|
|
|
|
const baseEditUser = {
|
|
id: 'u1',
|
|
firstName: 'Anna',
|
|
lastName: 'Schmidt',
|
|
email: 'anna@example.com',
|
|
birthDate: '',
|
|
contact: '',
|
|
groups: [{ id: 'g1' }, { id: 'g2' }]
|
|
};
|
|
|
|
const baseData = (overrides: Record<string, unknown> = {}) => ({
|
|
editUser: baseEditUser,
|
|
groups: [
|
|
{ id: 'g1', name: 'Familie' },
|
|
{ id: 'g2', name: 'Admins' },
|
|
{ id: 'g3', name: 'Gäste' }
|
|
],
|
|
...overrides
|
|
});
|
|
|
|
describe('admin/users/[id] page', () => {
|
|
it('renders the edit heading with the user email', async () => {
|
|
render(AdminUserEditPage, { props: { data: baseData(), form: undefined } });
|
|
|
|
await expect.element(page.getByRole('heading', { name: /anna@example/i })).toBeVisible();
|
|
});
|
|
|
|
it('renders all three card sections', async () => {
|
|
render(AdminUserEditPage, { props: { data: baseData(), form: undefined } });
|
|
|
|
await expect.element(page.getByRole('heading', { name: /persönliche daten/i })).toBeVisible();
|
|
await expect.element(page.getByRole('heading', { name: /^gruppen$/i })).toBeVisible();
|
|
await expect.element(page.getByRole('heading', { name: /neues passwort/i })).toBeVisible();
|
|
});
|
|
|
|
it('shows the update success banner when form.success is true', async () => {
|
|
render(AdminUserEditPage, { props: { data: baseData(), form: { success: true } } });
|
|
|
|
await expect.element(page.getByText('Änderungen gespeichert.')).toBeVisible();
|
|
});
|
|
|
|
it('shows the update error banner when form.error is set', async () => {
|
|
render(AdminUserEditPage, {
|
|
props: { data: baseData(), form: { error: 'E-Mail bereits vergeben' } }
|
|
});
|
|
|
|
await expect.element(page.getByText('E-Mail bereits vergeben')).toBeVisible();
|
|
});
|
|
|
|
it('preselects the user groups in UserGroupsSection', async () => {
|
|
render(AdminUserEditPage, { props: { data: baseData(), form: undefined } });
|
|
|
|
const checkboxes = Array.from(
|
|
document.querySelectorAll('input[name="groupIds"]')
|
|
) as HTMLInputElement[];
|
|
const checked = checkboxes.filter((c) => c.checked).map((c) => c.value);
|
|
expect(checked.sort()).toEqual(['g1', 'g2']);
|
|
});
|
|
|
|
it('renders cancel link to /admin/users', async () => {
|
|
render(AdminUserEditPage, { props: { data: baseData(), form: undefined } });
|
|
|
|
const cancel = document.querySelector('a[href="/admin/users"]');
|
|
expect(cancel).not.toBeNull();
|
|
});
|
|
|
|
it('renders the delete button', async () => {
|
|
render(AdminUserEditPage, { props: { data: baseData(), form: undefined } });
|
|
|
|
await expect.element(page.getByRole('button', { name: /löschen/i })).toBeVisible();
|
|
});
|
|
|
|
it('does not show success banner when form is undefined', async () => {
|
|
render(AdminUserEditPage, { props: { data: baseData(), form: undefined } });
|
|
|
|
const banner = document.querySelector('.bg-green-50');
|
|
expect(banner).toBeNull();
|
|
});
|
|
|
|
it('does not show error banner when form.error is undefined', async () => {
|
|
render(AdminUserEditPage, { props: { data: baseData(), form: { success: false } } });
|
|
|
|
// The error banner has both border-red-200 AND text-red-700 — the delete button has red-50
|
|
// background but is a button, not a div. Look for the specific error <div>.
|
|
const banner = document.querySelector('div.bg-red-50');
|
|
expect(banner).toBeNull();
|
|
});
|
|
|
|
it('handles a user with empty groups list (selectedGroupIds defaults to [])', async () => {
|
|
render(AdminUserEditPage, {
|
|
props: {
|
|
data: baseData({ editUser: { ...baseEditUser, groups: [] } }),
|
|
form: undefined
|
|
}
|
|
});
|
|
|
|
const checkboxes = Array.from(
|
|
document.querySelectorAll('input[name="groupIds"]')
|
|
) as HTMLInputElement[];
|
|
const checked = checkboxes.filter((c) => c.checked);
|
|
expect(checked.length).toBe(0);
|
|
});
|
|
|
|
it('handles a user with no groups field at all (defaults to [])', async () => {
|
|
const editUser = { ...baseEditUser } as typeof baseEditUser & { groups?: undefined };
|
|
delete (editUser as { groups?: unknown }).groups;
|
|
render(AdminUserEditPage, {
|
|
props: { data: baseData({ editUser }), form: undefined }
|
|
});
|
|
|
|
const checkboxes = Array.from(
|
|
document.querySelectorAll('input[name="groupIds"]')
|
|
) as HTMLInputElement[];
|
|
const checked = checkboxes.filter((c) => c.checked);
|
|
expect(checked.length).toBe(0);
|
|
});
|
|
});
|