diff --git a/components/contacts/contacts-sidebar.tsx b/components/contacts/contacts-sidebar.tsx index 5831e1b24..c645f2d3d 100644 --- a/components/contacts/contacts-sidebar.tsx +++ b/components/contacts/contacts-sidebar.tsx @@ -846,6 +846,9 @@ function AddressBookItem({ onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} + data-testid="address-book-item" + data-book-name={book.name} + data-account={book.accountName ?? ''} className={cn( "w-full flex items-center gap-2 ps-5 pe-3 text-sm transition-colors", isActive diff --git a/integration/tests/12-multi-account-contacts.spec.ts b/integration/tests/12-multi-account-contacts.spec.ts new file mode 100644 index 000000000..d275c7df0 --- /dev/null +++ b/integration/tests/12-multi-account-contacts.spec.ts @@ -0,0 +1,70 @@ +import { test, expect, type Page } from '@playwright/test'; +import { ACCOUNTS } from './helpers/config'; +import { + login, + addAccount, + enableProInterface, + openContacts, + addressBookRows, +} from './helpers/app'; + +/** + * Multi-account address-book aggregation across an account switch. + * + * Companion to the calendar spec for the cross-account id-namespacing fix. + * Contacts keep no persisted id-selection (unlike calendars' + * selectedCalendarIds), so there is no selection to reset; the observable here + * is that the aggregated cross-account address-book list stays correct and + * usable after switching the active account (the store now namespaces every + * account consistently, so ids no longer flip form on a switch). + */ +const { alice, bob } = ACCOUNTS; + +const switcher = (page: Page) => page.locator('[data-testid="account-switcher"]').first(); + +async function accountId(page: Page, email: string): Promise { + await switcher(page).click(); + const id = await page + .locator(`[data-testid="account-option"][data-account-email="${email}"]`) + .first() + .getAttribute('data-account-id'); + await page.keyboard.press('Escape'); + if (!id) throw new Error(`no account id for ${email}`); + return id; +} + +async function switchTo(page: Page, id: string): Promise { + await switcher(page).click(); + await page.locator(`[data-testid="account-option"][data-account-id="${id}"]`).first().click({ force: true }); + await expect + .poll(async () => switcher(page).getAttribute('data-active-account-id'), { timeout: 30000 }) + .toBe(id); +} + +test.describe('Multi-account contacts', () => { + test('address books from both accounts stay aggregated across a switch', async ({ page }) => { + await login(page, alice); + await addAccount(page, bob); + const bobId = await accountId(page, bob.email); + await enableProInterface(page); + + await openContacts(page); + + // Both accounts' (default) address books aggregate into one list with + // distinct names. + const before = await addressBookRows(page); + expect(before.length).toBeGreaterThanOrEqual(2); + const namesBefore = new Set(before.map((b) => b.name)); + expect(namesBefore.size).toBeGreaterThanOrEqual(2); + + // Switch the active account: the previously-active account's ids flip + // raw<->namespaced. The aggregated list must remain the same set (no loss, + // no duplication, no corruption from the id-form change). + await switchTo(page, bobId); + await openContacts(page); + + const after = await addressBookRows(page); + expect(after.length).toBe(before.length); + expect(new Set(after.map((b) => b.name))).toEqual(namesBefore); + }); +}); diff --git a/integration/tests/helpers/app.ts b/integration/tests/helpers/app.ts index 34383dc75..b671cc899 100644 --- a/integration/tests/helpers/app.ts +++ b/integration/tests/helpers/app.ts @@ -428,3 +428,26 @@ export async function enableProInterface(page: Page): Promise { await expect(toggle).toHaveAttribute('aria-checked', 'true'); } } + +// --- Contacts / address books (multi-account) ------------------------------ + +/** Navigate to contacts via in-app nav and wait for the address-book list. */ +export async function openContacts(page: Page): Promise { + await page.locator('a[href$="/contacts"]').first().click(); + await page + .locator('[data-testid="address-book-item"]') + .first() + .waitFor({ state: 'visible', timeout: 30000 }); +} + +/** All sidebar address books as {name, account}. */ +export async function addressBookRows( + page: Page, +): Promise> { + return page.locator('[data-testid="address-book-item"]').evaluateAll((els) => + els.map((e) => ({ + name: e.getAttribute('data-book-name') || '', + account: e.getAttribute('data-account') || '', + })), + ); +}