diff options
| author | Oskar <oskar@mullvad.net> | 2025-09-26 17:37:38 +0200 |
|---|---|---|
| committer | Oskar <oskar@mullvad.net> | 2025-09-30 09:37:18 +0200 |
| commit | b36e8ae73ec5e58e40f73002f4f3bd8f0892d21f (patch) | |
| tree | e7c0eddb5594b9d47c10dc07dc0db5eada2698fd | |
| parent | fb1271119cc886afbcdc7c4c0657ed1fd3445185 (diff) | |
| download | mullvadvpn-b36e8ae73ec5e58e40f73002f4f3bd8f0892d21f.tar.xz mullvadvpn-b36e8ae73ec5e58e40f73002f4f3bd8f0892d21f.zip | |
Improve navigation utils
4 files changed, 31 insertions, 74 deletions
diff --git a/desktop/packages/mullvad-vpn/test/e2e/installed/state-dependent/api-access-methods.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/installed/state-dependent/api-access-methods.spec.ts index c9d38988d8..c6719cce47 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/installed/state-dependent/api-access-methods.spec.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/installed/state-dependent/api-access-methods.spec.ts @@ -59,7 +59,7 @@ test('App should display access methods', async () => { test('App should add invalid access method', async () => { await page.locator('button:has-text("Add")').click(); - await util.waitForNextRoute(); + await util.waitForRoute(RoutePath.editApiAccessMethods); const title = page.locator('h1'); await expect(title).toHaveText('Add method'); @@ -113,7 +113,7 @@ test('App should edit access method', async () => { const customMethod = page.getByTestId('access-method').last(); await customMethod.locator('button').last().click(); await customMethod.getByText('Edit').click(); - await util.waitForNextRoute(); + await util.waitForRoute(RoutePath.editApiAccessMethods); const title = page.locator('h1'); await expect(title).toHaveText('Edit method'); diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts index 98b43bcf7f..7a3bce2af6 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts @@ -141,6 +141,8 @@ const featureIndicatorWithOption: FeatureIndicatorWithOptionTestOption[] = [ }, ]; +test.describe.configure({ mode: 'parallel' }); + test.describe('Feature indicators', () => { test.beforeAll(async () => { ({ page, util } = await startMockedApp()); @@ -246,8 +248,7 @@ test.describe('Feature indicators', () => { await helpers.connectWithFeatures([featureIndicator]); await clickFeatureIndicator(featureIndicatorLabel, route); - const currentRoute = await util.currentRoute(); - expect(currentRoute).toBe(route); + await util.waitForRoute(route); }); }, ); @@ -271,8 +272,7 @@ test.describe('Feature indicators', () => { } await expect(element).toBeInViewport(); - const currentRoute = await util.currentRoute(); - expect(currentRoute).toBe(route); + await util.waitForRoute(route); }); }, ); diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/navigation/navigation-object-model.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/navigation/navigation-object-model.ts index 3234905965..751551d22d 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/navigation/navigation-object-model.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/navigation/navigation-object-model.ts @@ -14,8 +14,7 @@ export class NavigationObjectModel { } async goBack() { - await this.navigationSelectors.backButton().click(); - await this.utils.waitForNextRoute(); + await this.utils.waitForRouteChange(() => this.navigationSelectors.backButton().click()); } async gotoRoot() { diff --git a/desktop/packages/mullvad-vpn/test/e2e/utils.ts b/desktop/packages/mullvad-vpn/test/e2e/utils.ts index 3d38577c41..b7065f6c62 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/utils.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/utils.ts @@ -1,22 +1,21 @@ +import { expect } from '@playwright/test'; import fs from 'fs'; import { _electron as electron, ElectronApplication, Locator, Page } from 'playwright'; +import { RoutePath } from '../../src/shared/routes'; + export interface StartAppResponse { app: ElectronApplication; page: Page; util: TestUtils; } +type TriggerFn = () => Promise<void> | void; + export interface TestUtils { currentRoute: () => Promise<string | null>; - waitForNavigation: (initiateNavigation?: () => Promise<void> | void) => Promise<string>; - waitForRoute: (route: string) => Promise<void>; - waitForNextRoute: () => Promise<string>; -} - -interface History { - entries: Array<{ pathname: string }>; - index: number; + waitForRoute: (route: RoutePath) => Promise<void>; + waitForRouteChange: (trigger: TriggerFn) => Promise<void>; } type LaunchOptions = NonNullable<Parameters<typeof electron.launch>[0]>; @@ -24,15 +23,15 @@ type LaunchOptions = NonNullable<Parameters<typeof electron.launch>[0]>; export const startApp = async (options: LaunchOptions): Promise<StartAppResponse> => { const app = await launch(options); const page = await app.firstWindow(); + await page.waitForEvent('load'); page.on('pageerror', (error) => console.log(error)); page.on('console', (msg) => console.log(msg.text())); const util: TestUtils = { - currentRoute: currentRouteFactory(app), - waitForNavigation: waitForNavigationFactory(app), - waitForRoute: waitForRouteFactory(app), - waitForNextRoute: waitForNextRouteFactory(app), + currentRoute: () => currentRoute(page), + waitForRoute: (route: RoutePath) => waitForRoute(page, route), + waitForRouteChange: (trigger: TriggerFn) => waitForRouteChange(page, trigger), }; return { app, page, util }; @@ -43,62 +42,21 @@ export const launch = (options: LaunchOptions): Promise<ElectronApplication> => return electron.launch(options); }; -const currentRouteFactory = (app: ElectronApplication) => { - return () => { - return app.evaluate<string | null>(({ webContents }) => { - const electronWebContent = webContents - .getAllWebContents() - // Select window that isn't devtools - .find((webContents) => webContents.getURL().startsWith('file://')); - - if (electronWebContent) { - return electronWebContent.executeJavaScript('window.e2e.location'); - } - - return null; - }); - }; -}; - -const waitForNavigationFactory = (app: ElectronApplication) => { - const waitForNextRoute = waitForNextRouteFactory(app); - // Wait for navigation animation to finish. A function can be provided that initiates the - // navigation, e.g. clicks a button. - return async (initiateNavigation?: () => Promise<void> | void) => { - // Wait for route to change after optionally initiating the navigation. - const [route] = await Promise.all([waitForNextRoute(), initiateNavigation?.()]); - - return route; - }; -}; - -// This factory returns a function which returns a boolean when the route passed to it matches that of the application. -const waitForRouteFactory = (app: ElectronApplication) => { - const getCurrentRoute = currentRouteFactory(app); - - const waitForRoute = async (route: string) => { - const currentRoute = await getCurrentRoute(); - - if (currentRoute !== route) { - return waitForRoute(route); - } - }; +function currentRoute(page: Page): Promise<string | null> { + return page.evaluate('window.e2e.location'); +} - return waitForRoute; -}; +// Returns a promise which resolves when the provided route is reached. +async function waitForRoute(page: Page, expectedRoute: RoutePath): Promise<void> { + await expect.poll(async () => currentRoute(page)).toBe(expectedRoute); +} -// Returns the route when it changes -const waitForNextRouteFactory = (app: ElectronApplication) => { - return async () => - app.evaluate<string>( - ({ ipcMain }) => - new Promise((resolve) => { - ipcMain.once('navigation-setHistory', (_event, history: History) => { - resolve(history.entries[history.index].pathname); - }); - }), - ); -}; +// Returns a promise which resolves when the route changes. +async function waitForRouteChange(page: Page, trigger: TriggerFn) { + const initialRoute = await currentRoute(page); + await trigger(); + await expect.poll(async () => currentRoute(page)).not.toBe(initialRoute); +} const getStyleProperty = (locator: Locator, property: string) => { return locator.evaluate( |
