diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx
index 3de801e95..c2fd793f1 100644
--- a/components/mail/mail-app.tsx
+++ b/components/mail/mail-app.tsx
@@ -4655,7 +4655,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
{/* Email List - full width on mobile, fixed width/height on tablet/desktop */}
/video.webm` |
Record videos for a whole run (passing tests included):
diff --git a/integration/tests/16-phone-layout.spec.ts b/integration/tests/16-phone-layout.spec.ts
new file mode 100644
index 000000000..46b8bd4de
--- /dev/null
+++ b/integration/tests/16-phone-layout.spec.ts
@@ -0,0 +1,63 @@
+import { test, expect, devices } from '@playwright/test';
+import { ACCOUNTS } from './helpers/config';
+import { sendMail } from './helpers/smtp';
+import { JmapClient } from './helpers/jmap';
+import { login, emailItem, expectEmailVisible } from './helpers/app';
+
+/**
+ * On a phone the message list is the whole screen. The selection toolbar
+ * above it stays in the layout while collapsed, and its row of nowrap
+ * buttons is wider than a phone. The list pane used to grow to that width
+ * (a flex item with min-width:auto), which pushed the header's buttons,
+ * every row's date and the subject's ellipsis off the right edge of the
+ * screen, with nothing selected. The toolbar then measured itself as wide
+ * and kept its labels too.
+ */
+const alice = ACCOUNTS.alice;
+const subject = `IT phone-layout ${Date.now()}`;
+
+test.use({ ...devices['Pixel 7'] });
+
+/** Fails when `locator` has no box or any part of it is off the screen's sides. */
+async function expectOnScreen(locator: import('@playwright/test').Locator, width: number): Promise {
+ const box = await locator.boundingBox();
+ expect(box).not.toBeNull();
+ expect(box!.x).toBeGreaterThanOrEqual(0);
+ expect(box!.x + box!.width).toBeLessThanOrEqual(width + 0.5);
+}
+
+test.describe('phone layout', () => {
+ test.beforeEach(async () => {
+ const jmap = await JmapClient.connect(alice.email, alice.password);
+ await jmap.reset();
+ await sendMail({ from: alice.email, authPass: alice.password, to: alice.email, subject, body: 'x' });
+ await jmap.waitForEmail(subject);
+ });
+
+ test('the message list is never wider than the screen', async ({ page }) => {
+ await login(page, alice);
+ await expectEmailVisible(page, subject);
+ const { width } = page.viewportSize()!;
+
+ expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
+ await expectOnScreen(page.locator('[data-mobile-mail-header]'), width);
+ // The account button is the header's last control.
+ await expectOnScreen(page.locator('[data-testid="account-switcher"]').first(), width);
+ await expectOnScreen(emailItem(page, subject).first(), width);
+ });
+
+ test('the selection toolbar compacts so every button is on screen', async ({ page }) => {
+ await login(page, alice);
+ await expectEmailVisible(page, subject);
+ const { width } = page.viewportSize()!;
+
+ await emailItem(page, subject).first().getByRole('checkbox').click();
+ // The toolbar only drops its labels when it measures narrower than a desktop list.
+ const toolbar = page.locator('[data-batch-narrow]');
+ await expect(toolbar).toBeVisible();
+ await expectOnScreen(toolbar, width);
+ for (const button of await toolbar.getByRole('button').all()) {
+ await expectOnScreen(button, width);
+ }
+ });
+});
diff --git a/integration/tests/helpers/config.ts b/integration/tests/helpers/config.ts
index b97998b41..90eec36fb 100644
--- a/integration/tests/helpers/config.ts
+++ b/integration/tests/helpers/config.ts
@@ -35,11 +35,16 @@ function acct(user: string): TestAccount {
return { user, email: `${user}@${DOMAIN}`, password: ACCOUNT_PASSWORD };
}
-/** The mailboxes provisioned by the Stalwart bootstrap plan. */
+/**
+ * The mailboxes provisioned by the Stalwart bootstrap plan. IT_ACCOUNT_USERS
+ * renames the three local parts (in this order) for a stack with other users,
+ * such as the shared test servers.
+ */
+const USERS = (process.env.IT_ACCOUNT_USERS ?? 'alice,bob,carol').split(',');
export const ACCOUNTS = {
- alice: acct('alice'),
- bob: acct('bob'),
- carol: acct('carol'),
+ alice: acct(USERS[0]),
+ bob: acct(USERS[1]),
+ carol: acct(USERS[2]),
} as const;
export type AccountKey = keyof typeof ACCOUNTS;