test: fix broken suites

This commit is contained in:
Linus Rath
2026-07-22 18:56:09 +02:00
parent 3f22a3323a
commit 813185e58d
6 changed files with 30 additions and 10 deletions
@@ -136,6 +136,7 @@ vi.mock('@/lib/plugin-hooks', () => ({
getRecipientSuggestions: { call: async () => [] }, getRecipientSuggestions: { call: async () => [] },
onSend: { call: async () => [] }, onSend: { call: async () => [] },
beforeSend: { call: async () => [] }, beforeSend: { call: async () => [] },
onRecipientChipsChange: { transform: async (chips: unknown) => chips },
}, },
contactHooks: { contactHooks: {
search: { call: async () => [] }, search: { call: async () => [] },
@@ -229,8 +230,10 @@ describe('RecipientChipInput drag and drop', () => {
const dt = new MockDataTransfer(); const dt = new MockDataTransfer();
fireEvent.dragStart(chipSpan, { dataTransfer: dt }); fireEvent.dragStart(chipSpan, { dataTransfer: dt });
// The enrichment pass may have stamped extra display metadata on the
// chip by drag time, so match the essential fields rather than deep-equal.
const payload = JSON.parse(dt.getData('application/x-recipient-chip')); const payload = JSON.parse(dt.getData('application/x-recipient-chip'));
expect(payload).toEqual({ recipient: { email: 'alice@example.com' }, fromField: 'to', fromIndex: 0 }); expect(payload).toMatchObject({ recipient: { email: 'alice@example.com' }, fromField: 'to', fromIndex: 0 });
}); });
it('keeps a display name with a comma in a single chip (array model)', async () => { it('keeps a display name with a comma in a single chip (array model)', async () => {
@@ -244,7 +247,7 @@ describe('RecipientChipInput drag and drop', () => {
const dt = new MockDataTransfer(); const dt = new MockDataTransfer();
fireEvent.dragStart(chipSpan, { dataTransfer: dt }); fireEvent.dragStart(chipSpan, { dataTransfer: dt });
const payload = JSON.parse(dt.getData('application/x-recipient-chip')); const payload = JSON.parse(dt.getData('application/x-recipient-chip'));
expect(payload).toEqual({ recipient: { name: 'Doo, John', email: 'john@doo.org' }, fromField: 'to', fromIndex: 0 }); expect(payload).toMatchObject({ recipient: { name: 'Doo, John', email: 'john@doo.org' }, fromField: 'to', fromIndex: 0 });
}); });
it('onDragEnd clears the opacity class on the chip', async () => { it('onDragEnd clears the opacity class on the chip', async () => {
@@ -135,6 +135,7 @@ vi.mock('@/lib/plugin-hooks', () => ({
getRecipientSuggestions: { call: async () => [] }, getRecipientSuggestions: { call: async () => [] },
onSend: { call: async () => [] }, onSend: { call: async () => [] },
beforeSend: { call: async () => [] }, beforeSend: { call: async () => [] },
onRecipientChipsChange: { transform: async (chips: unknown) => chips },
}, },
contactHooks: { contactHooks: {
search: { call: async () => [] }, search: { call: async () => [] },
+10 -1
View File
@@ -1,6 +1,15 @@
import { unlink, writeFileSync } from "fs"; import { mkdtempSync, unlink, writeFileSync } from "fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// The route consults admin-dashboard overrides (ADMIN_CONFIG_DIR, default
// data/admin) before env vars. Point it at an empty temp dir so local admin
// state on the developer's machine can't leak into these env-driven
// assertions. Must happen before the first GET, because the config manager
// singleton loads the directory once and caches it.
process.env.ADMIN_CONFIG_DIR = mkdtempSync(path.join(tmpdir(), 'bw-config-route-'));
// Mock NextResponse before importing the route // Mock NextResponse before importing the route
vi.mock('next/server', () => ({ vi.mock('next/server', () => ({
NextResponse: { NextResponse: {
@@ -302,6 +302,11 @@ describe('JMAPClient resilience', () => {
}); });
it('does not mark the connection lost or reconnect repeatedly while rate limited', async () => { it('does not mark the connection lost or reconnect repeatedly while rate limited', async () => {
// The file-level shouldAdvanceTime lets fake time creep forward with
// the wall clock, which can fire the 30s keep-alive an extra time on a
// slow or loaded machine. This test counts pings, so pin the clock and
// advance it explicitly.
vi.useFakeTimers({ shouldAdvanceTime: false });
const client = await createConnectedClient(); const client = await createConnectedClient();
const callback = vi.fn(); const callback = vi.fn();
client.onConnectionChange(callback); client.onConnectionChange(callback);
+4 -4
View File
@@ -247,18 +247,18 @@ describe('contact-store', () => {
expect(results.length).toBeLessThanOrEqual(10); expect(results.length).toBeLessThanOrEqual(10);
}); });
it('should expand group members when group name matches', () => { it('should suggest a matching group as a single entry with member count', () => {
const member1 = makeContact({ id: 'm1', name: { components: [{ kind: 'given', value: 'Alice' }], isOrdered: true }, emails: { e0: { address: 'alice@test.com' } } }); const member1 = makeContact({ id: 'm1', name: { components: [{ kind: 'given', value: 'Alice' }], isOrdered: true }, emails: { e0: { address: 'alice@test.com' } } });
const member2 = makeContact({ id: 'm2', name: { components: [{ kind: 'given', value: 'Bob' }], isOrdered: true }, emails: { e0: { address: 'bob@test.com' } } }); const member2 = makeContact({ id: 'm2', name: { components: [{ kind: 'given', value: 'Bob' }], isOrdered: true }, emails: { e0: { address: 'bob@test.com' } } });
const group = makeGroup({ id: 'g1', members: { m1: true, m2: true } }); const group = makeGroup({ id: 'g1', members: { m1: true, m2: true } });
useContactStore.setState({ contacts: [member1, member2, group] }); useContactStore.setState({ contacts: [member1, member2, group] });
const results = useContactStore.getState().getAutocomplete('Team'); const results = useContactStore.getState().getAutocomplete('Team');
expect(results).toHaveLength(2); expect(results).toHaveLength(1);
expect(results.map(r => r.email).sort()).toEqual(['alice@test.com', 'bob@test.com']); expect(results[0]).toEqual({ name: 'Team', email: '', group: { id: 'g1', memberCount: 2 } });
}); });
it('should not include group itself in results', () => { it('should not suggest a group with no addressable members', () => {
const group = makeGroup({ id: 'g1' }); const group = makeGroup({ id: 'g1' });
useContactStore.setState({ contacts: [group] }); useContactStore.setState({ contacts: [group] });
const results = useContactStore.getState().getAutocomplete('Team'); const results = useContactStore.getState().getAutocomplete('Team');
+5 -3
View File
@@ -9,9 +9,11 @@ export default defineConfig({
globals: true, globals: true,
setupFiles: ['./vitest.setup.ts'], setupFiles: ['./vitest.setup.ts'],
// integration/** is the dockerized Playwright suite (run via // integration/** is the dockerized Playwright suite (run via
// `npm run test:integration`); examples/** is untracked sample code. Both // `npm run test:integration`); examples/** is untracked sample code; and
// use their own runners and must not be collected by vitest. // repos/** holds sibling checkouts with their own runners. node_modules
exclude: ['e2e/**', 'integration/**', 'examples/**', 'node_modules/**', '.next/**'], // must be globbed at any depth or the nested repos/*/node_modules trees
// get collected too.
exclude: ['e2e/**', 'integration/**', 'examples/**', 'repos/**', '**/node_modules/**', '.next/**'],
}, },
resolve: { resolve: {
alias: { alias: {