test: cover announcements, out-of-office templates, the retention line and the device policies on the test servers

This commit is contained in:
Linus Rath
2026-11-01 09:14:08 +01:00
parent 9c54a4037c
commit 5d48e2a4ef
@@ -0,0 +1,499 @@
import { test, expect, type Browser, type BrowserContext, type Page } from '@playwright/test';
import { JMAP_URL } from './helpers/config';
import { accountSwitcher, addAccount, driveEntry, emailItem, folderRow, neutralizeDevOverlay, openCalendar, openFiles } from './helpers/app';
import {
ENT,
ENT_PASSWORD,
adminContext,
contextOptions,
getPolicy,
jmapAs,
putPolicy,
requireEntEnv,
shot,
userPage,
} from './helpers/ent';
/**
* Enterprise pack, package D, through the real UI on a VM 106 test Stalwart
* with the ent-* accounts:
* - item 10: the announcement shows in mail, calendar, contacts and files,
* ends at its end time without a reload, stays closed after a reload,
* shows again after the admin edits it, and only for its scope; at 390 px
* it wraps and its close button is 44 px;
* - item 11: out-of-office templates fill their placeholders and nothing is
* saved before the deputy is filled in;
* - item 12: the retention line in Trash and Junk;
* - item 13: each device policy holds in the real UI, a unified view with
* two accounts follows each message's own account, and the push preview
* route holds back sender, subject and text.
* Setup goes through the admin API and JMAP; the policy is put back at the end.
*/
test.describe.configure({ mode: 'serial' });
test.setTimeout(240_000);
const RUN = Date.now().toString(36);
const CORE = 'urn:ietf:params:jmap:core';
const MAIL = 'urn:ietf:params:jmap:mail';
const SUBMISSION = 'urn:ietf:params:jmap:submission';
const FILENODE = 'urn:ietf:params:jmap:filenode';
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64');
const ZIP = Buffer.from('UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==', 'base64');
let original: Record<string, unknown>;
const basic = (email: string) => 'Basic ' + Buffer.from(`${email}:${ENT_PASSWORD}`).toString('base64');
async function upload(account: string, bytes: Buffer, type: string): Promise<string> {
const client = await jmapAs(account);
const res = await fetch(`${JMAP_URL}/jmap/upload/${client.accountId}/`, {
method: 'POST',
headers: { Authorization: basic(account), 'Content-Type': type },
body: bytes,
});
if (!res.ok) throw new Error(`upload: ${res.status}`);
return (await res.json()).blobId as string;
}
/** Delivers a message with a picture and a ZIP attached, and waits until the recipient has it. */
async function deliverWithFiles(from: string, to: string, subject: string): Promise<void> {
const client = await jmapAs(from);
const png = await upload(from, PNG, 'image/png');
const zip = await upload(from, ZIP, 'application/zip');
const identity = (await client.identities()).find((i) => i.email === from)!;
const drafts = (await client.mailboxByRole('drafts'))!.id;
const sent = (await client.mailboxByRole('sent'))!.id;
const r = await client.request([
['Email/set', { accountId: client.accountId, create: { m: {
mailboxIds: { [drafts]: true },
keywords: { $draft: true, $seen: true },
from: [{ email: from }],
to: [{ email: to }],
subject,
bodyValues: { t: { value: `Report for ${subject}. Confidential numbers inside.` } },
textBody: [{ partId: 't', type: 'text/plain' }],
attachments: [
{ blobId: png, type: 'image/png', name: 'chart.png', disposition: 'attachment' },
{ blobId: zip, type: 'application/zip', name: 'figures.zip', disposition: 'attachment' },
],
} } }, '0'],
['EmailSubmission/set', {
accountId: client.accountId,
create: { s: { identityId: identity.id, emailId: '#m' } },
onSuccessUpdateEmail: { '#s': { [`mailboxIds/${drafts}`]: null, [`mailboxIds/${sent}`]: true, 'keywords/$draft': null } },
}, '1'],
], [CORE, MAIL, SUBMISSION]);
if (!r.methodResponses?.[1]?.[1]?.created?.s) throw new Error(`deliver: ${JSON.stringify(r.methodResponses).slice(0, 400)}`);
const recipient = await jmapAs(to);
const deadline = Date.now() + 60_000;
for (;;) {
const q = await recipient.request([['Email/query', { accountId: recipient.accountId, filter: { text: RUN }, limit: 50 }, '0'],
['Email/get', { accountId: recipient.accountId, '#ids': { resultOf: '0', name: 'Email/query', path: '/ids' }, properties: ['subject'] }, '1']]);
if ((q.methodResponses[1][1].list as Array<{ subject: string }>).some((m) => m.subject === subject)) return;
if (Date.now() > deadline) throw new Error(`${to} never got ${subject}`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
async function setPolicy(browser: Browser, patch: { organisation?: Record<string, unknown>; scopes?: unknown[]; restrictions?: Record<string, unknown>; features?: Record<string, unknown> }) {
const admin = await adminContext(browser);
await putPolicy(admin, {
...original,
organisation: { ...((original.organisation as object) ?? {}), ...(patch.organisation ?? {}) },
restrictions: { ...((original.restrictions as object) ?? {}), ...(patch.restrictions ?? {}) },
features: { ...((original.features as object) ?? {}), ...(patch.features ?? {}) },
scopes: patch.scopes ?? [],
});
await admin.close();
}
const DEVICES_OFF = { noDownloads: false, noExport: false, noPrint: false, noInstallPrompt: false, pushWithoutContent: false, watermark: false, noOffline: false };
const devices = (on: Partial<typeof DEVICES_OFF>) => ({ devices: { ...DEVICES_OFF, ...on } });
async function openMessage(page: Page, subject: string): Promise<void> {
const row = emailItem(page, subject).first();
await expect(row).toBeVisible({ timeout: 60_000 });
await row.click();
await expect(page.getByRole('heading', { name: subject }).first()).toBeVisible({ timeout: 20_000 });
}
async function openSettingsTab(page: Page, tab: string): Promise<void> {
await page.locator('a[href$="/settings"]').first().click();
await page.locator(`[data-testid="settings-tab-${tab}"]`).first().click();
}
const banner = (page: Page) => page.getByTestId('org-announcement');
test.beforeAll(async ({ browser }) => {
requireEntEnv();
const admin = await adminContext(browser);
original = await getPolicy(admin);
await putPolicy(admin, { ...original, scopes: [] });
await admin.close();
});
test.afterAll(async ({ browser }) => {
if (!original) return;
const admin = await adminContext(browser);
await putPolicy(admin, original);
await admin.close();
});
// ─── Item 10: announcement ───────────────────────────────────────────────
test('item 10: the admin writes an announcement on Organisation > Notices', async ({ browser }) => {
await setPolicy(browser, {});
const admin = await adminContext(browser);
const page = await admin.newPage();
await page.goto('/admin/org-notices');
await page.getByTestId('org-announcement-add').click();
await page.getByTestId('org-announcement-text').fill(`Mail maintenance tonight ${RUN}. Details: https://status.example.org/maintenance`);
await page.getByTestId('org-announcement-severity').selectOption('warning');
await expect(page.getByTestId('org-announcement-preview')).toContainText(`Mail maintenance tonight ${RUN}`);
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect(page.getByText('Saved.')).toBeVisible();
await shot(page, 'd-announcement-admin');
const stored = (await getPolicy(admin)).organisation as { announcement: { id: string; severity: string; text: Record<string, string> } };
expect(stored.announcement.severity).toBe('warning');
expect(stored.announcement.id).not.toBe('');
expect(stored.announcement.text.en).toContain(RUN);
await admin.close();
});
test('item 10: the announcement shows in mail, calendar, contacts and files', async ({ browser }) => {
const alice = await userPage(browser, ENT.alice);
await expect(banner(alice)).toContainText(`Mail maintenance tonight ${RUN}`, { timeout: 30_000 });
await expect(banner(alice)).toHaveAttribute('data-severity', 'warning');
await expect(banner(alice).getByRole('link', { name: 'https://status.example.org/maintenance' })).toBeVisible();
await shot(alice, 'd-announcement-mail');
await openCalendar(alice);
await expect(banner(alice)).toBeVisible();
await alice.locator('a[href$="/contacts"]').first().click();
await expect(banner(alice)).toBeVisible({ timeout: 30_000 });
await openFiles(alice);
await expect(banner(alice)).toBeVisible();
await alice.context().close();
});
test('item 10: closed stays closed after a reload, and an edit shows it again', async ({ browser }) => {
const alice = await userPage(browser, ENT.alice);
await expect(banner(alice)).toBeVisible({ timeout: 30_000 });
await alice.getByTestId('org-announcement-dismiss').click();
await expect(banner(alice)).toHaveCount(0);
await alice.reload();
await accountSwitcher(alice).waitFor({ state: 'visible', timeout: 60_000 });
await alice.waitForTimeout(3000);
await expect(banner(alice)).toHaveCount(0);
const admin = await adminContext(browser);
const policy = await getPolicy(admin);
const org = policy.organisation as { announcement: { text: Record<string, string> } };
await putPolicy(admin, { ...policy, organisation: { ...org, announcement: { ...org.announcement, text: { en: `Maintenance moved to Friday ${RUN}` } } } });
await admin.close();
await alice.reload();
await expect(banner(alice)).toContainText(`Maintenance moved to Friday ${RUN}`, { timeout: 30_000 });
await alice.context().close();
});
test('item 10: it goes away at its end time without a reload', async ({ browser }) => {
const endsAt = new Date(Date.now() + 75_000).toISOString();
await setPolicy(browser, { organisation: { announcement: { id: `end-${RUN}`, severity: 'critical', text: { en: `Outage until soon ${RUN}` }, fallbackLocale: 'en', startsAt: '', endsAt, dismissible: true } } });
const alice = await userPage(browser, ENT.alice);
await expect(banner(alice)).toContainText(`Outage until soon ${RUN}`, { timeout: 30_000 });
await expect(banner(alice)).toHaveAttribute('role', 'alert');
await expect(banner(alice)).toHaveCount(0, { timeout: 120_000 });
await alice.context().close();
});
test('item 10: a scope shows it only to its members', async ({ browser }) => {
await setPolicy(browser, {
scopes: [{ id: 'team-notice', label: 'Team', match: { groups: [ENT.team] }, policy: { organisation: { announcement: { id: `team-${RUN}`, severity: 'info', text: { en: `Team meeting at 10 ${RUN}` }, fallbackLocale: 'en', startsAt: '', endsAt: '', dismissible: true } } } }],
});
const alice = await userPage(browser, ENT.alice);
await expect(banner(alice)).toContainText(`Team meeting at 10 ${RUN}`, { timeout: 30_000 });
await alice.context().close();
const carol = await userPage(browser, ENT.carol);
await carol.waitForTimeout(4000);
await expect(banner(carol)).toHaveCount(0);
await carol.context().close();
});
test('item 10: at 390 px the text wraps and the close button is 44 px', async ({ browser }) => {
await setPolicy(browser, { organisation: { announcement: { id: `phone-${RUN}`, severity: 'warning', text: { en: `Planned maintenance on Saturday from 22:00 to 02:00. Sending and receiving may be slow. ${RUN}` }, fallbackLocale: 'en', startsAt: '', endsAt: '', dismissible: true } } });
const alice = await userPage(browser, ENT.alice, true);
await expect(banner(alice)).toBeVisible({ timeout: 30_000 });
const box = await banner(alice).boundingBox();
expect(box!.width).toBeLessThanOrEqual(390);
expect(box!.height).toBeGreaterThan(44);
const close = await alice.getByTestId('org-announcement-dismiss').boundingBox();
expect(close!.width).toBeGreaterThanOrEqual(44);
expect(close!.height).toBeGreaterThanOrEqual(44);
const overflow = await alice.evaluate(() => document.documentElement.scrollWidth > window.innerWidth);
expect(overflow).toBe(false);
await shot(alice, 'd-announcement-phone');
await alice.context().close();
});
// ─── Item 11: out-of-office templates ────────────────────────────────────
test('item 11: a template fills its placeholders, and nothing is saved before the deputy is in', async ({ browser }) => {
await setPolicy(browser, { organisation: { vacationTemplates: { library: [{
id: `holiday-${RUN}`, name: `Holiday ${RUN}`, category: 'Standard', subject: 'Out of office until {end}',
text: 'Hello,\n\nI am away until {end}.\nFor urgent matters, please write to {deputy}.\n\n{name}', html: '',
}] } } });
const client = await jmapAs(ENT.alice);
const vacation = async () => (await client.request([['VacationResponse/get', { accountId: client.accountId, ids: ['singleton'] }, '0']],
[CORE, 'urn:ietf:params:jmap:vacationresponse'])).methodResponses[0][1].list[0] as { subject: string | null; textBody: string | null; toDate: string | null; fromDate: string | null; isEnabled: boolean; htmlBody: string | null };
const before = await vacation();
const toDate = '2027-01-04T08:00:00Z';
await client.request([['VacationResponse/set', { accountId: client.accountId, update: { singleton: { isEnabled: false, fromDate: '2026-12-21T08:00:00Z', toDate, subject: 'Old', textBody: 'Old text', htmlBody: null } } }, '0']],
[CORE, 'urn:ietf:params:jmap:vacationresponse']);
try {
const alice = await userPage(browser, ENT.alice);
await openSettingsTab(alice, 'vacation');
const picker = alice.getByTestId('vacation-template-select');
await picker.scrollIntoViewIfNeeded();
await picker.selectOption({ label: `Holiday ${RUN}` });
await alice.getByTestId('vacation-template-apply').click();
await expect(alice.getByText("Enter your deputy's address first.")).toBeVisible();
await alice.waitForTimeout(2500);
expect((await vacation()).textBody, 'nothing saved before the deputy is filled in').toBe('Old text');
await alice.getByTestId('vacation-template-deputy').fill('ent-bob@example.org');
await alice.waitForTimeout(2000);
expect((await vacation()).textBody).toBe('Old text');
await alice.getByTestId('vacation-template-apply').click();
await shot(alice, 'd-vacation-template');
await expect.poll(async () => (await vacation()).textBody, { timeout: 20_000 }).toContain('please write to ent-bob@example.org');
const saved = await vacation();
// en-US, in the browser's zone (no zone set in Bulwark): 08:00 UTC is a time of day, so it is written out.
expect(saved.textBody).toMatch(/I am away until Monday, January 4, 2027 at \d{1,2}:00/);
// {name} is the identity's name.
expect(saved.textBody).toMatch(/\n\n\S.*$/);
expect(saved.textBody).not.toContain('{');
expect(saved.subject).toMatch(/^Out of office until Monday, January 4, 2027 at \d{1,2}:00/);
await alice.context().close();
} finally {
await client.request([['VacationResponse/set', { accountId: client.accountId, update: { singleton: {
isEnabled: before.isEnabled, fromDate: before.fromDate, toDate: before.toDate, subject: before.subject, textBody: before.textBody, htmlBody: before.htmlBody,
} } }, '0']], [CORE, 'urn:ietf:params:jmap:vacationresponse']);
}
});
// ─── Item 12: retention ──────────────────────────────────────────────────
test('item 12: Trash and Junk say how long they keep mail', async ({ browser }) => {
await setPolicy(browser, { organisation: { retention: { trashDays: 30, junkDays: 14 } } });
const alice = await userPage(browser, ENT.alice);
await folderRow(alice, { role: 'trash' }).first().click();
await expect(alice.getByTestId('retention-notice')).toHaveText('Messages here are deleted 30 days after they were moved here.', { timeout: 20_000 });
await shot(alice, 'd-retention-trash');
await folderRow(alice, { role: 'junk' }).first().click();
await expect(alice.getByTestId('retention-notice')).toHaveText('Messages here are deleted 14 days after they were moved here.', { timeout: 20_000 });
await folderRow(alice, { role: 'inbox' }).first().click();
await expect(alice.getByTestId('retention-notice')).toHaveCount(0);
await alice.context().close();
});
// ─── Item 13: device policies ────────────────────────────────────────────
test('item 13: the admin turns the device policies on, on Organisation > Devices', async ({ browser }) => {
await setPolicy(browser, {});
const admin = await adminContext(browser);
const page = await admin.newPage();
await page.goto('/admin/org-devices');
await expect(page.getByTestId('org-devices-warning')).toContainText("They don't stop screenshots");
for (const key of ['noDownloads', 'noExport', 'noPrint', 'watermark']) await page.getByTestId(`org-devices-${key}`).click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect(page.getByText('Saved.')).toBeVisible();
await shot(page, 'd-devices-admin');
const stored = (await getPolicy(admin)).organisation as { devices: Record<string, boolean> };
expect(stored.devices).toMatchObject({ noDownloads: true, noExport: true, noPrint: true, watermark: true, noInstallPrompt: false });
await admin.close();
});
test('item 13: no downloads, no export and no print hold in the reader, with the watermark', async ({ browser }) => {
const subject = `Device report ${RUN}`;
await setPolicy(browser, { organisation: devices({ noDownloads: true, noExport: true, noPrint: true, watermark: true }) });
await deliverWithFiles(ENT.bob, ENT.alice, subject);
const alice = await userPage(browser, ENT.alice);
await openMessage(alice, subject);
const attachments = alice.getByTestId('reader-attachments').first();
await expect(attachments).toBeVisible();
await expect(attachments.getByText('Download all')).toHaveCount(0);
await expect(attachments.getByRole('button', { name: /^Download / })).toHaveCount(0);
await expect(alice.getByTestId('attachments-downloads-off').first()).toBeVisible();
// The ZIP opens nothing; the picture previews, without Download or a new tab.
await expect(attachments.getByRole('button', { name: /figures\.zip/ })).toHaveAttribute('aria-disabled', 'true');
await attachments.getByRole('button', { name: /chart\.png/ }).first().click();
await expect(alice.getByAltText('chart.png')).toBeVisible({ timeout: 20_000 });
await expect(alice.getByRole('button', { name: 'Download', exact: true })).toHaveCount(0);
await expect(alice.getByRole('button', { name: 'Open in new tab' })).toHaveCount(0);
await expect(alice.getByTestId('watermark').last()).toContainText(ENT.alice);
await alice.keyboard.press('Escape');
// The watermark lies over the message.
await expect(alice.locator('.email-content-wrapper [data-testid="watermark"]').first()).toContainText(ENT.alice);
await shot(alice, 'd-devices-reader');
// No Print and no export in the toolbar and the menu.
await expect(alice.getByRole('button', { name: 'Print', exact: true })).toHaveCount(0);
await alice.getByRole('button', { name: 'More actions' }).first().click();
await expect(alice.getByRole('menuitem', { name: 'Export as .eml' })).toHaveCount(0);
await expect(alice.getByRole('menuitem', { name: 'View source' })).toHaveCount(0);
await expect(alice.getByRole('menuitem', { name: 'Print' })).toHaveCount(0);
await alice.keyboard.press('Escape');
// Ctrl+P prints only the note.
await alice.emulateMedia({ media: 'print' });
await expect(alice.locator('[data-print-blocked]')).toBeVisible();
await expect(alice.locator('[data-print-blocked]')).toHaveText('Printing is turned off by your organisation.');
await expect(alice.locator('nav[aria-label="Navigation"]').first()).toBeHidden();
await alice.emulateMedia({ media: 'screen' });
// No folder export from the folder menu, and Settings shows it set by the organisation.
await folderRow(alice, { role: 'inbox' }).first().click({ button: 'right' });
await expect(alice.getByTestId('mailbox-export')).toHaveCount(0);
await alice.keyboard.press('Escape');
await openSettingsTab(alice, 'backup');
await expect(alice.getByTestId('settings-mail-export')).toBeDisabled();
await alice.context().close();
});
test('item 13: Files offers no download under "No downloads"', async ({ browser }) => {
await setPolicy(browser, { organisation: devices({ noDownloads: true }) });
const client = await jmapAs(ENT.alice);
const name = `ent-d-${RUN}.zip`;
const blobId = await upload(ENT.alice, ZIP, 'application/zip');
const session = await (await fetch(`${JMAP_URL}/jmap/session`, { headers: { Authorization: basic(ENT.alice) } })).json();
const filesAccount = session.primaryAccounts?.[FILENODE] ?? client.accountId;
const typed = 'supportedNodeTypes' in (session.accounts?.[filesAccount]?.accountCapabilities?.[FILENODE] ?? {});
const created = await client.request([['FileNode/set', { accountId: filesAccount, create: { f: { name, blobId, type: 'application/zip', ...(typed ? { '@type': 'File' } : {}) } } }, '0']], [CORE, FILENODE]);
const fileId = created.methodResponses[0][1].created?.f?.id as string | undefined;
test.skip(!fileId, `this server refused the file node: ${JSON.stringify(created.methodResponses[0][1]).slice(0, 200)}`);
try {
const alice = await userPage(browser, ENT.alice);
await openFiles(alice);
const row = driveEntry(alice, name).first();
await expect(row).toBeVisible({ timeout: 30_000 });
await expect(row.getByRole('button', { name: 'Download' })).toHaveCount(0);
await row.click({ button: 'right' });
await expect(alice.getByRole('menuitem', { name: 'Info' }).or(alice.getByRole('menuitem', { name: 'Details' })).first()).toBeVisible();
await expect(alice.getByRole('menuitem', { name: 'Download', exact: true })).toHaveCount(0);
await shot(alice, 'd-devices-files');
await alice.context().close();
} finally {
if (fileId) await client.request([['FileNode/set', { accountId: filesAccount, destroy: [fileId] }, '0']], [CORE, FILENODE]);
}
});
test('item 13: with printing allowed the print popup carries the watermark', async ({ browser }) => {
const subject = `Watermark print ${RUN}`;
await setPolicy(browser, { organisation: devices({ watermark: true }) });
await deliverWithFiles(ENT.bob, ENT.alice, subject);
const alice = await userPage(browser, ENT.alice);
await alice.context().addInitScript(() => { window.print = () => {}; });
await openMessage(alice, subject);
const popupPromise = alice.context().waitForEvent('page');
// The toolbar's Print moves into More actions when the toolbar is narrow.
const toolbarPrint = alice.getByRole('button', { name: 'Print', exact: true });
if (await toolbarPrint.first().isVisible()) await toolbarPrint.first().click();
else {
await alice.getByRole('button', { name: 'More actions' }).first().click();
await alice.getByRole('menuitem', { name: 'Print' }).first().click();
}
const popup = await popupPromise;
await expect(popup.locator('[data-testid="watermark"]')).toContainText(ENT.alice, { timeout: 10_000 });
await expect(popup.locator('.subject')).toHaveText(subject);
await popup.close();
await alice.context().close();
});
test('item 13: no install card under "No install prompt"', async ({ browser }) => {
await setPolicy(browser, { organisation: devices({ noInstallPrompt: true }) });
const alice = await userPage(browser, ENT.alice);
await alice.waitForTimeout(1500);
const prevented = await alice.evaluate(() => {
const event = new Event('beforeinstallprompt', { cancelable: true });
Object.assign(event, { prompt: () => Promise.resolve(), userChoice: Promise.resolve({ outcome: 'dismissed' }) });
window.dispatchEvent(event);
return event.defaultPrevented;
});
expect(prevented).toBe(true);
await alice.waitForTimeout(1000);
await expect(alice.getByText(/^Install /)).toHaveCount(0);
await alice.context().close();
});
test('item 13: the push preview holds back sender, subject and text under the policy', async ({ browser }) => {
const subject = `Push secret ${RUN}`;
await deliverWithFiles(ENT.bob, ENT.alice, subject);
const client = await jmapAs(ENT.alice);
await setPolicy(browser, { organisation: devices({}) });
const alice = await userPage(browser, ENT.alice);
const open = await (await alice.request.get(`/api/push/preview?accountId=${client.accountId}`)).json();
expect(open.redacted).toBeUndefined();
expect(open.email?.subject, 'without the policy the preview names the newest unread message').toBeTruthy();
await setPolicy(browser, { scopes: [{ id: 'team-push', label: 'Team', match: { groups: [ENT.team] }, policy: { organisation: devices({ pushWithoutContent: true }) } }] });
const res = await alice.request.get(`/api/push/preview?accountId=${client.accountId}`);
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.redacted).toBe(true);
expect(body.email).toBeTruthy();
expect(body.email.from).toBeUndefined();
expect(body.email.subject).toBeUndefined();
expect(body.email.preview).toBeUndefined();
expect(JSON.stringify(body)).not.toContain('Confidential numbers');
await alice.context().close();
});
test('item 13: in a unified inbox with two accounts each message follows its own account', async ({ browser }) => {
const forAlice = `Unified alice ${RUN}`;
const forCarol = `Unified carol ${RUN}`;
await setPolicy(browser, {
features: { unifiedCrossAccountEnabled: true },
restrictions: { enableUnifiedMailbox: { locked: true, value: true }, unifiedCrossAccount: { locked: true, value: true } },
// ent-alice is in ent-team, ent-carol isn't.
scopes: [{ id: 'team-devices', label: 'Team', match: { groups: [ENT.team] }, policy: { organisation: devices({ noDownloads: true, watermark: true }) } }],
});
await deliverWithFiles(ENT.bob, ENT.alice, forAlice);
await deliverWithFiles(ENT.bob, ENT.carol, forCarol);
const context: BrowserContext = await browser.newContext(contextOptions());
const page = await context.newPage();
await neutralizeDevOverlay(page);
await page.goto('/');
await page.fill('#username', ENT.alice);
await page.fill('#password', ENT_PASSWORD);
await page.click('button[type="submit"]');
await accountSwitcher(page).waitFor({ state: 'visible', timeout: 60_000 });
await addAccount(page, { user: 'ent-carol', email: ENT.carol, password: ENT_PASSWORD });
await folderRow(page, { name: 'unified-inbox' }).first().click({ timeout: 60_000 });
await openMessage(page, forAlice);
const attachments = page.getByTestId('reader-attachments').first();
await expect(attachments.getByText('Download all')).toHaveCount(0);
await expect(page.locator('.email-content-wrapper [data-testid="watermark"]').first()).toContainText(ENT.alice);
await openMessage(page, forCarol);
await expect(page.getByTestId('reader-attachments').first().getByText('Download all')).toBeVisible();
await expect(page.locator('.email-content-wrapper [data-testid="watermark"]')).toHaveCount(0);
await shot(page, 'd-devices-unified');
// What isn't about one message follows the strictest account: Ctrl+P is still allowed here.
await context.close();
});
test('admin pages at 390 px', async ({ browser }) => {
await setPolicy(browser, { organisation: { vacationTemplates: { library: [{ id: `p-${RUN}`, name: 'Holiday', category: '', subject: 'Away', text: 'Back on {end}.', html: '' }] } } });
const admin = await adminContext(browser, true);
const page = await admin.newPage();
for (const [path, name] of [['/admin/org-notices', 'd-admin-notices-phone'], ['/admin/org-devices', 'd-admin-devices-phone'], ['/admin/org-templates', 'd-admin-templates-phone']] as const) {
await page.goto(path);
await page.waitForLoadState('networkidle');
await expect(page.locator('[data-testid="org-scope-bar"]')).toBeVisible({ timeout: 30_000 });
const overflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth + 1);
expect(overflow, `${path} scrolls sideways`).toBe(false);
await shot(page, name);
}
await admin.close();
});