diff --git a/.changeset/room-nav-mobile-options-sheet.md b/.changeset/room-nav-mobile-options-sheet.md new file mode 100644 index 0000000000..4d4ae16c0f --- /dev/null +++ b/.changeset/room-nav-mobile-options-sheet.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Keep the mobile room options sheet open instead of dismissing it as soon as the pointer leaves the room. diff --git a/oxlint.config.ts b/oxlint.config.ts index ec935b4da5..97f04f35b4 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -81,5 +81,11 @@ export default defineConfig({ 'typescript/no-unsafe-enum-comparison': 'off', }, }, + { + files: ['tests/e2e/**'], + rules: { + 'react/rules-of-hooks': 'off', + }, + }, ], }); diff --git a/playwright.config.ts b/playwright.config.ts index 73c623517b..f5a9587a89 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,10 +6,10 @@ export default defineConfig({ snapshotPathTemplate: 'tests/e2e/__screenshots__/{projectName}/{testFileName}/{arg}{ext}', fullyParallel: false, workers: 1, - retries: 2, + retries: process.env.CI ? 2 : 0, reporter: [['html', { open: 'never' }], ['list']], globalSetup: './tests/e2e/global-setup.ts', - timeout: 240_000, + timeout: 60_000, expect: { toHaveScreenshot: { maxDiffPixelRatio: 0.01 }, }, @@ -29,10 +29,10 @@ export default defineConfig({ }, ], webServer: { - command: 'pnpm dev', + command: 'pnpm run build && pnpm exec vite preview --port 8080 --strictPort', url: 'http://localhost:8080', - reuseExistingServer: !process.env.CI, - timeout: 240_000, + reuseExistingServer: false, + timeout: 180_000, env: { NODE_OPTIONS: '--max-old-space-size=8192' }, }, }); diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index 99c14b70c3..d2c255d749 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -439,7 +439,7 @@ export function RoomNavItem({ navigate(linkPath); }; - const optionsVisible = hover || !!menuAnchor; + const optionsVisible = hover || !!menuAnchor || isMobileMenuOpen; const isMutedRoom = notificationMode === RoomNotificationMode.Mute; const shouldShowUnreadIndicator = !isMutedRoom && (!!unread || hasRoomUnread); diff --git a/tests/e2e/fixtures/test.ts b/tests/e2e/fixtures/test.ts new file mode 100644 index 0000000000..fc016d1204 --- /dev/null +++ b/tests/e2e/fixtures/test.ts @@ -0,0 +1,16 @@ +import { test as base } from '@playwright/test'; +import { AppShell } from '../pages/AppShell'; + +type Fixtures = { + app: AppShell; +}; + +export const test = base.extend({ + app: async ({ page }, use) => { + const app = new AppShell(page); + await app.open(); + await use(app); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/tests/e2e/leave-room-prompt.spec.ts b/tests/e2e/leave-room-prompt.spec.ts new file mode 100644 index 0000000000..85a771e0ee --- /dev/null +++ b/tests/e2e/leave-room-prompt.spec.ts @@ -0,0 +1,8 @@ +import { test, expect } from './fixtures/test'; + +test('leave room prompt opens from the room options menu', async ({ app }) => { + const menu = await app.openRoomOptions('General'); + await menu.leaveRoom(); + + await expect(app.leaveRoomPrompt).toBeVisible(); +}); diff --git a/tests/e2e/pages/AppShell.ts b/tests/e2e/pages/AppShell.ts new file mode 100644 index 0000000000..8626b37943 --- /dev/null +++ b/tests/e2e/pages/AppShell.ts @@ -0,0 +1,46 @@ +import type { Locator, Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +export const CLIENT_READY_TIMEOUT = 30_000; + +export class AppShell { + readonly leaveRoomPrompt: Locator; + + readonly createRoomButton: Locator; + + constructor(readonly page: Page) { + this.leaveRoomPrompt = page.getByText('Are you sure you want to leave this room?'); + this.createRoomButton = page.getByRole('button', { name: 'Create Room' }).first(); + } + + async open(): Promise { + await this.page.goto('/'); + await expect(this.room('General')).toBeVisible({ timeout: CLIENT_READY_TIMEOUT }); + await this.dismissDeviceBanner(); + } + + room(name: string): Locator { + return this.page.getByText(name).first(); + } + + async dismissDeviceBanner(): Promise { + await this.page + .getByRole('button', { name: 'Dismiss' }) + .click({ timeout: 5_000 }) + .catch(() => undefined); + } + + async openRoomOptions(name: string): Promise { + await this.room(name).hover(); + await this.page.getByRole('button', { name: 'More Options' }).first().click(); + return new RoomOptionsMenu(this.page); + } +} + +export class RoomOptionsMenu { + constructor(readonly page: Page) {} + + async leaveRoom(): Promise { + await this.page.getByRole('button', { name: 'Leave Room' }).click(); + } +} diff --git a/tests/e2e/shell.spec.ts b/tests/e2e/shell.spec.ts index 938a551b1d..fd4c697bac 100644 --- a/tests/e2e/shell.spec.ts +++ b/tests/e2e/shell.spec.ts @@ -1,41 +1,28 @@ -import { test, expect, type Page } from '@playwright/test'; +import type { Page } from '@playwright/test'; +import { test, expect } from './fixtures/test'; const containerised = Boolean(process.env.PW_TEST_CONNECT_WS_ENDPOINT); -async function dismissDeviceBanner(page: Page): Promise { - await page - .getByRole('button', { name: 'Dismiss' }) - .click({ timeout: 5_000 }) - .catch(() => undefined); -} - const maskDynamic = (page: Page) => ({ mask: [page.locator('time'), page.getByText(/^Created by /)], }); test.describe('app shell', () => { - test('shows seeded rooms after login', async ({ page }) => { - await page.goto('/'); - - await expect(page.getByText('General').first()).toBeVisible({ timeout: 180_000 }); - await expect(page.getByText('Random').first()).toBeVisible(); + test('shows seeded rooms after login', async ({ app }) => { + await expect(app.room('General')).toBeVisible(); + await expect(app.room('Random')).toBeVisible(); }); - test('opens a room and renders its timeline', async ({ page }) => { - await page.goto('/'); - await dismissDeviceBanner(page); + test('opens a room and renders its timeline', async ({ app, page }) => { + await app.room('General').click(); - await page.getByText('General').first().click(); - await expect(page.getByText('Welcome to the test room.')).toBeVisible({ timeout: 180_000 }); + await expect(page.getByText('Welcome to the test room.')).toBeVisible(); await expect(page.getByText('Layout baseline seed message.')).toBeVisible(); }); - test('matches the shell layout baseline', async ({ page }) => { + test('matches the shell layout baseline', async ({ app, page }) => { test.skip(!containerised, 'run via pnpm test:e2e:docker'); - - await page.goto('/'); - await expect(page.getByText('General').first()).toBeVisible({ timeout: 180_000 }); - await dismissDeviceBanner(page); + await expect(app.room('General')).toBeVisible(); await page.evaluate(async () => { await document.fonts.ready;