fix: keep the message list within the screen on phones

This commit is contained in:
Linus Rath
2026-10-22 23:08:39 +02:00
parent 310f910a6a
commit 7049844cf3
4 changed files with 74 additions and 5 deletions
+1 -1
View File
@@ -4655,7 +4655,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
{/* Email List - full width on mobile, fixed width/height on tablet/desktop */}
<div
className={cn(
"relative flex flex-col bg-background",
"relative flex flex-col bg-background min-w-0",
isHorizontalMailLayout
? (shouldHideHorizontalViewerPane ? "md:w-full md:min-h-0" : "md:w-full md:h-auto")
: "h-full border-e border-border",
+1
View File
@@ -158,6 +158,7 @@ because the UI behaviour is currently incomplete. Worth a look:
| `IT_WEBMAIL_URL`| `http://localhost:3000` | Webmail origin |
| `IT_JMAP_URL` | `http://localhost:8025` | Stalwart JMAP/admin base URL |
| `IT_SMTP_PORT` | `1025` | Stalwart submission port |
| `IT_ACCOUNT_USERS` | `alice,bob,carol` | Local parts of the three test accounts, for a stack with other users |
| `IT_VIDEO` | `retain-on-failure`| Video capture: `on` records a `.webm` for **every** test; also `off` / `on-first-retry`. Videos land at `integration/test-results/<test>/video.webm` |
Record videos for a whole run (passing tests included):
+63
View File
@@ -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<void> {
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);
}
});
});
+9 -4
View File
@@ -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;