diff --git a/components/email/__tests__/recipient-chip-drag.test.tsx b/components/email/__tests__/recipient-chip-drag.test.tsx index f3138b38..97e88bc8 100644 --- a/components/email/__tests__/recipient-chip-drag.test.tsx +++ b/components/email/__tests__/recipient-chip-drag.test.tsx @@ -136,6 +136,7 @@ vi.mock('@/lib/plugin-hooks', () => ({ getRecipientSuggestions: { call: async () => [] }, onSend: { call: async () => [] }, beforeSend: { call: async () => [] }, + onRecipientChipsChange: { transform: async (chips: unknown) => chips }, }, contactHooks: { search: { call: async () => [] }, @@ -229,8 +230,10 @@ describe('RecipientChipInput drag and drop', () => { const dt = new MockDataTransfer(); 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')); - 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 () => { @@ -244,7 +247,7 @@ describe('RecipientChipInput drag and drop', () => { const dt = new MockDataTransfer(); fireEvent.dragStart(chipSpan, { dataTransfer: dt }); 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 () => { diff --git a/components/email/__tests__/recipient-paste.test.tsx b/components/email/__tests__/recipient-paste.test.tsx index a8d4d15c..14b8637f 100644 --- a/components/email/__tests__/recipient-paste.test.tsx +++ b/components/email/__tests__/recipient-paste.test.tsx @@ -135,6 +135,7 @@ vi.mock('@/lib/plugin-hooks', () => ({ getRecipientSuggestions: { call: async () => [] }, onSend: { call: async () => [] }, beforeSend: { call: async () => [] }, + onRecipientChipsChange: { transform: async (chips: unknown) => chips }, }, contactHooks: { search: { call: async () => [] }, diff --git a/lib/__tests__/config-route.test.ts b/lib/__tests__/config-route.test.ts index 7787ec1d..7c62ac59 100644 --- a/lib/__tests__/config-route.test.ts +++ b/lib/__tests__/config-route.test.ts @@ -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'; +// 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 vi.mock('next/server', () => ({ NextResponse: { diff --git a/lib/__tests__/jmap-client-resilience.test.ts b/lib/__tests__/jmap-client-resilience.test.ts index cd13d5cb..cc3eaa17 100644 --- a/lib/__tests__/jmap-client-resilience.test.ts +++ b/lib/__tests__/jmap-client-resilience.test.ts @@ -302,6 +302,11 @@ describe('JMAPClient resilience', () => { }); 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 callback = vi.fn(); client.onConnectionChange(callback); diff --git a/stores/__tests__/contact-store.test.ts b/stores/__tests__/contact-store.test.ts index cd469e06..df6c32d5 100644 --- a/stores/__tests__/contact-store.test.ts +++ b/stores/__tests__/contact-store.test.ts @@ -247,18 +247,18 @@ describe('contact-store', () => { 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 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 } }); useContactStore.setState({ contacts: [member1, member2, group] }); const results = useContactStore.getState().getAutocomplete('Team'); - expect(results).toHaveLength(2); - expect(results.map(r => r.email).sort()).toEqual(['alice@test.com', 'bob@test.com']); + expect(results).toHaveLength(1); + 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' }); useContactStore.setState({ contacts: [group] }); const results = useContactStore.getState().getAutocomplete('Team'); diff --git a/vitest.config.ts b/vitest.config.ts index 27ee8288..e08c48c3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,9 +9,11 @@ export default defineConfig({ globals: true, setupFiles: ['./vitest.setup.ts'], // integration/** is the dockerized Playwright suite (run via - // `npm run test:integration`); examples/** is untracked sample code. Both - // use their own runners and must not be collected by vitest. - exclude: ['e2e/**', 'integration/**', 'examples/**', 'node_modules/**', '.next/**'], + // `npm run test:integration`); examples/** is untracked sample code; and + // repos/** holds sibling checkouts with their own runners. node_modules + // 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: { alias: {