diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2022-12-15 16:34:38 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-12-15 16:34:38 +0100 |
| commit | d719c03f546d64284a8a4898aa04500d5677abd7 (patch) | |
| tree | dd346890d9bc75d66775868c6250e32b2d7f6f38 | |
| parent | 0982a4a55e692797cee18f82cd1ffe753e02da33 (diff) | |
| parent | c9beb24e50a52b5941a731b6fb456e41ebcdc55e (diff) | |
| download | mullvadvpn-d719c03f546d64284a8a4898aa04500d5677abd7.tar.xz mullvadvpn-d719c03f546d64284a8a4898aa04500d5677abd7.zip | |
Merge branch 'add-more-e2e-tests'
| -rw-r--r-- | gui/README.md | 4 | ||||
| -rw-r--r-- | gui/package.json | 4 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 9 | ||||
| -rw-r--r-- | gui/test/e2e/installed/installed-utils.ts | 4 | ||||
| -rw-r--r-- | gui/test/e2e/installed/requires-input/login.spec.ts | 32 | ||||
| -rw-r--r-- | gui/test/e2e/installed/state-dependent/disconnected.spec.ts | 2 | ||||
| -rw-r--r-- | gui/test/e2e/installed/state-dependent/location.spec.ts | 10 | ||||
| -rw-r--r-- | gui/test/e2e/mocked/expired-account-error-view.spec.ts | 8 | ||||
| -rw-r--r-- | gui/test/e2e/mocked/mocked-utils.ts | 17 | ||||
| -rw-r--r-- | gui/test/e2e/mocked/settings.spec.ts | 12 | ||||
| -rw-r--r-- | gui/test/e2e/mocked/tunnel-state.spec.ts | 27 | ||||
| -rw-r--r-- | gui/test/e2e/utils.ts | 47 | ||||
| -rw-r--r-- | gui/types/global/index.d.ts | 1 |
13 files changed, 131 insertions, 46 deletions
diff --git a/gui/README.md b/gui/README.md index 068b5dd70e..f26495426a 100644 --- a/gui/README.md +++ b/gui/README.md @@ -25,11 +25,11 @@ The app has unit tests and integration tests located in test/: The tests in **test/e2e/installed** are run against the already installed app using the currently running daemon. It's possible to run these tests on any machine with the app installed by running ``` -npm run e2e:installed +npm run e2e:sequential installed/<test> ``` or without building by running ``` -npm run e2e:installed:no-build +npm run e2e:sequential:no-build installed/<test> ``` It is also possible to build these tests along with all its dependencies into an executable that can diff --git a/gui/package.json b/gui/package.json index 2c7f860987..5153c85d45 100644 --- a/gui/package.json +++ b/gui/package.json @@ -97,8 +97,8 @@ "tsc": "tsc -p . --noEmit", "e2e": "npm run build && npm run e2e:no-build", "e2e:no-build": "xvfb-maybe -- playwright test mocked", - "e2e:installed": "npm run build && npm run e2e:installed:no-build", - "e2e:installed:no-build": "xvfb-maybe -- playwright test --workers 1", + "e2e:sequential": "npm run build && npm run e2e:sequential:no-build", + "e2e:sequential:no-build": "xvfb-maybe -- playwright test --workers 1", "e2e:update-snapshots": "npm run e2e:no-build -- --update-snapshots", "develop": "gulp develop", "test": "cross-env NODE_ENV=test electron-mocha --renderer --reporter spec --require ts-node/register --require \"test/unit/setup/renderer.ts\" \"test/unit/**/*.{ts,tsx}\"", diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index e78462041b..afdda1d401 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -253,6 +253,11 @@ export default class AppRenderer { const navigationBase = this.getNavigationBase(); this.history = new History(navigationBase); } + + if (window.env.e2e) { + // Make the current location available to the tests if running e2e tests + window.e2e = { location: this.history.location.pathname }; + } } public renderView() { @@ -507,6 +512,10 @@ export default class AppRenderer { public setNavigationHistory(history: IHistoryObject) { IpcRendererEventChannel.navigation.setHistory(history); + + if (window.env.e2e) { + window.e2e.location = history.entries[history.index].pathname; + } } private isLoggedIn(): boolean { diff --git a/gui/test/e2e/installed/installed-utils.ts b/gui/test/e2e/installed/installed-utils.ts index bfc188db3a..1a8cd4258e 100644 --- a/gui/test/e2e/installed/installed-utils.ts +++ b/gui/test/e2e/installed/installed-utils.ts @@ -1,6 +1,6 @@ -import { startApp, StartAppResponse } from '../utils'; +import { startApp } from '../utils'; -export const startInstalledApp = async (): Promise<StartAppResponse> => { +export const startInstalledApp = async (): ReturnType<typeof startApp> => { return startApp({ executablePath: getAppInstallPath() }); } diff --git a/gui/test/e2e/installed/requires-input/login.spec.ts b/gui/test/e2e/installed/requires-input/login.spec.ts new file mode 100644 index 0000000000..44ea19a970 --- /dev/null +++ b/gui/test/e2e/installed/requires-input/login.spec.ts @@ -0,0 +1,32 @@ +import { expect, test } from '@playwright/test'; +import { Page } from 'playwright'; +import { RoutePath } from '../../../../src/renderer/lib/routes'; +import { TestUtils } from '../../utils'; +import { assertDisconnected } from '../../shared/tunnel-state'; + +import { startInstalledApp } from '../installed-utils'; + +// This test expects the daemon to be logged out and then log in. + +let page: Page; +let util: TestUtils; + +test.beforeAll(async () => { + ({ page, util } = await startInstalledApp()); +}); + +test.afterAll(async () => { + await page.close(); +}); + +// Disables timeout since it's handled by the rust test +test.setTimeout(0); + +test('App should go from login view to main view when daemon logs in', async () => { + expect(await util.currentRoute()).toEqual(RoutePath.login); + + // Waiting for the daemon to log in + expect(await util.nextRoute()).toEqual(RoutePath.main); + + await assertDisconnected(page); +}); diff --git a/gui/test/e2e/installed/state-dependent/disconnected.spec.ts b/gui/test/e2e/installed/state-dependent/disconnected.spec.ts index 7fa853099e..a26b61279c 100644 --- a/gui/test/e2e/installed/state-dependent/disconnected.spec.ts +++ b/gui/test/e2e/installed/state-dependent/disconnected.spec.ts @@ -18,5 +18,5 @@ test.afterAll(async () => { }); test('App should show disconnected tunnel state', async () => { - await assertDisconnected(page) + await assertDisconnected(page); }); diff --git a/gui/test/e2e/installed/state-dependent/location.spec.ts b/gui/test/e2e/installed/state-dependent/location.spec.ts index c439a23408..e57a8d94ed 100644 --- a/gui/test/e2e/installed/state-dependent/location.spec.ts +++ b/gui/test/e2e/installed/state-dependent/location.spec.ts @@ -1,16 +1,16 @@ import { expect, test } from '@playwright/test'; import { Page } from 'playwright'; -import { GetByTestId } from '../../utils'; +import { TestUtils } from '../../utils'; import { startInstalledApp } from '../installed-utils'; // This test expects the daemon to be logged into an account that has time left. let page: Page; -let getByTestId: GetByTestId; +let util: TestUtils; test.beforeAll(async () => { - ({ page, getByTestId } = await startInstalledApp()); + ({ page, util } = await startInstalledApp()); }); test.afterAll(async () => { @@ -18,10 +18,10 @@ test.afterAll(async () => { }); test('App should have a country', async () => { - const countryLabel = getByTestId('country'); + const countryLabel = util.getByTestId('country'); await expect(countryLabel).not.toBeEmpty(); - const cityLabel = getByTestId('city'); + const cityLabel = util.getByTestId('city'); const noCityLabel = await cityLabel.count() === 0; expect(noCityLabel).toBeTruthy(); }); diff --git a/gui/test/e2e/mocked/expired-account-error-view.spec.ts b/gui/test/e2e/mocked/expired-account-error-view.spec.ts index c6e857121d..0d6bacd747 100644 --- a/gui/test/e2e/mocked/expired-account-error-view.spec.ts +++ b/gui/test/e2e/mocked/expired-account-error-view.spec.ts @@ -1,15 +1,15 @@ import { Page } from 'playwright'; -import { SendMockIpcResponse, startMockedApp } from './mocked-utils'; +import { MockedTestUtils, startMockedApp } from './mocked-utils'; import { expect, test } from '@playwright/test'; import { IAccountData } from '../../../src/shared/daemon-rpc-types'; import { getBackgroundColor } from '../utils'; import { colors } from '../../../src/config.json'; let page: Page; -let sendMockIpcResponse: SendMockIpcResponse; +let util: MockedTestUtils; test.beforeAll(async () => { - ({ page, sendMockIpcResponse } = await startMockedApp()); + ({ page, util } = await startMockedApp()); }); test.afterAll(async () => { @@ -17,7 +17,7 @@ test.afterAll(async () => { }); test('App should show Expired Account Error View', async () => { - await sendMockIpcResponse<IAccountData>({ + await util.sendMockIpcResponse<IAccountData>({ channel: 'account-', response: { expiry: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString() }, }); diff --git a/gui/test/e2e/mocked/mocked-utils.ts b/gui/test/e2e/mocked/mocked-utils.ts index f1a8420ef4..19e7ad00f4 100644 --- a/gui/test/e2e/mocked/mocked-utils.ts +++ b/gui/test/e2e/mocked/mocked-utils.ts @@ -1,8 +1,12 @@ import { ElectronApplication } from 'playwright'; -import { startApp, StartAppResponse } from '../utils'; +import { startApp, TestUtils } from '../utils'; -interface StartMockedAppResponse extends StartAppResponse { +interface StartMockedAppResponse extends Awaited<ReturnType<typeof startApp>> { + util: MockedTestUtils, +} + +export interface MockedTestUtils extends TestUtils { mockIpcHandle: MockIpcHandle; sendMockIpcResponse: SendMockIpcResponse; } @@ -14,9 +18,12 @@ export const startMockedApp = async (): Promise<StartMockedAppResponse> => { return { ...startAppResult, - mockIpcHandle, - sendMockIpcResponse, - } + util: { + ...startAppResult.util, + mockIpcHandle, + sendMockIpcResponse, + } + }; }; type MockIpcHandleProps<T> = { diff --git a/gui/test/e2e/mocked/settings.spec.ts b/gui/test/e2e/mocked/settings.spec.ts index bb2d9bedac..633369c920 100644 --- a/gui/test/e2e/mocked/settings.spec.ts +++ b/gui/test/e2e/mocked/settings.spec.ts @@ -1,14 +1,14 @@ import { expect, test } from '@playwright/test'; import { Page } from 'playwright'; -import { SendMockIpcResponse, startMockedApp } from './mocked-utils'; +import { MockedTestUtils, startMockedApp } from './mocked-utils'; import { IAccountData } from '../../../src/shared/daemon-rpc-types'; let page: Page; -let sendMockIpcResponse: SendMockIpcResponse; +let util: MockedTestUtils; test.beforeAll(async () => { - ({ page, sendMockIpcResponse } = await startMockedApp()); + ({ page, util } = await startMockedApp()); await page.click('button[aria-label="Settings"]'); }); @@ -35,7 +35,7 @@ test('Account button should be displayed correctly', async () => { * 729 days left * Add a one-second margin to the test, since it randomly fails in Github Actions otherwise */ - await sendMockIpcResponse<IAccountData>({ + await util.sendMockIpcResponse<IAccountData>({ channel: 'account-', response: { expiry: new Date(Date.now() + 730 * 24 * 60 * 60 * 1000 - 1000).toISOString() }, }); @@ -45,7 +45,7 @@ test('Account button should be displayed correctly', async () => { /** * 2 years left */ - await sendMockIpcResponse<IAccountData>({ + await util.sendMockIpcResponse<IAccountData>({ channel: 'account-', response: { expiry: new Date(Date.now() + 731 * 24 * 60 * 60 * 1000).toISOString() }, }); @@ -55,7 +55,7 @@ test('Account button should be displayed correctly', async () => { /** * Expiry 1 day ago should show 'out of time' */ - await sendMockIpcResponse<IAccountData>({ + await util.sendMockIpcResponse<IAccountData>({ channel: 'account-', response: { expiry: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString() }, }); diff --git a/gui/test/e2e/mocked/tunnel-state.spec.ts b/gui/test/e2e/mocked/tunnel-state.spec.ts index 51e76cb150..c729fa27af 100644 --- a/gui/test/e2e/mocked/tunnel-state.spec.ts +++ b/gui/test/e2e/mocked/tunnel-state.spec.ts @@ -1,7 +1,7 @@ import { test } from '@playwright/test'; import { Page } from 'playwright'; -import { startMockedApp, MockIpcHandle, SendMockIpcResponse } from './mocked-utils'; +import { MockedTestUtils, startMockedApp } from './mocked-utils'; import { ErrorStateCause, ILocation, ITunnelEndpoint, TunnelState } from '../../../src/shared/daemon-rpc-types'; import { assertConnected, assertConnecting, assertDisconnected, assertDisconnecting, assertError } from '../shared/tunnel-state'; @@ -14,11 +14,10 @@ const mockLocation: ILocation = { }; let page: Page; -let mockIpcHandle: MockIpcHandle; -let sendMockIpcResponse: SendMockIpcResponse; +let util: MockedTestUtils; test.beforeAll(async () => { - ({ page, mockIpcHandle, sendMockIpcResponse } = await startMockedApp()); + ({ page, util } = await startMockedApp()); }); test.afterAll(async () => { @@ -29,11 +28,11 @@ test.afterAll(async () => { * Disconnected state */ test('App should show disconnected tunnel state', async () => { - await mockIpcHandle<ILocation>({ + await util.mockIpcHandle<ILocation>({ channel: 'location-get', response: mockLocation, }); - await sendMockIpcResponse<TunnelState>({ + await util.sendMockIpcResponse<TunnelState>({ channel: 'tunnel-', response: { state: 'disconnected' }, }); @@ -44,11 +43,11 @@ test('App should show disconnected tunnel state', async () => { * Connecting state */ test('App should show connecting tunnel state', async () => { - await mockIpcHandle<ILocation>({ + await util.mockIpcHandle<ILocation>({ channel: 'location-get', response: mockLocation, }); - await sendMockIpcResponse<TunnelState>({ + await util.sendMockIpcResponse<TunnelState>({ channel: 'tunnel-', response: { state: 'connecting' }, }); @@ -60,7 +59,7 @@ test('App should show connecting tunnel state', async () => { */ test('App should show connected tunnel state', async () => { const location: ILocation = { ...mockLocation, mullvadExitIp: true }; - await mockIpcHandle<ILocation>({ + await util.mockIpcHandle<ILocation>({ channel: 'location-get', response: location, }); @@ -71,7 +70,7 @@ test('App should show connected tunnel state', async () => { quantumResistant: false, tunnelType: 'wireguard', }; - await sendMockIpcResponse<TunnelState>({ + await util.sendMockIpcResponse<TunnelState>({ channel: 'tunnel-', response: { state: 'connected', details: { endpoint, location } }, }); @@ -83,11 +82,11 @@ test('App should show connected tunnel state', async () => { * Disconnecting state */ test('App should show disconnecting tunnel state', async () => { - await mockIpcHandle<ILocation>({ + await util.mockIpcHandle<ILocation>({ channel: 'location-get', response: mockLocation, }); - await sendMockIpcResponse<TunnelState>({ + await util.sendMockIpcResponse<TunnelState>({ channel: 'tunnel-', response: { state: 'disconnecting', details: 'nothing' }, }); @@ -98,11 +97,11 @@ test('App should show disconnecting tunnel state', async () => { * Error state */ test('App should show error tunnel state', async () => { - await mockIpcHandle<ILocation>({ + await util.mockIpcHandle<ILocation>({ channel: 'location-get', response: mockLocation, }); - await sendMockIpcResponse<TunnelState>({ + await util.sendMockIpcResponse<TunnelState>({ channel: 'tunnel-', response: { state: 'error', details: { cause: ErrorStateCause.isOffline } }, }); diff --git a/gui/test/e2e/utils.ts b/gui/test/e2e/utils.ts index a1fcf206ca..1978f4ede1 100644 --- a/gui/test/e2e/utils.ts +++ b/gui/test/e2e/utils.ts @@ -1,11 +1,20 @@ import { Locator, Page, _electron as electron, ElectronApplication } from 'playwright'; -export type GetByTestId = (id: string) => Locator; - export interface StartAppResponse { app: ElectronApplication; page: Page; - getByTestId: GetByTestId; + util: TestUtils; +} + +export interface TestUtils { + getByTestId: (id: string) => Locator; + currentRoute: () => Promise<void>; + nextRoute: () => Promise<string>; +} + +interface History { + entries: Array<{ pathname: string }>; + index: number; } export const startApp = async ( @@ -30,9 +39,37 @@ export const startApp = async ( console.log(msg.text()); }); - const getByTestId = (id: string) => page.locator(`data-test-id=${id}`); + const util: TestUtils = { + getByTestId: (id: string) => page.locator(`data-test-id=${id}`), + currentRoute: currentRouteFactory(app), + nextRoute: nextRouteFactory(app), + }; + + return { app, page, util }; +}; + +export const currentRouteFactory = (app: ElectronApplication) => { + return async () => { + return await app.evaluate(({ webContents }) => { + return webContents.getAllWebContents()[0].executeJavaScript('window.e2e.location'); + }); + }; +} + +export const nextRouteFactory = (app: ElectronApplication) => { + return async () => { + const nextRoute: string = await app.evaluate(({ ipcMain }) => { + return new Promise((resolve) => { + ipcMain.once('navigation-setHistory', (_event, history: History) => { + resolve(history.entries[history.index].pathname); + }); + }); + }); - return { app, page, getByTestId }; + // TODO: Disable view transitions and shorten timeout or remove completely. + await new Promise((resolve) => setTimeout(resolve, 1000)); + return nextRoute; + }; }; const getStyleProperty = (locator: Locator, property: string) => { diff --git a/gui/types/global/index.d.ts b/gui/types/global/index.d.ts index 72210cacc7..fafa8e3b8c 100644 --- a/gui/types/global/index.d.ts +++ b/gui/types/global/index.d.ts @@ -4,5 +4,6 @@ declare global { interface Window { ipc: typeof IpcRendererEventChannel; env: { platform: NodeJS.Platform; development: boolean; e2e: boolean }; + e2e: { location: string }; } } |
