From 7049844cf36f077d7a3c22edee5d58b69a97e8d4 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:08:39 +0200 Subject: [PATCH] fix: keep the message list within the screen on phones --- components/mail/mail-app.tsx | 2 +- integration/README.md | 1 + integration/tests/16-phone-layout.spec.ts | 63 +++++++++++++++++++++++ integration/tests/helpers/config.ts | 13 +++-- 4 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 integration/tests/16-phone-layout.spec.ts 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;