-
Notifications
You must be signed in to change notification settings - Fork 972
e2e(helpers): Playwright helpers for server views and desktop #3887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3cd4202
57c3771
7374cae
d2cbfad
e5244b8
73496d6
edd38a1
2a229dc
c5ee149
fa76ac9
faf8abd
19ee0c6
6e371fd
be44a81
aa6d814
1cc8b9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. | ||
| // See LICENSE.txt for license information. | ||
|
|
||
| import type {ElectronApplication} from 'playwright'; | ||
|
|
||
| /** Shape returned by Electron `app.getAppMetrics()` (subset used by tests). */ | ||
| export type AppProcessMetric = { | ||
| pid: number; | ||
| type: string; | ||
| name?: string; | ||
| }; | ||
|
|
||
| export type AppProcessMetricsSummary = { | ||
| metrics: AppProcessMetric[]; | ||
| totalCount: number; | ||
| tabCount: number; | ||
| nonTabCount: number; | ||
| types: string[]; | ||
| pids: number[]; | ||
| }; | ||
|
|
||
| /** | ||
| * Upper bounds for non-Tab processes under the sandboxed E2E launch flags in | ||
| * `e2e/fixtures/index.ts` (`--disable-gpu`, `--no-zygote`, etc.). These differ | ||
| * from a normal Task Manager baseline on an end-user install. | ||
| */ | ||
| const NON_TAB_PROCESS_MAX: Partial<Record<NodeJS.Platform, number>> = { | ||
| darwin: 10, | ||
| linux: 10, | ||
| win32: 12, | ||
| }; | ||
|
|
||
| const DEFAULT_NON_TAB_PROCESS_MAX = 12; | ||
|
|
||
| /** Tab/renderer processes for demoConfig (2 servers + main chrome). */ | ||
| const TAB_PROCESS_MAX = 30; | ||
|
|
||
| export async function getAppProcessMetrics(app: ElectronApplication): Promise<AppProcessMetric[]> { | ||
| return app.evaluate(({app: electronApp}) => { | ||
| return electronApp.getAppMetrics().map((metric) => ({ | ||
| pid: metric.pid, | ||
| type: metric.type, | ||
| name: metric.name, | ||
| })); | ||
| }); | ||
| } | ||
|
|
||
| export function summarizeAppProcessMetrics(metrics: AppProcessMetric[]): AppProcessMetricsSummary { | ||
| const tabMetrics = metrics.filter((metric) => metric.type === 'Tab'); | ||
| const nonTabMetrics = metrics.filter((metric) => metric.type !== 'Tab'); | ||
|
|
||
| return { | ||
| metrics, | ||
| totalCount: metrics.length, | ||
| tabCount: tabMetrics.length, | ||
| nonTabCount: nonTabMetrics.length, | ||
| types: [...new Set(metrics.map((metric) => metric.type))].sort(), | ||
| pids: metrics.map((metric) => metric.pid), | ||
| }; | ||
| } | ||
|
|
||
| export function getNonTabProcessMax(): number { | ||
| return NON_TAB_PROCESS_MAX[process.platform] ?? DEFAULT_NON_TAB_PROCESS_MAX; | ||
| } | ||
|
|
||
| export function getTabProcessMax(): number { | ||
| return TAB_PROCESS_MAX; | ||
| } | ||
|
|
||
| export async function summarizeProcessMetrics(app: ElectronApplication): Promise<AppProcessMetricsSummary> { | ||
| const metrics = await getAppProcessMetrics(app); | ||
| return summarizeAppProcessMetrics(metrics); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. | ||
| // See LICENSE.txt for license information. | ||
|
|
||
| import {findMainWindow} from './appReadiness'; | ||
| import {closeDownloadsDropdownIfOpen} from './downloadsDropdown'; | ||
| import {closeOverlayWindowsIfOpen} from './overlayWindows'; | ||
| import {activateServerView} from './serverContext'; | ||
| import type {ServerView} from './serverView'; | ||
|
|
||
| /** Close desktop overlays that steal focus from server views (dropdowns, modals). */ | ||
| export async function dismissBlockingOverlays(win: ServerView): Promise<void> { | ||
| await closeDownloadsDropdownIfOpen(win.app); | ||
| await closeOverlayWindowsIfOpen(win.app); | ||
| await activateServerView(win.app, win.webContentsId); | ||
| const mainWindow = findMainWindow(win.app); | ||
| await mainWindow?.keyboard.press('Escape').catch(() => undefined); | ||
| await win.keyboard.press('Escape').catch(() => undefined); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,10 +28,12 @@ export const COPY_LINK_SELECTORS = [ | |
| * The webapp migrated from #channelHeaderDropdownButton to Menu.Button | ||
| * with an aria-label like "off-topic channel menu". | ||
| */ | ||
| export async function openChannelHeaderMenu(win: ServerView): Promise<void> { | ||
| await win.waitForSelector(CHANNEL_HEADER_MENU_TRIGGER, {state: 'visible', timeout: 15_000}); | ||
| export async function openChannelHeaderMenu(win: ServerView, timeout = 20_000): Promise<void> { | ||
| const menuTimeout = Math.min(Math.max(Math.floor(timeout * 0.25), 500), 5_000); | ||
| const triggerTimeout = Math.max(timeout - menuTimeout, 500); | ||
|
Comment on lines
+32
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why these timeouts has to be computed?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @saturninoabril this was mostly an AI suggestion while I was fixing repeated CI flakiness around the channel header menu, especially in enableBookmarksBar. The idea is timeout should mean total wait for the helper, not per step. Earlier we had fixed 15s for the trigger + 5s for the menu. When enableBookmarksBar retries and passes remaining, each attempt could still burn the full 15s + 5s. So even with a 15s outer deadline, one retry could overshoot, eat into the test timeout, and fail in a confusing way. With the split, if you pass 20s, trigger gets ~15s and menu gets ~5s — together ~20s, not 40s. That way we stay within the budget and it’s clearer whether it failed waiting for the trigger or for the menu to open. |
||
| await win.waitForSelector(CHANNEL_HEADER_MENU_TRIGGER, {state: 'visible', timeout: triggerTimeout}); | ||
| await win.click(CHANNEL_HEADER_MENU_TRIGGER); | ||
| await win.waitForSelector('#channelHeaderDropdownMenu, .a11y__popup', {timeout: 5_000}); | ||
| await win.waitForSelector('#channelHeaderDropdownMenu, .a11y__popup', {timeout: menuTimeout}); | ||
| } | ||
|
|
||
| const SIDEBAR_CHANNEL_MENU_BUTTON = (channelItemSelector: string) => [ | ||
|
|
@@ -117,34 +119,59 @@ export async function clickCopyLinkInMenu(win: ServerView): Promise<void> { | |
| * helper only toggles the preference — callers wait for bookmark items later. | ||
| */ | ||
| export async function enableBookmarksBar(win: ServerView): Promise<void> { | ||
| const alreadyVisible = await win.runInRenderer(` | ||
| const isBookmarksBarVisible = async (): Promise<boolean> => win.runInRenderer(` | ||
| const container = document.querySelector('[data-testid="channel-bookmarks-container"]'); | ||
| if (!container) { | ||
| return false; | ||
| } | ||
| const rect = container.getBoundingClientRect(); | ||
| return rect.width > 0 && rect.height > 0; | ||
| `); | ||
| if (alreadyVisible) { | ||
|
|
||
| if (await isBookmarksBarVisible()) { | ||
| return; | ||
| } | ||
|
|
||
| await openChannelHeaderMenu(win); | ||
| const toggled = await win.runInRenderer(` | ||
| const items = Array.from(document.querySelectorAll( | ||
| '[role="menuitem"], .MenuItem, [id^="channel-menu-"]', | ||
| )); | ||
| const barItem = items.find((item) => /bookmarks bar/i.test((item.textContent || '').trim())); | ||
| if (!barItem) { | ||
| return false; | ||
| const deadline = Date.now() + 15_000; | ||
| while (Date.now() < deadline) { | ||
| const remaining = Math.max(deadline - Date.now(), 1_000); | ||
| await openChannelHeaderMenu(win, remaining); | ||
|
|
||
| const toggleResult = await win.runInRenderer(` | ||
| const submenuTrigger = document.querySelector('[id^="channel-menu-"][id$="-bookmarks"]'); | ||
| if (submenuTrigger instanceof HTMLElement) { | ||
| submenuTrigger.dispatchEvent(new MouseEvent('mouseenter', {bubbles: true})); | ||
| submenuTrigger.dispatchEvent(new MouseEvent('mouseover', {bubbles: true})); | ||
| } | ||
|
|
||
| const items = Array.from(document.querySelectorAll( | ||
| '[role="menuitem"], .MenuItem, [id^="channel-menu-"]', | ||
| )); | ||
| const barItem = items.find((item) => { | ||
| const text = (item.textContent || '').trim(); | ||
| return /bookmarks bar/i.test(text) && !/add a link|add bookmark|attach file/i.test(text); | ||
| }); | ||
| if (!barItem) { | ||
| // Modern webapp: bookmarks live under the Bookmarks submenu and the bar | ||
| // autoshows once a bookmark exists — no separate Show/Hide toggle. | ||
| return submenuTrigger ? 'submenu-only' : 'missing'; | ||
| } | ||
| const label = (barItem.textContent || '').trim().toLowerCase(); | ||
| const checked = barItem.getAttribute('aria-checked'); | ||
| if (label.includes('hide') || checked === 'true') { | ||
| return 'enabled'; | ||
| } | ||
| barItem.click(); | ||
| return 'clicked'; | ||
| `, true); | ||
| await win.keyboard.press('Escape').catch(() => undefined); | ||
| if (toggleResult === 'enabled' || toggleResult === 'clicked' || toggleResult === 'submenu-only') { | ||
| return; | ||
| } | ||
| barItem.click(); | ||
| return true; | ||
| `, true); | ||
| if (!toggled) { | ||
| throw new Error('Bookmarks Bar menu item not found in channel header menu'); | ||
| await new Promise((resolve) => setTimeout(resolve, 300)); | ||
| } | ||
| await win.keyboard.press('Escape'); | ||
|
|
||
| throw new Error('Bookmarks Bar menu item not found in channel header menu'); | ||
| } | ||
|
|
||
| export const TEAM_SIDEBAR_BUTTON = [ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.