summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-12-15 16:34:38 +0100
committerOskar Nyberg <oskar@mullvad.net>2022-12-15 16:34:38 +0100
commitd719c03f546d64284a8a4898aa04500d5677abd7 (patch)
treedd346890d9bc75d66775868c6250e32b2d7f6f38
parent0982a4a55e692797cee18f82cd1ffe753e02da33 (diff)
parentc9beb24e50a52b5941a731b6fb456e41ebcdc55e (diff)
downloadmullvadvpn-d719c03f546d64284a8a4898aa04500d5677abd7.tar.xz
mullvadvpn-d719c03f546d64284a8a4898aa04500d5677abd7.zip
Merge branch 'add-more-e2e-tests'
-rw-r--r--gui/README.md4
-rw-r--r--gui/package.json4
-rw-r--r--gui/src/renderer/app.tsx9
-rw-r--r--gui/test/e2e/installed/installed-utils.ts4
-rw-r--r--gui/test/e2e/installed/requires-input/login.spec.ts32
-rw-r--r--gui/test/e2e/installed/state-dependent/disconnected.spec.ts2
-rw-r--r--gui/test/e2e/installed/state-dependent/location.spec.ts10
-rw-r--r--gui/test/e2e/mocked/expired-account-error-view.spec.ts8
-rw-r--r--gui/test/e2e/mocked/mocked-utils.ts17
-rw-r--r--gui/test/e2e/mocked/settings.spec.ts12
-rw-r--r--gui/test/e2e/mocked/tunnel-state.spec.ts27
-rw-r--r--gui/test/e2e/utils.ts47
-rw-r--r--gui/types/global/index.d.ts1
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 };
}
}