summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--gui/package.json4
-rw-r--r--gui/src/renderer/components/Marquee.tsx10
-rw-r--r--gui/src/renderer/components/TunnelControl.tsx4
-rw-r--r--gui/src/renderer/preload.ts4
-rw-r--r--gui/test/e2e/daemon/daemon-utils.ts5
-rw-r--r--gui/test/e2e/daemon/location.spec.ts26
-rw-r--r--gui/test/e2e/main.spec.ts21
-rw-r--r--gui/test/e2e/mocked/main.spec.ts20
-rw-r--r--gui/test/e2e/mocked/mocked-utils.ts62
-rw-r--r--gui/test/e2e/mocked/settings.spec.ts (renamed from gui/test/e2e/settings.spec.ts)20
-rw-r--r--gui/test/e2e/mocked/tunnel-state.spec.ts (renamed from gui/test/e2e/tunnel-state.spec.ts)34
-rw-r--r--gui/test/e2e/utils.ts62
12 files changed, 168 insertions, 104 deletions
diff --git a/gui/package.json b/gui/package.json
index fcf86629f8..5d06305e5d 100644
--- a/gui/package.json
+++ b/gui/package.json
@@ -95,7 +95,9 @@
"format": "prettier \"**/*.{js,css,ts,tsx}\" --write",
"tsc": "tsc -p . --noEmit",
"e2e": "npm run build && npm run e2e:no-build",
- "e2e:no-build": "xvfb-maybe -- playwright test",
+ "e2e:no-build": "xvfb-maybe -- playwright test mocked",
+ "e2e:with-daemon": "npm run build && npm run e2e:with-daemon:no-build",
+ "e2e:with-daemon:no-build": "xvfb-maybe -- playwright test daemon",
"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/components/Marquee.tsx b/gui/src/renderer/components/Marquee.tsx
index ed5999c279..5175fdc4a0 100644
--- a/gui/src/renderer/components/Marquee.tsx
+++ b/gui/src/renderer/components/Marquee.tsx
@@ -22,7 +22,7 @@ interface IMarqueeProps {
children?: React.ReactNode;
}
-interface IMarqueeState {
+interface IMarqueeState extends React.HTMLAttributes<HTMLSpanElement> {
alignRight: boolean;
// uniqueKey is used to force the Text component to remount to achieve the initial position of the
// text without using a transition.
@@ -60,16 +60,18 @@ export default class Marquee extends React.Component<IMarqueeProps, IMarqueeStat
}
public render() {
+ const { children, ...otherProps } = this.props;
+
return (
<Container>
<Text
key={this.state.uniqueKey}
ref={this.textRef}
- className={this.props.className}
overflow={this.calculateOverflow()}
alignRight={this.state.alignRight}
- onTransitionEnd={this.scheduleToggleAlignRight}>
- {this.props.children}
+ onTransitionEnd={this.scheduleToggleAlignRight}
+ {...otherProps}>
+ {children}
</Text>
</Container>
);
diff --git a/gui/src/renderer/components/TunnelControl.tsx b/gui/src/renderer/components/TunnelControl.tsx
index 4d210ed2c2..0e282d88aa 100644
--- a/gui/src/renderer/components/TunnelControl.tsx
+++ b/gui/src/renderer/components/TunnelControl.tsx
@@ -221,7 +221,7 @@ export default class TunnelControl extends React.Component<ITunnelControlProps>
const city = this.props.city === undefined ? '' : relayLocations.gettext(this.props.city);
return (
<LocationRow>
- <StyledMarquee>{city}</StyledMarquee>
+ <StyledMarquee data-test-id="city">{city}</StyledMarquee>
</LocationRow>
);
}
@@ -231,7 +231,7 @@ export default class TunnelControl extends React.Component<ITunnelControlProps>
this.props.country === undefined ? '' : relayLocations.gettext(this.props.country);
return (
<LocationRow>
- <StyledMarquee>{country}</StyledMarquee>
+ <StyledMarquee data-test-id="country">{country}</StyledMarquee>
</LocationRow>
);
}
diff --git a/gui/src/renderer/preload.ts b/gui/src/renderer/preload.ts
index 61b92a5962..864d1dc4d0 100644
--- a/gui/src/renderer/preload.ts
+++ b/gui/src/renderer/preload.ts
@@ -9,3 +9,7 @@ contextBridge.exposeInMainWorld('env', {
development: process.env.NODE_ENV === 'development',
platform: process.platform,
});
+
+if (process.env.CI) {
+ contextBridge.exposeInMainWorld('__REACT_DEVTOOLS_GLOBAL_HOOK__', { isDisabled: true });
+}
diff --git a/gui/test/e2e/daemon/daemon-utils.ts b/gui/test/e2e/daemon/daemon-utils.ts
new file mode 100644
index 0000000000..a5aeacc376
--- /dev/null
+++ b/gui/test/e2e/daemon/daemon-utils.ts
@@ -0,0 +1,5 @@
+import { startApp, StartAppResponse } from '../utils';
+
+export const startAppWithDaemon = async (): Promise<StartAppResponse> => {
+ return startApp('.');
+};
diff --git a/gui/test/e2e/daemon/location.spec.ts b/gui/test/e2e/daemon/location.spec.ts
new file mode 100644
index 0000000000..15a86abb37
--- /dev/null
+++ b/gui/test/e2e/daemon/location.spec.ts
@@ -0,0 +1,26 @@
+import { expect, test } from '@playwright/test';
+import { Page } from 'playwright';
+
+import { GetByTestId } from '../utils';
+import { startAppWithDaemon } from './daemon-utils';
+
+let page: Page;
+let getByTestId: GetByTestId;
+
+test.beforeAll(async () => {
+ ({ page, getByTestId } = await startAppWithDaemon());
+});
+
+test.afterAll(async () => {
+ await page.close();
+});
+
+test('App should have a country', async () => {
+ const countryLabel = getByTestId('country');
+ await expect(countryLabel).not.toBeEmpty();
+
+ const cityLabel = getByTestId('city');
+ const noCityLabel = await cityLabel.count() === 0;
+ expect(noCityLabel).toBeTruthy();
+});
+
diff --git a/gui/test/e2e/main.spec.ts b/gui/test/e2e/main.spec.ts
deleted file mode 100644
index 85f69f9cf6..0000000000
--- a/gui/test/e2e/main.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { startApp } from './utils';
-
-let appWindow: Page;
-
-test.beforeAll(async () => {
- const startAppResponse = await startApp();
- appWindow = startAppResponse.appWindow;
-});
-
-test.afterAll(async () => {
- await appWindow.close();
-});
-
-test('Validate title', async () => {
- const title = await appWindow.title();
- expect(title).toBe('Mullvad VPN');
- await expect(appWindow.locator('header')).toBeVisible();
-});
diff --git a/gui/test/e2e/mocked/main.spec.ts b/gui/test/e2e/mocked/main.spec.ts
new file mode 100644
index 0000000000..a3f85c0ab6
--- /dev/null
+++ b/gui/test/e2e/mocked/main.spec.ts
@@ -0,0 +1,20 @@
+import { expect, test } from '@playwright/test';
+import { Page } from 'playwright';
+
+import { startAppWithMocking } from './mocked-utils';
+
+let page: Page;
+
+test.beforeAll(async () => {
+ ({ page } = await startAppWithMocking());
+});
+
+test.afterAll(async () => {
+ await page.close();
+});
+
+test('Validate title', async () => {
+ const title = await page.title();
+ expect(title).toBe('Mullvad VPN');
+ await expect(page.locator('header')).toBeVisible();
+});
diff --git a/gui/test/e2e/mocked/mocked-utils.ts b/gui/test/e2e/mocked/mocked-utils.ts
new file mode 100644
index 0000000000..f7e604f350
--- /dev/null
+++ b/gui/test/e2e/mocked/mocked-utils.ts
@@ -0,0 +1,62 @@
+import { ElectronApplication } from 'playwright';
+
+import { startApp, StartAppResponse } from '../utils';
+
+interface StartMockedAppResponse extends StartAppResponse {
+ mockIpcHandle: MockIpcHandle;
+ sendMockIpcResponse: SendMockIpcResponse;
+}
+
+export const startAppWithMocking = async (): Promise<StartMockedAppResponse> => {
+ const startAppResult = await startApp('build/test/e2e/setup/main.js');
+ const mockIpcHandle = generateMockIpcHandle(startAppResult.app);
+ const sendMockIpcResponse = generateSendMockIpcResponse(startAppResult.app);
+
+ return {
+ ...startAppResult,
+ mockIpcHandle,
+ sendMockIpcResponse,
+ }
+};
+
+type MockIpcHandleProps<T> = {
+ channel: string;
+ response: T;
+};
+
+export type MockIpcHandle = ReturnType<typeof generateMockIpcHandle>;
+
+export const generateMockIpcHandle = (electronApp: ElectronApplication) => {
+ return async <T>({ channel, response }: MockIpcHandleProps<T>): Promise<void> => {
+ await electronApp.evaluate(
+ ({ ipcMain }, { channel, response }) => {
+ ipcMain.removeHandler(channel);
+ ipcMain.handle(channel, () => {
+ return Promise.resolve({
+ type: 'success',
+ value: response,
+ });
+ });
+ },
+ { channel, response },
+ );
+ };
+};
+
+type SendMockIpcResponseProps<T> = {
+ channel: string;
+ response: T;
+};
+
+export type SendMockIpcResponse = ReturnType<typeof generateMockIpcHandle>;
+
+export const generateSendMockIpcResponse = (electronApp: ElectronApplication) => {
+ return async <T>({ channel, response }: SendMockIpcResponseProps<T>) => {
+ await electronApp.evaluate(
+ ({ webContents }, { channel, response }) => {
+ webContents.getAllWebContents()[0].send(channel, response);
+ },
+ { channel, response },
+ );
+ };
+};
diff --git a/gui/test/e2e/settings.spec.ts b/gui/test/e2e/mocked/settings.spec.ts
index e4b1f47108..510c5e97e6 100644
--- a/gui/test/e2e/settings.spec.ts
+++ b/gui/test/e2e/mocked/settings.spec.ts
@@ -1,31 +1,31 @@
import { expect, test } from '@playwright/test';
import { Page } from 'playwright';
-import { sendMockIpcResponse, startApp } from './utils';
-import { IAccountData } from '../../src/shared/daemon-rpc-types';
+import { SendMockIpcResponse, startAppWithMocking } from './mocked-utils';
+import { IAccountData } from '../../../src/shared/daemon-rpc-types';
-let appWindow: Page;
+let page: Page;
+let sendMockIpcResponse: SendMockIpcResponse;
test.beforeAll(async () => {
- const startAppResponse = await startApp();
- appWindow = startAppResponse.appWindow;
- await appWindow.click('button[aria-label="Settings"]');
+ ({ page, sendMockIpcResponse } = await startAppWithMocking());
+ await page.click('button[aria-label="Settings"]');
});
test.afterAll(async () => {
- await appWindow.close();
+ await page.close();
});
test('Settings Page', async () => {
- const title = appWindow.locator('h1');
+ const title = page.locator('h1');
await expect(title).toContainText('Settings');
- const closeButton = appWindow.locator('button[aria-label="Close"]');
+ const closeButton = page.locator('button[aria-label="Close"]');
await expect(closeButton).toBeVisible();
});
test('Account button should be displayed correctly', async () => {
- const accountButton = appWindow.locator('button:has-text("Account")');
+ const accountButton = page.locator('button:has-text("Account")');
await expect(accountButton).toBeVisible();
let expiryText = accountButton.locator('span');
diff --git a/gui/test/e2e/tunnel-state.spec.ts b/gui/test/e2e/mocked/tunnel-state.spec.ts
index 985ac9e665..c1f0317e24 100644
--- a/gui/test/e2e/tunnel-state.spec.ts
+++ b/gui/test/e2e/mocked/tunnel-state.spec.ts
@@ -1,15 +1,10 @@
import { expect, test } from '@playwright/test';
import { Page } from 'playwright';
-import { colors } from '../../src/config.json';
-import { ILocation, ITunnelEndpoint, TunnelState } from '../../src/shared/daemon-rpc-types';
-import {
- getBackgroundColor,
- getColor,
- mockIpcHandle,
- sendMockIpcResponse,
- startApp,
-} from './utils';
+import { colors } from '../../../src/config.json';
+import { ILocation, ITunnelEndpoint, TunnelState } from '../../../src/shared/daemon-rpc-types';
+import { getBackgroundColor, getColor } from '../utils';
+import { startAppWithMocking, MockIpcHandle, SendMockIpcResponse } from './mocked-utils';
const UNSECURED_COLOR = colors.red;
const SECURE_COLOR = colors.green;
@@ -23,18 +18,19 @@ const mockLocation: ILocation = {
mullvadExitIp: false,
};
-const getLabel = () => appWindow.locator('span[role="status"]');
-const getHeader = () => appWindow.locator('header');
+const getLabel = () => page.locator('span[role="status"]');
+const getHeader = () => page.locator('header');
-let appWindow: Page;
+let page: Page;
+let mockIpcHandle: MockIpcHandle;
+let sendMockIpcResponse: SendMockIpcResponse;
test.beforeAll(async () => {
- const startAppResponse = await startApp();
- appWindow = startAppResponse.appWindow;
+ ({ page, mockIpcHandle, sendMockIpcResponse } = await startAppWithMocking());
});
test.afterAll(async () => {
- await appWindow.close();
+ await page.close();
});
/**
@@ -60,7 +56,7 @@ test('App should show disconnected tunnel state', async () => {
const headerColor = await getBackgroundColor(header);
expect(headerColor).toBe(UNSECURED_COLOR);
- const button = appWindow.locator('button', { hasText: /secure my connection/i });
+ const button = page.locator('button', { hasText: /secure my connection/i });
const buttonColor = await getBackgroundColor(button);
expect(buttonColor).toBe(SECURE_COLOR);
});
@@ -88,7 +84,7 @@ test('App should show connecting tunnel state', async () => {
const headerColor = await getBackgroundColor(header);
expect(headerColor).toBe(SECURE_COLOR);
- const button = appWindow.locator('button', { hasText: /cancel/i });
+ const button = page.locator('button', { hasText: /cancel/i });
const buttonColor = await getBackgroundColor(button);
expect(buttonColor).toBe('rgba(227, 64, 57, 0.6)');
});
@@ -123,7 +119,7 @@ test('App should show connected tunnel state', async () => {
const headerColor = await getBackgroundColor(header);
expect(headerColor).toBe(SECURE_COLOR);
- const button = appWindow.locator('button', { hasText: /switch location/i });
+ const button = page.locator('button', { hasText: /switch location/i });
const buttonColor = await getBackgroundColor(button);
expect(buttonColor).toBe('rgba(255, 255, 255, 0.2)');
});
@@ -149,7 +145,7 @@ test('App should show disconnecting tunnel state', async () => {
const headerColor = await getBackgroundColor(header);
expect(headerColor).toBe(UNSECURED_COLOR);
- const button = appWindow.locator('button', { hasText: /secure my connection/i });
+ const button = page.locator('button', { hasText: /secure my connection/i });
const buttonColor = await getBackgroundColor(button);
expect(buttonColor).toBe(SECURE_COLOR);
});
diff --git a/gui/test/e2e/utils.ts b/gui/test/e2e/utils.ts
index 865d74bb1c..0c68d47d49 100644
--- a/gui/test/e2e/utils.ts
+++ b/gui/test/e2e/utils.ts
@@ -1,65 +1,33 @@
-import { Locator, Page } from 'playwright';
-import { _electron as electron, ElectronApplication } from 'playwright-core';
+import { Locator, Page, _electron as electron, ElectronApplication } from 'playwright';
-interface StartAppResponse {
- electronApp: ElectronApplication;
- appWindow: Page;
-}
+export type GetByTestId = (id: string) => Locator;
-let electronApp: ElectronApplication;
+export interface StartAppResponse {
+ app: ElectronApplication;
+ page: Page;
+ getByTestId: GetByTestId;
+}
-export const startApp = async (): Promise<StartAppResponse> => {
+export const startApp = async (mainPath: string): Promise<StartAppResponse> => {
process.env.CI = 'e2e';
- electronApp = await electron.launch({
- args: ['build/test/e2e/setup/main.js'],
+ const app = await electron.launch({
+ args: [mainPath],
});
- const appWindow = await electronApp.firstWindow();
+ const page = await app.firstWindow();
- appWindow.on('pageerror', (error) => {
+ page.on('pageerror', (error) => {
console.log(error);
});
- appWindow.on('console', (msg) => {
+ page.on('console', (msg) => {
console.log(msg.text());
});
- return { electronApp, appWindow };
-};
+ const getByTestId = (id: string) => page.locator(`data-test-id=${id}`);
-type MockIpcHandleProps<T> = {
- channel: string;
- response: T;
-};
-
-export const mockIpcHandle = async <T>({ channel, response }: MockIpcHandleProps<T>) => {
- await electronApp.evaluate(
- ({ ipcMain }, { channel, response }) => {
- ipcMain.removeHandler(channel);
- ipcMain.handle(channel, () => {
- return Promise.resolve({
- type: 'success',
- value: response,
- });
- });
- },
- { channel, response },
- );
-};
-
-type SendMockIpcResponseProps<T> = {
- channel: string;
- response: T;
-};
-
-export const sendMockIpcResponse = async <T>({ channel, response }: SendMockIpcResponseProps<T>) => {
- await electronApp.evaluate(
- ({ webContents }, { channel, response }) => {
- webContents.getAllWebContents()[0].send(channel, response);
- },
- { channel, response },
- );
+ return { app, page, getByTestId };
};
const getStyleProperty = (locator: Locator, property: string) => {