test(integration): multi-account address-book aggregation across a switch

Companion to the calendar spec for the cross-account id-namespacing fix. Logs
in two accounts, enables the Pro shell, and asserts both accounts' address books
stay aggregated (same set, no loss/duplication/corruption) after switching the
active account. Contacts keep no persisted id-selection, so this verifies the
aggregated list stays correct/usable rather than a selection reset.

Adds a data-testid on address-book rows (address-book-item + data-book-name/
-account) and openContacts / addressBookRows helpers.
This commit is contained in:
Stefan Hildebrandt
2026-08-10 22:45:35 +02:00
parent ce3c5db315
commit ed49e63f1f
3 changed files with 96 additions and 0 deletions
+3
View File
@@ -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
@@ -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<string> {
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<void> {
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);
});
});
+23
View File
@@ -428,3 +428,26 @@ export async function enableProInterface(page: Page): Promise<void> {
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<void> {
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<Array<{ name: string; account: string }>> {
return page.locator('[data-testid="address-book-item"]').evaluateAll((els) =>
els.map((e) => ({
name: e.getAttribute('data-book-name') || '',
account: e.getAttribute('data-account') || '',
})),
);
}