summaryrefslogtreecommitdiffhomepage
path: root/gui/test
diff options
context:
space:
mode:
authorOskar <oskar@mullvad.net>2024-11-05 07:57:08 +0100
committerOskar <oskar@mullvad.net>2024-11-14 16:43:18 +0100
commit84f14d79c4f0dde73337820ec94ba8ff928a3797 (patch)
treece468658e5ba7b0a74950c7ad1b09b3a4d00520b /gui/test
parente3ce0eb5cd0610dbff6ec98cb8cb388415c74bf6 (diff)
downloadmullvadvpn-84f14d79c4f0dde73337820ec94ba8ff928a3797.tar.xz
mullvadvpn-84f14d79c4f0dde73337820ec94ba8ff928a3797.zip
Move gui directory to desktop/packages/mullvad-vpn
Diffstat (limited to 'gui/test')
-rw-r--r--gui/test/e2e/installed/installed-utils.ts18
-rw-r--r--gui/test/e2e/installed/playwright.config.ts12
-rw-r--r--gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts179
-rw-r--r--gui/test/e2e/installed/state-dependent/custom-bridge.spec.ts160
-rw-r--r--gui/test/e2e/installed/state-dependent/device-revoked.spec.ts29
-rw-r--r--gui/test/e2e/installed/state-dependent/disconnected.spec.ts22
-rw-r--r--gui/test/e2e/installed/state-dependent/location.spec.ts25
-rw-r--r--gui/test/e2e/installed/state-dependent/login.spec.ts145
-rw-r--r--gui/test/e2e/installed/state-dependent/macos-split-tunneling.spec.ts163
-rw-r--r--gui/test/e2e/installed/state-dependent/obfuscation.spec.ts106
-rw-r--r--gui/test/e2e/installed/state-dependent/settings-import.spec.ts116
-rw-r--r--gui/test/e2e/installed/state-dependent/settings.spec.ts103
-rw-r--r--gui/test/e2e/installed/state-dependent/too-many-devices.spec.ts58
-rw-r--r--gui/test/e2e/installed/state-dependent/tunnel-state.spec.ts182
-rw-r--r--gui/test/e2e/mocked/expired-account-error-view.spec.ts49
-rw-r--r--gui/test/e2e/mocked/feature-indicators.spec.ts140
-rw-r--r--gui/test/e2e/mocked/main.spec.ts20
-rw-r--r--gui/test/e2e/mocked/mocked-utils.ts83
-rw-r--r--gui/test/e2e/mocked/notifications.spec.ts52
-rw-r--r--gui/test/e2e/mocked/select-location.spec.ts148
-rw-r--r--gui/test/e2e/mocked/settings.spec.ts64
-rw-r--r--gui/test/e2e/mocked/tunnel-state.spec.ts121
-rw-r--r--gui/test/e2e/setup/main.ts204
-rw-r--r--gui/test/e2e/shared/tunnel-state.ts90
-rw-r--r--gui/test/e2e/utils.ts134
-rw-r--r--gui/test/unit/account-data-cache.spec.ts315
-rw-r--r--gui/test/unit/changelog.spec.ts66
-rw-r--r--gui/test/unit/date-helper.spec.ts174
-rw-r--r--gui/test/unit/history.spec.ts105
-rw-r--r--gui/test/unit/ip.spec.ts175
-rw-r--r--gui/test/unit/keyframe-animation.spec.ts137
-rw-r--r--gui/test/unit/list-diff.spec.ts104
-rw-r--r--gui/test/unit/logging.spec.ts139
-rw-r--r--gui/test/unit/notification-evaluation.spec.ts154
-rw-r--r--gui/test/unit/setup.ts6
-rw-r--r--gui/test/unit/tunnel-state.spec.ts109
36 files changed, 0 insertions, 3907 deletions
diff --git a/gui/test/e2e/installed/installed-utils.ts b/gui/test/e2e/installed/installed-utils.ts
deleted file mode 100644
index 9e4dd6c79c..0000000000
--- a/gui/test/e2e/installed/installed-utils.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { startApp } from '../utils';
-
-export const startInstalledApp = async (): ReturnType<typeof startApp> => {
- return startApp({ executablePath: getAppInstallPath() });
-};
-
-function getAppInstallPath(): string {
- switch (process.platform) {
- case 'win32':
- return 'C:\\Program Files\\Mullvad VPN\\Mullvad VPN.exe';
- case 'linux':
- return '/opt/Mullvad VPN/mullvad-gui';
- case 'darwin':
- return '/Applications/Mullvad VPN.app/Contents/MacOS/Mullvad VPN';
- default:
- throw new Error('Platform not supported');
- }
-}
diff --git a/gui/test/e2e/installed/playwright.config.ts b/gui/test/e2e/installed/playwright.config.ts
deleted file mode 100644
index 9487e06905..0000000000
--- a/gui/test/e2e/installed/playwright.config.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineConfig } from '@playwright/test';
-
-export default defineConfig({
- testDir: process.cwd(),
- timeout: 60_000,
- workers: 1,
- reportSlowTests: null,
- maxFailures: 1,
- expect: {
- timeout: 30_000,
- },
-});
diff --git a/gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts b/gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts
deleted file mode 100644
index a447b5975e..0000000000
--- a/gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts
+++ /dev/null
@@ -1,179 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { RoutePath } from '../../../../src/renderer/lib/routes';
-import { TestUtils } from '../../utils';
-import { startInstalledApp } from '../installed-utils';
-
-// This test expects the daemon to be logged in and only have "Direct" and "Mullvad Bridges"
-// access methods.
-// Env parameters:
-// `SHADOWSOCKS_SERVER_IP`
-// `SHADOWSOCKS_SERVER_PORT`
-// `SHADOWSOCKS_SERVER_CIPHER`
-// `SHADOWSOCKS_SERVER_PASSWORD`
-
-const DIRECT_NAME = 'Direct';
-const BRIDGES_NAME = 'Mullvad Bridges';
-const ENCRYPTED_DNS_PROXY_NAME = 'Encrypted DNS proxy';
-const IN_USE_LABEL = 'In use';
-const FUNCTIONING_METHOD_NAME = 'Test method';
-const NON_FUNCTIONING_METHOD_NAME = 'Non functioning test method';
-
-let page: Page;
-let util: TestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-async function navigateToAccessMethods() {
- await util.waitForNavigation(() => page.click('button[aria-label="Settings"]'));
- await util.waitForNavigation(() => page.getByText('API access').click());
-
- const title = page.locator('h1');
- await expect(title).toHaveText('API access');
-}
-
-test('App should display access methods', async () => {
- await navigateToAccessMethods();
-
- const accessMethods = page.getByTestId('access-method');
- await expect(accessMethods).toHaveCount(3);
-
- const direct = accessMethods.first();
- const bridges = accessMethods.nth(1);
- const encryptedDnsProxy = accessMethods.nth(2);
- await expect(direct).toContainText(DIRECT_NAME);
- await expect(bridges).toContainText(BRIDGES_NAME);
- await expect(encryptedDnsProxy).toContainText(ENCRYPTED_DNS_PROXY_NAME);
- await expect(page.getByText(IN_USE_LABEL)).toHaveCount(1);
-});
-
-test('App should add invalid access method', async () => {
- await util.waitForNavigation(() => page.locator('button:has-text("Add")').click());
-
- const title = page.locator('h1');
- await expect(title).toHaveText('Add method');
-
- const inputs = page.locator('input');
- const addButton = page.locator('button:has-text("Add")');
- await expect(addButton).toBeVisible();
- await expect(addButton).toBeDisabled();
-
- await inputs.first().fill(NON_FUNCTIONING_METHOD_NAME);
- await expect(addButton).toBeDisabled();
-
- await inputs.nth(1).fill(process.env.SHADOWSOCKS_SERVER_IP!);
- await expect(addButton).toBeDisabled();
-
- await inputs.nth(2).fill(process.env.SHADOWSOCKS_SERVER_PORT!);
- await expect(addButton).toBeEnabled();
-
- await addButton.click();
-
- await expect(page.getByText('Testing method...')).toBeVisible();
- await expect(page.getByText('API unreachable, add anyway?')).toBeVisible();
-
- expect(
- await util.waitForNavigation(() => page.locator('button:has-text("Save")').click()),
- ).toEqual(RoutePath.apiAccessMethods);
-
- const accessMethods = page.getByTestId('access-method');
- // Direct, Bridges, Encrypted DNS Proxy & the non-functioning access method.
- await expect(accessMethods).toHaveCount(4);
-
- await expect(accessMethods.last()).toHaveText(NON_FUNCTIONING_METHOD_NAME);
-});
-
-test('App should use invalid method', async () => {
- const accessMethods = page.getByTestId('access-method');
- const nonFunctioningTestMethod = accessMethods.last();
-
- await expect(page.getByText(IN_USE_LABEL)).toHaveCount(1);
- await expect(nonFunctioningTestMethod).not.toContainText(IN_USE_LABEL);
-
- await nonFunctioningTestMethod.locator('button').last().click();
- await nonFunctioningTestMethod.getByText('Use').click();
- await expect(nonFunctioningTestMethod).toContainText('Testing...');
- await expect(nonFunctioningTestMethod).toContainText('API unreachable');
-
- await expect(page.getByText(IN_USE_LABEL)).toHaveCount(1);
- await expect(nonFunctioningTestMethod).not.toContainText(IN_USE_LABEL);
-});
-
-test('App should edit access method', async () => {
- const customMethod = page.getByTestId('access-method').last();
- await customMethod.locator('button').last().click();
- await util.waitForNavigation(() => customMethod.getByText('Edit').click());
-
- const title = page.locator('h1');
- await expect(title).toHaveText('Edit method');
-
- const inputs = page.locator('input');
- const saveButton = page.locator('button:has-text("Save")');
- await expect(saveButton).toBeVisible();
- await expect(saveButton).toBeEnabled();
-
- await expect(inputs.first()).toHaveValue(NON_FUNCTIONING_METHOD_NAME);
- await expect(inputs.nth(1)).toHaveValue(process.env.SHADOWSOCKS_SERVER_IP!);
- await expect(inputs.nth(2)).toHaveValue(process.env.SHADOWSOCKS_SERVER_PORT!);
-
- await inputs.first().fill(FUNCTIONING_METHOD_NAME);
- await expect(saveButton).toBeEnabled();
-
- await inputs.nth(3).fill(process.env.SHADOWSOCKS_SERVER_PASSWORD!);
-
- await page.getByTestId('ciphers').click();
- await page
- .getByRole('option', { name: process.env.SHADOWSOCKS_SERVER_CIPHER!, exact: true })
- .click();
-
- expect(await util.waitForNavigation(() => saveButton.click())).toEqual(
- RoutePath.apiAccessMethods,
- );
-
- const accessMethods = page.getByTestId('access-method');
- // Direct, Bridges, Encrypted DNS Proxy & the custom access method.
- await expect(accessMethods).toHaveCount(4);
-
- await expect(accessMethods.last()).toHaveText(FUNCTIONING_METHOD_NAME);
-});
-
-test('App should use valid method', async () => {
- const accessMethods = page.getByTestId('access-method');
-
- const direct = accessMethods.first();
- const bridges = accessMethods.nth(1);
- const encryptedDnsProxy = accessMethods.nth(2);
- const functioningTestMethod = accessMethods.last();
-
- await expect(page.getByText(IN_USE_LABEL)).toHaveCount(1);
- await expect(functioningTestMethod).not.toContainText(IN_USE_LABEL);
- await expect(functioningTestMethod).toHaveText(FUNCTIONING_METHOD_NAME);
-
- await functioningTestMethod.locator('button').last().click();
- await functioningTestMethod.getByText('Use').click();
- await expect(direct).not.toContainText(IN_USE_LABEL);
- await expect(bridges).not.toContainText(IN_USE_LABEL);
- await expect(encryptedDnsProxy).not.toContainText(IN_USE_LABEL);
- await expect(functioningTestMethod).toContainText('API reachable');
- await expect(functioningTestMethod).toContainText(IN_USE_LABEL);
-});
-
-test('App should delete method', async () => {
- const accessMethods = page.getByTestId('access-method');
- const customMethod = accessMethods.last();
-
- await customMethod.locator('button').last().click();
- await customMethod.getByText('Delete').click();
-
- await expect(page.getByText(`Delete ${FUNCTIONING_METHOD_NAME}?`)).toBeVisible();
- await page.locator('button:has-text("Delete")').click();
- // Direct, Bridges, Encrypted DNS Proxy.
- await expect(accessMethods).toHaveCount(3);
-});
diff --git a/gui/test/e2e/installed/state-dependent/custom-bridge.spec.ts b/gui/test/e2e/installed/state-dependent/custom-bridge.spec.ts
deleted file mode 100644
index 34efb60f26..0000000000
--- a/gui/test/e2e/installed/state-dependent/custom-bridge.spec.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { colors } from '../../../../src/config.json';
-import { RoutePath } from '../../../../src/renderer/lib/routes';
-import { TestUtils } from '../../utils';
-import { startInstalledApp } from '../installed-utils';
-
-// This test expects the daemon to be logged in and not have a custom bridge configured.
-// Env parameters:
-// `SHADOWSOCKS_SERVER_IP`
-// `SHADOWSOCKS_SERVER_PORT`
-// `SHADOWSOCKS_SERVER_CIPHER`
-// `SHADOWSOCKS_SERVER_PASSWORD`
-
-let page: Page;
-let util: TestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-test('App should enable bridge mode', async () => {
- await util.waitForNavigation(() => page.click('button[aria-label="Settings"]'));
- expect(await util.waitForNavigation(() => page.getByText('VPN settings').click())).toBe(
- RoutePath.vpnSettings,
- );
-
- await page.getByRole('option', { name: 'OpenVPN' }).click();
-
- expect(await util.waitForNavigation(() => page.getByText('OpenVPN settings').click())).toBe(
- RoutePath.openVpnSettings,
- );
-
- await page.getByTestId('bridge-mode-on').click();
- await expect(page.getByText('Enable bridge mode?')).toBeVisible();
-
- await page.getByTestId('enable-confirm').click();
-
- await util.waitForNavigation(() => page.click('button[aria-label="Back"]'));
- await util.waitForNavigation(() => page.click('button[aria-label="Back"]'));
- expect(await util.waitForNavigation(() => page.click('button[aria-label="Close"]'))).toBe(
- RoutePath.main,
- );
-});
-
-test('App display disabled custom bridge', async () => {
- expect(
- await util.waitForNavigation(() => page.click('button[aria-label^="Select location"]')),
- ).toBe(RoutePath.selectLocation);
-
- const title = page.locator('h1');
- await expect(title).toHaveText('Select location');
-
- await page.getByText(/^Entry$/).click();
-
- const customBridgeButton = page.getByText('Custom bridge');
- await expect(customBridgeButton).toBeDisabled();
-});
-
-test('App should add new custom bridge', async () => {
- expect(
- await util.waitForNavigation(() => page.click('button[aria-label="Add new custom bridge"]')),
- ).toBe(RoutePath.editCustomBridge);
-
- const title = page.locator('h1');
- await expect(title).toHaveText('Add custom bridge');
-
- const inputs = page.locator('input');
- const addButton = page.locator('button:has-text("Add")');
- await expect(addButton).toBeVisible();
- await expect(addButton).toBeDisabled();
-
- await inputs.first().fill(process.env.SHADOWSOCKS_SERVER_IP!);
- await expect(addButton).toBeDisabled();
-
- await inputs.nth(1).fill('443');
- await expect(addButton).toBeEnabled();
-
- await inputs.nth(2).fill(process.env.SHADOWSOCKS_SERVER_PASSWORD!);
-
- await page.getByTestId('ciphers').click();
- await page
- .getByRole('option', { name: process.env.SHADOWSOCKS_SERVER_CIPHER!, exact: true })
- .click();
-
- expect(await util.waitForNavigation(() => addButton.click())).toEqual(RoutePath.selectLocation);
-
- const customBridgeButton = page.getByText('Custom bridge');
- await expect(customBridgeButton).toBeEnabled();
-
- await expect(page.locator('button[aria-label="Edit custom bridge"]')).toBeVisible();
-});
-
-test('App should select custom bridge', async () => {
- const customBridgeButton = page.locator('button:has-text("Custom bridge")');
- await expect(customBridgeButton).toHaveCSS('background-color', colors.green);
-
- const automaticButton = page.getByText('Automatic');
- await automaticButton.click();
- await page.getByText(/^Entry$/).click();
- await expect(customBridgeButton).not.toHaveCSS('background-color', colors.green);
-
- await customBridgeButton.click();
- await page.getByText(/^Entry$/).click();
- await expect(customBridgeButton).toHaveCSS('background-color', colors.green);
-});
-
-test('App should edit custom bridge', async () => {
- const automaticButton = page.getByText('Automatic');
- await automaticButton.click();
- await page.getByText(/^Entry$/).click();
-
- expect(
- await util.waitForNavigation(() => page.click('button[aria-label="Edit custom bridge"]')),
- ).toBe(RoutePath.editCustomBridge);
-
- const title = page.locator('h1');
- await expect(title).toHaveText('Edit custom bridge');
-
- const inputs = page.locator('input');
- const saveButton = page.locator('button:has-text("Save")');
- await expect(saveButton).toBeVisible();
- await expect(saveButton).toBeEnabled();
-
- await inputs.nth(1).fill(process.env.SHADOWSOCKS_SERVER_PORT!);
- await expect(saveButton).toBeEnabled();
-
- expect(await util.waitForNavigation(() => saveButton.click())).toEqual(RoutePath.selectLocation);
-
- const customBridgeButton = page.locator('button:has-text("Custom bridge")');
- await expect(customBridgeButton).toBeEnabled();
- await expect(customBridgeButton).toHaveCSS('background-color', colors.green);
-});
-
-test('App should delete custom bridge', async () => {
- expect(
- await util.waitForNavigation(() => page.click('button[aria-label="Edit custom bridge"]')),
- ).toBe(RoutePath.editCustomBridge);
-
- const deleteButton = page.locator('button:has-text("Delete")');
- await expect(deleteButton).toBeVisible();
- await expect(deleteButton).toBeEnabled();
-
- await deleteButton.click();
- await expect(page.getByText('Delete custom bridge?')).toBeVisible();
-
- const confirmButton = page.getByTestId('delete-confirm');
- expect(await util.waitForNavigation(() => confirmButton.click())).toEqual(
- RoutePath.selectLocation,
- );
-
- const customBridgeButton = page.locator('button:has-text("Custom bridge")');
- await expect(customBridgeButton).toBeDisabled();
- await expect(customBridgeButton).not.toHaveCSS('background-color', colors.green);
-});
diff --git a/gui/test/e2e/installed/state-dependent/device-revoked.spec.ts b/gui/test/e2e/installed/state-dependent/device-revoked.spec.ts
deleted file mode 100644
index 2b9f8d0c58..0000000000
--- a/gui/test/e2e/installed/state-dependent/device-revoked.spec.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { RoutePath } from '../../../../src/renderer/lib/routes';
-import { TestUtils } from '../../utils';
-import { startInstalledApp } from '../installed-utils';
-
-// This test expects the daemon to be logged in to a revoked device.
-
-let page: Page;
-let util: TestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-test('App should fail to login', async () => {
- expect(await util.currentRoute()).toEqual(RoutePath.deviceRevoked);
-
- await expect(page.getByTestId('title')).toHaveText('Device is inactive');
-
- expect(await util.waitForNavigation(() => page.getByText('Go to login').click())).toEqual(
- RoutePath.login,
- );
-});
diff --git a/gui/test/e2e/installed/state-dependent/disconnected.spec.ts b/gui/test/e2e/installed/state-dependent/disconnected.spec.ts
deleted file mode 100644
index 253545dad7..0000000000
--- a/gui/test/e2e/installed/state-dependent/disconnected.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { expectDisconnected } from '../../shared/tunnel-state';
-import { startInstalledApp } from '../installed-utils';
-
-// This test expects the daemon to be logged into an account that has time left and to be
-// disconnected.
-
-let page: Page;
-
-test.beforeAll(async () => {
- ({ page } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-test('App should show disconnected tunnel state', async () => {
- await expectDisconnected(page);
-});
diff --git a/gui/test/e2e/installed/state-dependent/location.spec.ts b/gui/test/e2e/installed/state-dependent/location.spec.ts
deleted file mode 100644
index f0bd2e11f7..0000000000
--- a/gui/test/e2e/installed/state-dependent/location.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { startInstalledApp } from '../installed-utils';
-
-// This test expects the daemon to be logged into an account that has time left.
-
-let page: Page;
-
-test.beforeAll(async () => {
- ({ page } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-test('App should have a country', async () => {
- const countryLabel = page.getByTestId('country');
- await expect(countryLabel).not.toBeEmpty();
-
- const cityLabel = page.getByTestId('city');
- const noCityLabel = (await cityLabel.count()) === 0;
- expect(noCityLabel).toBeTruthy();
-});
diff --git a/gui/test/e2e/installed/state-dependent/login.spec.ts b/gui/test/e2e/installed/state-dependent/login.spec.ts
deleted file mode 100644
index ad49c88edd..0000000000
--- a/gui/test/e2e/installed/state-dependent/login.spec.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { exec, execSync } from 'child_process';
-import { Locator, Page } from 'playwright';
-
-import { RoutePath } from '../../../../src/renderer/lib/routes';
-import { expectDisconnected } from '../../shared/tunnel-state';
-import { TestUtils } from '../../utils';
-import { startInstalledApp } from '../installed-utils';
-
-// This test expects the daemon to be logged out.
-// Env parameters:
-// `ACCOUNT_NUMBER`: Account number to use when logging in
-
-let page: Page;
-let util: TestUtils;
-
-let accountNumber: string;
-
-test.beforeAll(async () => {
- ({ page, util } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-test('App should fail to login', async () => {
- expect(await util.currentRoute()).toEqual(RoutePath.login);
-
- const title = page.locator('h1');
- const subtitle = page.getByTestId('subtitle');
- const loginInput = getInput(page);
-
- await expect(title).toHaveText('Login');
- await expect(subtitle).toHaveText('Enter your account number');
-
- await loginInput.fill('1234 1234 1324 1234');
- await loginInput.press('Enter');
-
- await expect(title).toHaveText('Login failed');
- await expect(subtitle).toHaveText('Invalid account number');
-
- await loginInput.fill('');
-});
-
-test('App should create account', async () => {
- expect(await util.currentRoute()).toEqual(RoutePath.login);
-
- const title = page.locator('h1');
- const subtitle = page.getByTestId('subtitle');
-
- expect(
- await util.waitForNavigation(async () => {
- await page.getByText('Create account').click();
-
- await expect(title).toHaveText('Account created');
- await expect(subtitle).toHaveText('Logged in');
- }),
- ).toEqual(RoutePath.expired);
-
- const outOfTimeTitle = page.getByTestId('title');
- await expect(outOfTimeTitle).toHaveText('Congrats!');
-
- const inputValue = await page.getByTestId('account-number').textContent();
- expect(inputValue).toHaveLength(19);
- accountNumber = inputValue!.replaceAll(' ', '');
-});
-
-test('App should become logged out', async () => {
- expect(
- await util.waitForNavigation(() => {
- exec('mullvad account logout');
- }),
- ).toEqual(RoutePath.login);
-});
-
-test('App should log in', async () => {
- expect(await util.currentRoute()).toEqual(RoutePath.login);
-
- const title = page.locator('h1');
- const subtitle = page.getByTestId('subtitle');
- const loginInput = getInput(page);
-
- await expect(title).toHaveText('Login');
- await expect(subtitle).toHaveText('Enter your account number');
-
- await loginInput.fill(process.env.ACCOUNT_NUMBER!);
-
- expect(
- await util.waitForNavigation(async () => {
- await loginInput.press('Enter');
-
- await expect(title).toHaveText('Logged in');
- await expect(subtitle).toHaveText('Valid account number');
- }),
- ).toEqual(RoutePath.main);
- await expectDisconnected(page);
-});
-
-test('App should log out', async () => {
- expect(
- await util.waitForNavigation(() => {
- void page.getByTestId('account-button').click();
- }),
- ).toEqual(RoutePath.account);
-
- expect(
- await util.waitForNavigation(() => {
- void page.getByText('Log out').click();
- }),
- ).toEqual(RoutePath.login);
-
- const title = page.locator('h1');
- const subtitle = page.getByTestId('subtitle');
- await expect(title).toHaveText('Login');
- await expect(subtitle).toHaveText('Enter your account number');
-});
-
-test('App should log in to expired account', async () => {
- expect(await util.currentRoute()).toEqual(RoutePath.login);
-
- const title = page.locator('h1');
- const subtitle = page.getByTestId('subtitle');
- const loginInput = getInput(page);
-
- await expect(title).toHaveText('Login');
- await expect(subtitle).toHaveText('Enter your account number');
-
- await loginInput.fill(accountNumber);
-
- expect(
- await util.waitForNavigation(async () => {
- await loginInput.press('Enter');
- }),
- ).toEqual(RoutePath.expired);
-
- const outOfTimeTitle = page.getByTestId('title');
- await expect(outOfTimeTitle).toHaveText('Out of time');
-
- execSync('mullvad account logout');
-});
-
-function getInput(page: Page): Locator {
- return page.getByPlaceholder('0000 0000 0000 0000');
-}
diff --git a/gui/test/e2e/installed/state-dependent/macos-split-tunneling.spec.ts b/gui/test/e2e/installed/state-dependent/macos-split-tunneling.spec.ts
deleted file mode 100644
index 6d49173bcd..0000000000
--- a/gui/test/e2e/installed/state-dependent/macos-split-tunneling.spec.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-import { expect, Locator, test } from '@playwright/test';
-import { execSync } from 'child_process';
-import { Page } from 'playwright';
-
-import { RoutePath } from '../../../../src/renderer/lib/routes';
-import { TestUtils } from '../../utils';
-import { startInstalledApp } from '../installed-utils';
-
-// macOS only. This test expects the daemon to be logged in and for split tunneling to be off and
-// have no split applications.
-
-let page: Page;
-let util: TestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-async function navigateToSplitTunneling() {
- await util.waitForNavigation(() => page.click('button[aria-label="Settings"]'));
-
- expect(await util.waitForNavigation(() => page.getByText('Split tunneling').click())).toEqual(
- RoutePath.splitTunneling,
- );
-
- const title = page.locator('h1');
- await expect(title).toHaveText('Split tunneling');
-}
-
-test('App should enable split tunneling', async () => {
- await navigateToSplitTunneling();
-
- const toggle = page.getByRole('checkbox');
- await expect(toggle).not.toBeChecked();
-
- const splitList = page.getByTestId('split-applications');
- const nonSplitList = page.getByTestId('non-split-applications');
-
- await expect(splitList).not.toBeVisible();
- await expect(nonSplitList).not.toBeVisible();
-
- const launchPadApp = page.getByText('launchpad');
- await expect(launchPadApp).not.toBeVisible();
-
- await toggle.click();
- await expect(toggle).toBeChecked();
- await expect(splitList).not.toBeVisible();
- await expect(nonSplitList).toBeVisible();
- await expect(launchPadApp).toBeVisible();
- expect(await numberOfApplicationsInList('split-applications')).toBe(0);
- expect(getDaemonSplitTunnelingApplications()).toHaveLength(0);
-});
-
-test('App should split launchpad', async () => {
- const splitList = page.getByTestId('split-applications');
- const nonSplitList = page.getByTestId('non-split-applications');
-
- const splitLaunchPadApp = splitList.getByText('launchpad');
- const nonSplitLaunchPadApp = nonSplitList.getByText('launchpad');
-
- await expect(splitLaunchPadApp).not.toBeVisible();
- await expect(nonSplitLaunchPadApp).toBeVisible();
-
- await toggleApplication(nonSplitLaunchPadApp);
-
- await expect(splitLaunchPadApp).toBeVisible();
- await expect(nonSplitLaunchPadApp).not.toBeVisible();
- expect(await numberOfApplicationsInList('split-applications')).toBe(1);
-
- const daemonSplitTunnelingApplications = getDaemonSplitTunnelingApplications();
- expect(daemonSplitTunnelingApplications).toHaveLength(1);
- expect(isSplitInDaemon('launchpad')).toBeTruthy();
-});
-
-test('App should split clock', async () => {
- const splitList = page.getByTestId('split-applications');
- const nonSplitList = page.getByTestId('non-split-applications');
-
- const splitClockApp = splitList.getByText('clock');
- const nonSplitClockApp = nonSplitList.getByText('clock');
-
- await expect(splitClockApp).not.toBeVisible();
- await expect(nonSplitClockApp).toBeVisible();
-
- await toggleApplication(nonSplitClockApp);
-
- await expect(splitClockApp).toBeVisible();
- await expect(nonSplitClockApp).not.toBeVisible();
- expect(await numberOfApplicationsInList('split-applications')).toBe(2);
-
- const daemonSplitTunnelingApplications = getDaemonSplitTunnelingApplications();
- expect(daemonSplitTunnelingApplications).toHaveLength(2);
- expect(isSplitInDaemon('launchpad')).toBeTruthy();
- expect(isSplitInDaemon('clock')).toBeTruthy();
-});
-
-test('App should unsplit launchpad', async () => {
- const splitList = page.getByTestId('split-applications');
- const nonSplitList = page.getByTestId('non-split-applications');
-
- const splitLaunchPadApp = splitList.getByText('launchpad');
- const nonSplitLaunchPadApp = nonSplitList.getByText('launchpad');
-
- await expect(splitLaunchPadApp).toBeVisible();
- await expect(nonSplitLaunchPadApp).not.toBeVisible();
-
- await toggleApplication(splitLaunchPadApp);
-
- await expect(splitLaunchPadApp).not.toBeVisible();
- await expect(nonSplitLaunchPadApp).toBeVisible();
- expect(await numberOfApplicationsInList('split-applications')).toBe(1);
-
- const daemonSplitTunnelingApplications = getDaemonSplitTunnelingApplications();
- expect(daemonSplitTunnelingApplications).toHaveLength(1);
- expect(isSplitInDaemon('launchpad')).toBeFalsy();
- expect(isSplitInDaemon('clock')).toBeTruthy();
-});
-
-test('App should disable split tunneling', async () => {
- const toggle = page.getByRole('checkbox');
- await expect(toggle).toBeChecked();
-
- const splitList = page.getByTestId('split-applications');
- const nonSplitList = page.getByTestId('non-split-applications');
-
- await expect(splitList).toBeVisible();
- await expect(nonSplitList).toBeVisible();
-
- const launchPadApp = page.getByText('launchpad');
- await expect(launchPadApp).toBeVisible();
-
- await toggle.click();
- await expect(toggle).not.toBeChecked();
-});
-
-async function toggleApplication(applicationLocator: Locator) {
- await applicationLocator.locator('~ div').click();
-}
-
-async function numberOfApplicationsInList(listTestid: string) {
- const list = page.getByTestId(listTestid);
- const listHidden = await list.isHidden();
- if (listHidden) {
- return 0;
- }
-
- return list.locator('button').count();
-}
-
-function getDaemonSplitTunnelingApplications() {
- const output = execSync('mullvad split-tunnel get').toString().trim().split('\n');
- return output.slice(output.indexOf('Excluded applications:') + 1);
-}
-
-function isSplitInDaemon(app: string): boolean {
- return !!getDaemonSplitTunnelingApplications().find((splitApp) =>
- splitApp.toLowerCase().includes(app),
- );
-}
diff --git a/gui/test/e2e/installed/state-dependent/obfuscation.spec.ts b/gui/test/e2e/installed/state-dependent/obfuscation.spec.ts
deleted file mode 100644
index b9518f8717..0000000000
--- a/gui/test/e2e/installed/state-dependent/obfuscation.spec.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { execSync } from 'child_process';
-import { Page } from 'playwright';
-
-import { colors } from '../../../../src/config.json';
-import { RoutePath } from '../../../../src/renderer/lib/routes';
-import { TestUtils } from '../../utils';
-import { startInstalledApp } from '../installed-utils';
-
-const SHADOWSOCKS_PORT = 65_000;
-const UDPOVERTCP_PORT = '80';
-
-// This test sets different obfuscation settings combinations and verifies that it was set in the
-// daemon.
-
-let page: Page;
-let util: TestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-test('App should have automatic obfuscation', async () => {
- await util.waitForNavigation(() => page.click('button[aria-label="Settings"]'));
- expect(await util.waitForNavigation(() => page.getByText('VPN settings').click())).toBe(
- RoutePath.vpnSettings,
- );
-
- expect(await util.waitForNavigation(() => page.getByText('WireGuard settings').click())).toBe(
- RoutePath.wireguardSettings,
- );
-
- const automatic = page.getByTestId('automatic-obfuscation');
- await expect(automatic).toHaveCSS('background-color', colors.green);
-
- const cliObfuscation = execSync('mullvad obfuscation get').toString().split('\n');
- expect(cliObfuscation[0]).toEqual('Obfuscation mode: auto');
- expect(cliObfuscation[1]).toEqual('udp2tcp settings: any port');
- expect(cliObfuscation[2]).toEqual('Shadowsocks settings: any port');
-});
-
-test('App should set obfuscation to shadowsocks with custom port', async () => {
- expect(
- await util.waitForNavigation(() => page.click('button[aria-label="Shadowsocks settings"]')),
- ).toBe(RoutePath.shadowsocks);
-
- const automatic = page.locator('button', { hasText: 'Automatic' });
- await expect(automatic).toHaveCSS('background-color', colors.green);
-
- const customInput = page.locator('input[type="text"]');
- await customInput.click();
- await customInput.fill(`${SHADOWSOCKS_PORT}`);
- await customInput.blur();
-
- const customItem = page.locator('div[role="option"]', { hasText: 'Custom' });
- await expect(customItem).toHaveCSS('background-color', colors.green);
-
- await util.waitForNavigation(() => page.click('button[aria-label="Back"]'));
-
- const shadowsocksItem = page.locator('button', { hasText: 'Shadowsocks' });
- await shadowsocksItem.click();
- await expect(shadowsocksItem).toHaveCSS('background-color', colors.green);
- await expect(shadowsocksItem).toContainText(`Port: ${SHADOWSOCKS_PORT}`);
-
- const cliObfuscation = execSync('mullvad obfuscation get').toString().split('\n')[2];
- expect(cliObfuscation).toEqual(`Shadowsocks settings: port ${SHADOWSOCKS_PORT}`);
-});
-
-test('App should still have shadowsocks custom port', async () => {
- expect(
- await util.waitForNavigation(() => page.click('button[aria-label="Shadowsocks settings"]')),
- ).toBe(RoutePath.shadowsocks);
-
- const customItem = page.locator('div[role="option"]', { hasText: 'Custom' });
- await expect(customItem).toHaveCSS('background-color', colors.green);
-
- await util.waitForNavigation(() => page.click('button[aria-label="Back"]'));
-});
-
-test('App should set obfuscation to UDP-over-TCP with port', async () => {
- expect(
- await util.waitForNavigation(() => page.click('button[aria-label="UDP-over-TCP settings"]')),
- ).toBe(RoutePath.udpOverTcp);
-
- const automatic = page.locator('button', { hasText: 'Automatic' });
- await expect(automatic).toHaveCSS('background-color', colors.green);
-
- const portButton = page.locator('button', { hasText: UDPOVERTCP_PORT });
- await portButton.click();
-
- await expect(portButton).toHaveCSS('background-color', colors.green);
-
- await util.waitForNavigation(() => page.click('button[aria-label="Back"]'));
-
- const udpOverTcpItem = page.locator('button', { hasText: 'UDP-over-TCP' });
- await udpOverTcpItem.click();
- await expect(udpOverTcpItem).toHaveCSS('background-color', colors.green);
- await expect(udpOverTcpItem).toContainText(`Port: ${UDPOVERTCP_PORT}`);
-
- const cliObfuscation = execSync('mullvad obfuscation get').toString().split('\n')[1];
- expect(cliObfuscation).toEqual(`udp2tcp settings: port ${UDPOVERTCP_PORT}`);
-});
diff --git a/gui/test/e2e/installed/state-dependent/settings-import.spec.ts b/gui/test/e2e/installed/state-dependent/settings-import.spec.ts
deleted file mode 100644
index d9a8859f76..0000000000
--- a/gui/test/e2e/installed/state-dependent/settings-import.spec.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { RoutePath } from '../../../../src/renderer/lib/routes';
-import { TestUtils } from '../../utils';
-import { startInstalledApp } from '../installed-utils';
-
-const INVALID_JSON = 'invalid json';
-const VALID_JSON = `
-{
- "relay_overrides": [
- {
- "hostname": "se-got-wg-001",
- "ipv4_addr_in": "127.0.0.1"
- }
- ]
-}
-`;
-
-// This test expects the daemon to be logged in.
-
-let page: Page;
-let util: TestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-async function navigateToSettingsImport() {
- await util.waitForNavigation(() => page.click('button[aria-label="Settings"]'));
- await util.waitForNavigation(() => page.getByText('VPN settings').click());
-
- expect(await util.waitForNavigation(() => page.getByText('Server IP override').click())).toEqual(
- RoutePath.settingsImport,
- );
-
- const title = page.locator('h1');
- await expect(title).toHaveText('Server IP override');
-}
-
-test('App should display no overrides', async () => {
- await navigateToSettingsImport();
- await expect(page.getByTestId('status-title')).toHaveText('NO OVERRIDES IMPORTED');
- await expect(page.getByText('Clear all overrides')).toBeDisabled();
-});
-
-test('App should fail to import text', async () => {
- expect(await util.waitForNavigation(() => page.getByText('Import via text').click())).toEqual(
- RoutePath.settingsTextImport,
- );
-
- await page.locator('textarea').fill(INVALID_JSON);
- expect(await util.waitForNavigation(() => page.click('button[aria-label="Save"]'))).toEqual(
- RoutePath.settingsImport,
- );
-
- await expect(page.getByTestId('status-title')).toHaveText('NO OVERRIDES IMPORTED');
- await expect(page.getByTestId('status-subtitle')).toBeVisible();
- await expect(page.getByText('Clear all overrides')).toBeDisabled();
- await expect(page.getByTestId('status-subtitle')).not.toBeEmpty();
-});
-
-test('App should succeed to import text', async () => {
- expect(await util.waitForNavigation(() => page.getByText('Import via text').click())).toEqual(
- RoutePath.settingsTextImport,
- );
-
- const textarea = page.locator('textarea');
- await expect(textarea).toHaveValue(INVALID_JSON);
- await textarea.fill(VALID_JSON);
- expect(await util.waitForNavigation(() => page.click('button[aria-label="Save"]'))).toEqual(
- RoutePath.settingsImport,
- );
-
- await expect(page.getByTestId('status-title')).toHaveText('IMPORT SUCCESSFUL');
- await expect(page.getByTestId('status-subtitle')).toBeVisible();
- await expect(page.getByText('Clear all overrides')).toBeEnabled();
- await expect(page.getByTestId('status-subtitle')).not.toBeEmpty();
-
- await expect(page.getByTestId('status-title')).toHaveText('OVERRIDES ACTIVE');
-
- expect(await util.waitForNavigation(() => page.getByText('Import via text').click())).toEqual(
- RoutePath.settingsTextImport,
- );
-
- await expect(textarea).toHaveValue('');
-
- expect(await util.waitForNavigation(() => page.click('button[aria-label="Close"]'))).toEqual(
- RoutePath.settingsImport,
- );
-});
-
-test('App should show active overrides', async () => {
- expect(await util.waitForNavigation(() => page.click('button[aria-label="Back"]'))).toEqual(
- RoutePath.vpnSettings,
- );
- expect(await util.waitForNavigation(() => page.getByText('Server IP override').click())).toEqual(
- RoutePath.settingsImport,
- );
-
- await expect(page.getByTestId('status-title')).toHaveText('OVERRIDES ACTIVE');
- await expect(page.getByText('Clear all overrides')).toBeEnabled();
-});
-
-test('App should clear overrides', async () => {
- await page.getByText('Clear all overrides').click();
- await expect(page.getByText('Clear all overrides?')).toBeVisible();
-
- await page.getByText(/^Clear$/).click();
- await expect(page.getByTestId('status-title')).toHaveText('NO OVERRIDES IMPORTED');
- await expect(page.getByText(/Clear all overrides$/)).toBeDisabled();
-});
diff --git a/gui/test/e2e/installed/state-dependent/settings.spec.ts b/gui/test/e2e/installed/state-dependent/settings.spec.ts
deleted file mode 100644
index 343d0fc430..0000000000
--- a/gui/test/e2e/installed/state-dependent/settings.spec.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { expect, Page, test } from '@playwright/test';
-import { execSync } from 'child_process';
-import os from 'os';
-import path from 'path';
-
-import { fileExists, TestUtils } from '../../utils';
-import { startInstalledApp } from '../installed-utils';
-
-function getAutoStartPath() {
- return path.join(os.homedir(), '.config', 'autostart', 'mullvad-vpn.desktop');
-}
-
-function autoStartPathExists() {
- return fileExists(getAutoStartPath());
-}
-
-let page: Page;
-let util: TestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-test.describe('VPN Settings', () => {
- test('Auto-connect setting', async () => {
- // Navigate to the VPN settings view
- await util.waitForNavigation(() => page.click('button[aria-label="Settings"]'));
- await util.waitForNavigation(() => page.click('text=VPN settings'));
-
- // Find the auto-connect toggle
- const autoConnectToggle = page.getByText('Auto-connect').locator('..').getByRole('checkbox');
-
- // Check initial state
- const initialCliState = execSync('mullvad auto-connect get').toString().trim();
- expect(initialCliState).toMatch(/off$/);
- await expect(autoConnectToggle).toHaveAttribute('aria-checked', 'false');
-
- // Toggle auto-connect
- await autoConnectToggle.click();
-
- // Verify the setting was applied correctly
- await expect(autoConnectToggle).toHaveAttribute('aria-checked', 'true');
- const newCliState = execSync('mullvad auto-connect get').toString().trim();
- expect(newCliState).toMatch(/off$/);
- });
-
- test('Launch on startup setting', async () => {
- // Find the launch on start-up toggle
- const launchOnStartupToggle = page
- .getByText('Launch app on start-up')
- .locator('..')
- .getByRole('checkbox');
-
- // Check initial state
- const initialCliState = execSync('mullvad auto-connect get').toString().trim();
- expect(initialCliState).toMatch(/off$/);
- await expect(launchOnStartupToggle).toHaveAttribute('aria-checked', 'false');
- if (process.platform === 'linux') {
- expect(autoStartPathExists()).toBeFalsy();
- }
-
- // Toggle launch on start-up
- await launchOnStartupToggle.click();
-
- // Verify the setting was applied correctly
- await expect(launchOnStartupToggle).toHaveAttribute('aria-checked', 'true');
- if (process.platform === 'linux') {
- expect(autoStartPathExists()).toBeTruthy();
- }
- const newCliState = execSync('mullvad auto-connect get').toString().trim();
- expect(newCliState).toMatch(/on$/);
-
- await launchOnStartupToggle.click();
-
- // Toggle auto-connect back off
- // NOTE: This must be done to clean up the auto-start file
- // TODO: Reset GUI settings between all tests
- const autoConnectToggle = page.getByText('Auto-connect').locator('..').getByRole('checkbox');
- await autoConnectToggle.click();
- });
-
- test('LAN settings', async () => {
- // Find the LAN toggle
- const lanToggle = page.getByText('Local network sharing').locator('..').getByRole('checkbox');
-
- // Check initial state
- const initialCliState = execSync('mullvad lan get').toString().trim();
- expect(initialCliState).toMatch(/block$/);
- await expect(lanToggle).toHaveAttribute('aria-checked', 'false');
-
- // Toggle LAN setting
- await lanToggle.click();
-
- // Verify the setting was applied correctly
- await expect(lanToggle).toHaveAttribute('aria-checked', 'true');
- const newState = execSync('mullvad lan get').toString().trim();
- expect(newState).toMatch(/allow$/);
- });
-});
diff --git a/gui/test/e2e/installed/state-dependent/too-many-devices.spec.ts b/gui/test/e2e/installed/state-dependent/too-many-devices.spec.ts
deleted file mode 100644
index 8ff8675b67..0000000000
--- a/gui/test/e2e/installed/state-dependent/too-many-devices.spec.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Locator, Page } from 'playwright';
-
-import { RoutePath } from '../../../../src/renderer/lib/routes';
-import { TestUtils } from '../../utils';
-import { startInstalledApp } from '../installed-utils';
-
-// This test expects the daemon to be logged out and the provided account to have five registered
-// devices..
-// Env parameters:
-// `ACCOUNT_NUMBER`: Account number to use when logging in
-
-let page: Page;
-let util: TestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-test('App should show too many devices', async () => {
- expect(await util.currentRoute()).toEqual(RoutePath.login);
-
- const loginInput = getInput(page);
- await loginInput.fill(process.env.ACCOUNT_NUMBER!);
-
- expect(await util.waitForNavigation(() => loginInput.press('Enter'))).toEqual(
- RoutePath.tooManyDevices,
- );
-
- const loginButton = page.getByText('Continue with login');
-
- await expect(page.getByTestId('title')).toHaveText('Too many devices');
- await expect(loginButton).toBeDisabled();
- await page
- .getByLabel(/^Remove device named/)
- .first()
- .click();
- await page.getByText('Yes, log out device').click();
-
- await expect(loginButton).toBeEnabled();
-
- // Trigger transition: too-many-devices -> login -> main
- expect(await util.waitForNavigation(() => loginButton.click())).toEqual(RoutePath.login);
-
- // Note: `util.waitForNavigation` won't return the navigation event when
- // transitioning from login -> main, so we need to observe the state of the
- // app after the entire transition chain has finished.
- await util.waitForNoTransition();
- await expect(page.getByTestId(RoutePath.main)).toBeVisible();
-});
-
-function getInput(page: Page): Locator {
- return page.getByPlaceholder('0000 0000 0000 0000');
-}
diff --git a/gui/test/e2e/installed/state-dependent/tunnel-state.spec.ts b/gui/test/e2e/installed/state-dependent/tunnel-state.spec.ts
deleted file mode 100644
index 15ce240e57..0000000000
--- a/gui/test/e2e/installed/state-dependent/tunnel-state.spec.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { exec as execAsync } from 'child_process';
-import { Page } from 'playwright';
-import { promisify } from 'util';
-
-import { expectConnected, expectDisconnected, expectError } from '../../shared/tunnel-state';
-import { escapeRegExp } from '../../utils';
-import { startInstalledApp } from '../installed-utils';
-
-const exec = promisify(execAsync);
-
-// This test expects the daemon to be logged into an account that has time left and to be
-// disconnected. Env parameters:
-// HOSTNAME: hostname of the currently selected WireGuard relay
-// IN_IP: In ip of the relay passed in `HOSTNAME`
-// CONNECTION_CHECK_URL: Url to the connection check
-
-let page: Page;
-
-test.beforeAll(async () => {
- ({ page } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-test('App should show disconnected tunnel state', async () => {
- await expectDisconnected(page);
-});
-
-test('App should connect', async () => {
- await page.getByText('Connect', { exact: true }).click();
- await expectConnected(page);
-
- const relay = page.getByTestId('hostname-line');
- const inIp = page.locator(':text("In") + span');
- const outIp = page.locator(':text("Out") + div > span');
-
- await expect(relay).toHaveText(process.env.HOSTNAME!);
- await expect(inIp).not.toBeVisible();
- await relay.click();
-
- await expect(inIp).toBeVisible();
- expect(await inIp.textContent()).toMatch(new RegExp(`^${process.env.IN_IP!}`));
-
- await expect(outIp).toBeVisible();
-
- const ipResponse = await fetch(`${process.env.CONNECTION_CHECK_URL!}/ip`);
- const ip = await ipResponse.text();
-
- expect(await outIp.textContent()).toBe(ip.trim());
-});
-
-test('App should show correct WireGuard port', async () => {
- const inData = page.getByTestId('in-ip');
-
- await expect(inData).toContainText(new RegExp(':[0-9]+'));
-
- await exec('mullvad obfuscation set mode off');
- await exec('mullvad relay set tunnel wireguard --port=53');
- await expectConnected(page);
- await page.getByTestId('connection-panel-chevron').click();
- await expect(inData).toContainText(new RegExp(':53'));
-
- await exec('mullvad relay set tunnel wireguard --port=51820');
- await expectConnected(page);
- await page.getByTestId('connection-panel-chevron').click();
- await expect(inData).toContainText(new RegExp(':51820'));
-
- await exec('mullvad relay set tunnel wireguard --port=any');
- await exec('mullvad obfuscation set mode auto');
-});
-
-test('App should show correct WireGuard transport protocol', async () => {
- const inData = page.getByTestId('in-ip');
-
- await exec('mullvad obfuscation set mode udp2tcp');
- await expectConnected(page);
- await page.getByTestId('connection-panel-chevron').click();
- await expect(inData).toContainText(new RegExp('TCP'));
-
- await exec('mullvad obfuscation set mode off');
- await expectConnected(page);
- await page.getByTestId('connection-panel-chevron').click();
- await expect(inData).toContainText(new RegExp('UDP$'));
-});
-
-test('App should connect with Shadowsocks', async () => {
- await exec('mullvad obfuscation set mode shadowsocks');
- await expectConnected(page);
- await exec('mullvad obfuscation set mode off');
- await expectConnected(page);
-});
-
-test('App should show correct tunnel protocol', async () => {
- const tunnelProtocol = page.getByTestId('tunnel-protocol');
- await expect(tunnelProtocol).toHaveText('WireGuard');
-
- await exec('mullvad relay set tunnel-protocol openvpn');
- await exec('mullvad relay set location se');
- await expectConnected(page);
- await page.getByTestId('connection-panel-chevron').click();
- await expect(tunnelProtocol).toHaveText('OpenVPN');
-});
-
-test('App should show correct OpenVPN transport protocol and port', async () => {
- const inData = page.getByTestId('in-ip');
-
- await expect(inData).toContainText(new RegExp(':[0-9]+'));
- await expect(inData).toContainText(new RegExp('(TCP|UDP)$'));
- await exec('mullvad relay set tunnel openvpn --transport-protocol udp --port 1195');
- await expectConnected(page);
- await page.getByTestId('connection-panel-chevron').click();
- await expect(inData).toContainText(new RegExp(':1195'));
-
- await exec('mullvad relay set tunnel openvpn --transport-protocol udp --port 1300');
- await expectConnected(page);
- await page.getByTestId('connection-panel-chevron').click();
- await expect(inData).toContainText(new RegExp(':1300'));
-
- await exec('mullvad relay set tunnel openvpn --transport-protocol tcp --port any');
- await expectConnected(page);
- await page.getByTestId('connection-panel-chevron').click();
- await expect(inData).toContainText(new RegExp(':[0-9]+'));
- await expect(inData).toContainText(new RegExp('TCP$'));
-
- await exec('mullvad relay set tunnel openvpn --transport-protocol tcp --port 80');
- await expectConnected(page);
- await page.getByTestId('connection-panel-chevron').click();
- await expect(inData).toContainText(new RegExp(':80'));
-
- await exec('mullvad relay set tunnel openvpn --transport-protocol tcp --port 443');
- await expectConnected(page);
- await page.getByTestId('connection-panel-chevron').click();
- await expect(inData).toContainText(new RegExp(':443'));
-
- await exec('mullvad relay set tunnel openvpn --transport-protocol any');
-});
-
-test('App should show bridge mode', async () => {
- await exec('mullvad bridge set state on');
- await expectConnected(page);
- const relay = page.getByTestId('hostname-line');
- await expect(relay).toHaveText(new RegExp(' via ', 'i'));
- await exec('mullvad bridge set state off');
-
- await exec('mullvad relay set tunnel-protocol wireguard');
-});
-
-test('App should enter blocked state', async () => {
- await exec('mullvad debug block-connection');
- await expectError(page);
-
- await exec(`mullvad relay set location ${process.env.HOSTNAME}`);
- await expectConnected(page);
-});
-
-test('App should show multihop', async () => {
- await exec('mullvad relay set tunnel wireguard --use-multihop=on');
- await expectConnected(page);
- const relay = page.getByTestId('hostname-line');
- await expect(relay).toHaveText(
- new RegExp('^' + escapeRegExp(`${process.env.HOSTNAME} via`), 'i'),
- );
- await exec('mullvad relay set tunnel wireguard --use-multihop=off');
- await page.getByText('Disconnect').click();
-});
-
-test('App should disconnect', async () => {
- await page.getByText('Disconnect').click();
- await expectDisconnected(page);
-});
-
-test('App should become connected when other frontend connects', async () => {
- await expectDisconnected(page);
- await exec('mullvad connect');
- await expectConnected(page);
-
- await exec('mullvad disconnect');
- await expectDisconnected(page);
-});
diff --git a/gui/test/e2e/mocked/expired-account-error-view.spec.ts b/gui/test/e2e/mocked/expired-account-error-view.spec.ts
deleted file mode 100644
index e63e62f51f..0000000000
--- a/gui/test/e2e/mocked/expired-account-error-view.spec.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { colors } from '../../../src/config.json';
-import { RoutePath } from '../../../src/renderer/lib/routes';
-import { IAccountData } from '../../../src/shared/daemon-rpc-types';
-import { getBackgroundColor } from '../utils';
-import { MockedTestUtils, startMockedApp } from './mocked-utils';
-
-let page: Page;
-let util: MockedTestUtils;
-
-test.beforeEach(async () => {
- ({ page, util } = await startMockedApp());
-});
-
-test.afterEach(async () => {
- await page.close();
-});
-
-test('App should show Expired Account Error View', async () => {
- await util.sendMockIpcResponse<IAccountData>({
- channel: 'account-',
- response: { expiry: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString() },
- });
-
- await expect(page.locator('text=Out of time')).toBeVisible();
- const buyMoreButton = page.locator('button:has-text("Buy more credit")');
- await expect(buyMoreButton).toBeVisible();
- expect(await getBackgroundColor(buyMoreButton)).toBe(colors.green);
-
- const redeemVoucherButton = page.locator('button:has-text("Redeem voucher")');
- await expect(redeemVoucherButton).toBeVisible();
- expect(await getBackgroundColor(redeemVoucherButton)).toBe(colors.green);
-});
-
-test('App should show out of time view after running out of time', async () => {
- const expiryDate = new Date();
- expiryDate.setSeconds(expiryDate.getSeconds() + 2);
-
- expect(
- await util.waitForNavigation(async () => {
- await util.sendMockIpcResponse<IAccountData>({
- channel: 'account-',
- response: { expiry: expiryDate.toISOString() },
- });
- }),
- ).toEqual(RoutePath.expired);
-});
diff --git a/gui/test/e2e/mocked/feature-indicators.spec.ts b/gui/test/e2e/mocked/feature-indicators.spec.ts
deleted file mode 100644
index 6e0e034c35..0000000000
--- a/gui/test/e2e/mocked/feature-indicators.spec.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import {
- FeatureIndicator,
- ILocation,
- ITunnelEndpoint,
- TunnelState,
-} from '../../../src/shared/daemon-rpc-types';
-import { expectConnected } from '../shared/tunnel-state';
-import { MockedTestUtils, startMockedApp } from './mocked-utils';
-
-const endpoint: ITunnelEndpoint = {
- address: 'wg10:80',
- protocol: 'tcp',
- quantumResistant: false,
- tunnelType: 'wireguard',
- daita: false,
-};
-
-const mockDisconnectedLocation: ILocation = {
- country: 'Sweden',
- city: 'Gothenburg',
- latitude: 58,
- longitude: 12,
- mullvadExitIp: false,
-};
-
-const mockConnectedLocation: ILocation = { ...mockDisconnectedLocation, mullvadExitIp: true };
-
-let page: Page;
-let util: MockedTestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startMockedApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-test('App should show no feature indicators', async () => {
- await util.mockIpcHandle<ILocation>({
- channel: 'location-get',
- response: mockDisconnectedLocation,
- });
- await util.sendMockIpcResponse<TunnelState>({
- channel: 'tunnel-',
- response: {
- state: 'connected',
- details: { endpoint, location: mockConnectedLocation },
- featureIndicators: undefined,
- },
- });
-
- await expectConnected(page);
- await expectFeatureIndicators(page, []);
-
- const ellipsis = page.getByText(/^\d more.../);
- await expect(ellipsis).not.toBeVisible();
-
- await page.getByTestId('connection-panel-chevron').click();
- await expect(ellipsis).not.toBeVisible();
-
- await expectFeatureIndicators(page, []);
- await page.getByTestId('connection-panel-chevron').click();
-});
-
-test('App should show feature indicators', async () => {
- await util.mockIpcHandle<ILocation>({
- channel: 'location-get',
- response: mockDisconnectedLocation,
- });
- await util.sendMockIpcResponse<TunnelState>({
- channel: 'tunnel-',
- response: {
- state: 'connected',
- details: { endpoint, location: mockConnectedLocation },
- featureIndicators: [
- FeatureIndicator.daita,
- FeatureIndicator.udp2tcp,
- FeatureIndicator.customMssFix,
- FeatureIndicator.customMtu,
- FeatureIndicator.lanSharing,
- FeatureIndicator.serverIpOverride,
- FeatureIndicator.customDns,
- FeatureIndicator.lockdownMode,
- FeatureIndicator.quantumResistance,
- FeatureIndicator.multihop,
- ],
- },
- });
-
- // Make sure panel is collapsed before checking indicator visibility.
- const ellipsis = page.getByText(/^\d more.../);
- await expect(ellipsis).toBeVisible();
-
- await expectConnected(page);
- await expectFeatureIndicators(page, ['DAITA', 'Quantum resistance'], false);
- await expectHiddenFeatureIndicator(page, 'Mssfix');
-
- await page.getByTestId('connection-panel-chevron').click();
- await expect(ellipsis).not.toBeVisible();
-
- await expectFeatureIndicators(page, [
- 'DAITA',
- 'Quantum resistance',
- 'Mssfix',
- 'MTU',
- 'Obfuscation',
- 'Local network sharing',
- 'Lockdown mode',
- 'Multihop',
- 'Custom DNS',
- 'Server IP override',
- ]);
-});
-
-async function expectHiddenFeatureIndicator(page: Page, hiddenIndicator: string) {
- const indicators = page.getByTestId('feature-indicator');
- const indicator = indicators.getByText(hiddenIndicator, { exact: true });
-
- // Make sure at least one is visible to not run the "not visible" check before they become
- // visible.
- await expect(indicators.first()).toBeVisible();
-
- await expect(indicator).toHaveCount(1);
- await expect(indicator).not.toBeVisible();
-}
-
-async function expectFeatureIndicators(page: Page, expectedIndicators: Array<string>, only = true) {
- const indicators = page.getByTestId('feature-indicator');
- if (only) {
- await expect(indicators).toHaveCount(expectedIndicators.length);
- }
-
- for (const indicator of expectedIndicators) {
- await expect(indicators.getByText(indicator, { exact: true })).toBeVisible();
- }
-}
diff --git a/gui/test/e2e/mocked/main.spec.ts b/gui/test/e2e/mocked/main.spec.ts
deleted file mode 100644
index cfd6d26302..0000000000
--- a/gui/test/e2e/mocked/main.spec.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { startMockedApp } from './mocked-utils';
-
-let page: Page;
-
-test.beforeAll(async () => {
- ({ page } = await startMockedApp());
-});
-
-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
deleted file mode 100644
index 2ae14f8c28..0000000000
--- a/gui/test/e2e/mocked/mocked-utils.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { ElectronApplication } from 'playwright';
-
-import { startApp, TestUtils } from '../utils';
-
-// This option can be removed in the future when/if we're able to tun the tests with the sandbox
-// enabled in GitHub actions (frontend.yml).
-const noSandbox = process.env.NO_SANDBOX === '1';
-
-interface StartMockedAppResponse extends Awaited<ReturnType<typeof startApp>> {
- util: MockedTestUtils;
-}
-
-export interface MockedTestUtils extends TestUtils {
- mockIpcHandle: MockIpcHandle;
- sendMockIpcResponse: SendMockIpcResponse;
-}
-
-export const startMockedApp = async (): Promise<StartMockedAppResponse> => {
- const args = ['build/test/e2e/setup/main.js'];
- if (noSandbox) {
- console.log('Running tests without chromium sandbox');
- args.unshift('--no-sandbox');
- }
-
- const startAppResult = await startApp({ args });
- const mockIpcHandle = generateMockIpcHandle(startAppResult.app);
- const sendMockIpcResponse = generateSendMockIpcResponse(startAppResult.app);
-
- return {
- ...startAppResult,
- util: {
- ...startAppResult.util,
- 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 generateSendMockIpcResponse>;
-
-export const generateSendMockIpcResponse = (electronApp: ElectronApplication) => {
- return async <T>({ channel, response }: SendMockIpcResponseProps<T>) => {
- await electronApp.evaluate(
- ({ webContents }, { channel, response }) => {
- webContents
- .getAllWebContents()
- // Select window that isn't devtools
- .find((webContents) => webContents.getURL().startsWith('file://'))!
- .send(channel, response);
- },
- { channel, response },
- );
- };
-};
diff --git a/gui/test/e2e/mocked/notifications.spec.ts b/gui/test/e2e/mocked/notifications.spec.ts
deleted file mode 100644
index 7af0d1297a..0000000000
--- a/gui/test/e2e/mocked/notifications.spec.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { colors } from '../../../src/config.json';
-import { IAccountData } from '../../../src/shared/daemon-rpc-types';
-import { getBackgroundColor } from '../utils';
-import { MockedTestUtils, startMockedApp } from './mocked-utils';
-
-let page: Page;
-let util: MockedTestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startMockedApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-/**
- * Expires soon
- */
-test('App should notify user about account expiring soon', async () => {
- await util.sendMockIpcResponse<IAccountData>({
- channel: 'account-',
- response: { expiry: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString() },
- });
-
- const title = page.getByTestId('notificationTitle');
- await expect(title).toContainText(/account credit expires soon/i);
-
- let subTitle = page.getByTestId('notificationSubTitle');
- await expect(subTitle).toContainText(/1 day left\. buy more credit\./i);
-
- const indicator = page.getByTestId('notificationIndicator');
- const indicatorColor = await getBackgroundColor(indicator);
- expect(indicatorColor).toBe(colors.yellow);
-
- await util.sendMockIpcResponse<IAccountData>({
- channel: 'account-',
- response: { expiry: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString() },
- });
- subTitle = page.getByTestId('notificationSubTitle');
- await expect(subTitle).toContainText(/2 days left\. buy more credit\./i);
-
- await util.sendMockIpcResponse<IAccountData>({
- channel: 'account-',
- response: { expiry: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000).toISOString() },
- });
- subTitle = page.getByTestId('notificationSubTitle');
- await expect(subTitle).toContainText(/less than a day left\. buy more credit\./i);
-});
diff --git a/gui/test/e2e/mocked/select-location.spec.ts b/gui/test/e2e/mocked/select-location.spec.ts
deleted file mode 100644
index 342d511b9e..0000000000
--- a/gui/test/e2e/mocked/select-location.spec.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { colors } from '../../../src/config.json';
-import { getDefaultSettings } from '../../../src/main/default-settings';
-import {
- IRelayList,
- IRelayListWithEndpointData,
- ISettings,
- IWireguardEndpointData,
-} from '../../../src/shared/daemon-rpc-types';
-import { MockedTestUtils, startMockedApp } from './mocked-utils';
-
-const relayList: IRelayList = {
- countries: [
- {
- name: 'Sweden',
- code: 'se',
- cities: [
- {
- name: 'Gothenburg',
- code: 'got',
- latitude: 58,
- longitude: 12,
- relays: [
- {
- hostname: 'se-got-wg-101',
- provider: 'mullvad',
- ipv4AddrIn: '10.0.0.1',
- includeInCountry: true,
- active: true,
- weight: 0,
- owned: true,
- endpointType: 'wireguard',
- daita: true,
- },
- {
- hostname: 'se-got-wg-102',
- provider: 'mullvad',
- ipv4AddrIn: '10.0.0.2',
- includeInCountry: true,
- active: true,
- weight: 0,
- owned: true,
- endpointType: 'wireguard',
- daita: true,
- },
- ],
- },
- ],
- },
- ],
-};
-
-const wireguardEndpointData: IWireguardEndpointData = {
- portRanges: [],
- udp2tcpPorts: [],
-};
-
-let page: Page;
-let util: MockedTestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startMockedApp());
- await setMultihop();
- await util.waitForNavigation(() => page.getByLabel('Select location').click());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-async function setMultihop() {
- const settings = getDefaultSettings();
- if ('normal' in settings.relaySettings) {
- settings.relaySettings.normal.wireguardConstraints.useMultihop = true;
- }
-
- await util.sendMockIpcResponse<ISettings>({
- channel: 'settings-',
- response: settings,
- });
-
- await util.sendMockIpcResponse<IRelayListWithEndpointData>({
- channel: 'relays-',
- response: { relayList, wireguardEndpointData },
- });
-}
-
-test('App should show entry selection', async () => {
- const entryTab = page.getByText('Entry');
- await entryTab.click();
- await expect(entryTab).toHaveCSS('background-color', colors.green);
-
- const sweden = page.getByText('Sweden');
- await expect(sweden).toBeVisible();
-});
-
-test('App should show exit selection', async () => {
- const exitTab = page.getByText('Exit');
- await exitTab.click();
- await expect(exitTab).toHaveCSS('background-color', colors.green);
-
- const sweden = page.getByText('Sweden');
- await expect(sweden).toBeVisible();
-});
-
-test("App shouldn't show entry selection when daita is enabled without direct only", async () => {
- const settings = getDefaultSettings();
- if ('normal' in settings.relaySettings && settings.tunnelOptions.wireguard.daita) {
- settings.relaySettings.normal.wireguardConstraints.useMultihop = true;
- settings.tunnelOptions.wireguard.daita.enabled = true;
- settings.tunnelOptions.wireguard.daita.directOnly = false;
- }
-
- await util.sendMockIpcResponse<ISettings>({
- channel: 'settings-',
- response: settings,
- });
-
- const entryTab = page.getByText('Entry');
- await entryTab.click();
- await expect(entryTab).toHaveCSS('background-color', colors.green);
-
- const sweden = page.getByText('Sweden');
- await expect(sweden).not.toBeVisible();
-});
-
-test('App should show entry selection when daita is enabled with direct only', async () => {
- const settings = getDefaultSettings();
- if ('normal' in settings.relaySettings && settings.tunnelOptions.wireguard.daita) {
- settings.relaySettings.normal.wireguardConstraints.useMultihop = true;
- settings.tunnelOptions.wireguard.daita.enabled = true;
- settings.tunnelOptions.wireguard.daita.directOnly = true;
- }
-
- await util.sendMockIpcResponse<ISettings>({
- channel: 'settings-',
- response: settings,
- });
-
- const entryTab = page.getByText('Entry');
- await entryTab.click();
- await expect(entryTab).toHaveCSS('background-color', colors.green);
-
- const sweden = page.getByText('Sweden');
- await expect(sweden).toBeVisible();
-});
diff --git a/gui/test/e2e/mocked/settings.spec.ts b/gui/test/e2e/mocked/settings.spec.ts
deleted file mode 100644
index 52dbc72402..0000000000
--- a/gui/test/e2e/mocked/settings.spec.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { IAccountData } from '../../../src/shared/daemon-rpc-types';
-import { MockedTestUtils, startMockedApp } from './mocked-utils';
-
-let page: Page;
-let util: MockedTestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startMockedApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-test('Account button should be displayed correctly', async () => {
- const accountButton = page.getByLabel('Account settings');
- await expect(accountButton).toBeVisible();
-});
-
-test('Headerbar account info should be displayed correctly', async () => {
- const expiryText = page.getByText(/^Time left:/);
- await expect(expiryText).toContainText(/Time left: 29 days/i);
-
- /**
- * 729 days left
- * Add a one-second margin to the test, since it randomly fails in Github Actions otherwise
- */
- await util.sendMockIpcResponse<IAccountData>({
- channel: 'account-',
- response: { expiry: new Date(Date.now() + 730 * 24 * 60 * 60 * 1000 - 1000).toISOString() },
- });
- await expect(expiryText).toContainText(/Time left: 729 days/i);
-
- /**
- * 2 years left
- */
- await util.sendMockIpcResponse<IAccountData>({
- channel: 'account-',
- response: { expiry: new Date(Date.now() + 731 * 24 * 60 * 60 * 1000).toISOString() },
- });
- await expect(expiryText).toContainText(/Time left: 2 years/i);
-
- /**
- * Expiry 1 day ago should show 'out of time'
- */
- await util.sendMockIpcResponse<IAccountData>({
- channel: 'account-',
- response: { expiry: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString() },
- });
- await expect(expiryText).not.toBeVisible();
-});
-
-test('Settings Page', async () => {
- await util.waitForNavigation(() => page.click('button[aria-label="Settings"]'));
-
- const title = page.locator('h1');
- await expect(title).toContainText('Settings');
-
- const closeButton = page.locator('button[aria-label="Close"]');
- await expect(closeButton).toBeVisible();
-});
diff --git a/gui/test/e2e/mocked/tunnel-state.spec.ts b/gui/test/e2e/mocked/tunnel-state.spec.ts
deleted file mode 100644
index 6ecf707ba6..0000000000
--- a/gui/test/e2e/mocked/tunnel-state.spec.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import { test } from '@playwright/test';
-import { Page } from 'playwright';
-
-import {
- ErrorStateCause,
- ILocation,
- ITunnelEndpoint,
- TunnelState,
-} from '../../../src/shared/daemon-rpc-types';
-import {
- expectConnected,
- expectConnecting,
- expectDisconnected,
- expectDisconnecting,
- expectError,
-} from '../shared/tunnel-state';
-import { MockedTestUtils, startMockedApp } from './mocked-utils';
-
-const mockLocation: ILocation = {
- country: 'Sweden',
- city: 'Gothenburg',
- latitude: 58,
- longitude: 12,
- mullvadExitIp: false,
-};
-
-let page: Page;
-let util: MockedTestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startMockedApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-/**
- * Disconnected state
- */
-test('App should show disconnected tunnel state', async () => {
- await util.mockIpcHandle<ILocation>({
- channel: 'location-get',
- response: mockLocation,
- });
- await util.sendMockIpcResponse<TunnelState>({
- channel: 'tunnel-',
- response: { state: 'disconnected' },
- });
- await expectDisconnected(page);
-});
-
-/**
- * Connecting state
- */
-test('App should show connecting tunnel state', async () => {
- await util.mockIpcHandle<ILocation>({
- channel: 'location-get',
- response: mockLocation,
- });
- await util.sendMockIpcResponse<TunnelState>({
- channel: 'tunnel-',
- response: { state: 'connecting', featureIndicators: undefined },
- });
- await expectConnecting(page);
-});
-
-/**
- * Connected state
- */
-test('App should show connected tunnel state', async () => {
- const location: ILocation = { ...mockLocation, mullvadExitIp: true };
- await util.mockIpcHandle<ILocation>({
- channel: 'location-get',
- response: location,
- });
-
- const endpoint: ITunnelEndpoint = {
- address: 'wg10:80',
- protocol: 'tcp',
- quantumResistant: false,
- tunnelType: 'wireguard',
- daita: false,
- };
- await util.sendMockIpcResponse<TunnelState>({
- channel: 'tunnel-',
- response: { state: 'connected', details: { endpoint, location }, featureIndicators: undefined },
- });
-
- await expectConnected(page);
-});
-
-/**
- * Disconnecting state
- */
-test('App should show disconnecting tunnel state', async () => {
- await util.mockIpcHandle<ILocation>({
- channel: 'location-get',
- response: mockLocation,
- });
- await util.sendMockIpcResponse<TunnelState>({
- channel: 'tunnel-',
- response: { state: 'disconnecting', details: 'nothing' },
- });
- await expectDisconnecting(page);
-});
-
-/**
- * Error state
- */
-test('App should show error tunnel state', async () => {
- await util.mockIpcHandle<ILocation>({
- channel: 'location-get',
- response: mockLocation,
- });
- await util.sendMockIpcResponse<TunnelState>({
- channel: 'tunnel-',
- response: { state: 'error', details: { cause: ErrorStateCause.isOffline } },
- });
- await expectError(page);
-});
diff --git a/gui/test/e2e/setup/main.ts b/gui/test/e2e/setup/main.ts
deleted file mode 100644
index d8f3ceee48..0000000000
--- a/gui/test/e2e/setup/main.ts
+++ /dev/null
@@ -1,204 +0,0 @@
-import { app, BrowserWindow } from 'electron';
-import * as path from 'path';
-
-import { getDefaultSettings } from '../../../src/main/default-settings';
-import { changeIpcWebContents, IpcMainEventChannel } from '../../../src/main/ipc-event-channel';
-import { loadTranslations } from '../../../src/main/load-translations';
-import {
- DeviceState,
- IAccountData,
- IAppVersionInfo,
- ILocation,
- IRelayList,
- IWireguardEndpointData,
-} from '../../../src/shared/daemon-rpc-types';
-import { messages, relayLocations } from '../../../src/shared/gettext';
-import { IGuiSettingsState } from '../../../src/shared/gui-settings-state';
-import { ITranslations, MacOsScrollbarVisibility } from '../../../src/shared/ipc-schema';
-import { ICurrentAppVersionInfo } from '../../../src/shared/ipc-types';
-
-const DEBUG = false;
-
-class ApplicationMain {
- private guiSettings: IGuiSettingsState = {
- preferredLocale: 'en',
- autoConnect: false,
- enableSystemNotifications: true,
- monochromaticIcon: false,
- startMinimized: false,
- unpinnedWindow: process.platform !== 'win32' && process.platform !== 'darwin',
- browsedForSplitTunnelingApplications: [],
- changelogDisplayedForVersion: '',
- animateMap: true,
- };
-
- private settings = getDefaultSettings();
-
- private translations: ITranslations = { locale: this.guiSettings.preferredLocale };
-
- private isConnectedToDaemon = true;
-
- private accountData: IAccountData = {
- expiry: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
- };
-
- private deviceState: DeviceState = {
- type: 'logged in',
- accountAndDevice: {
- accountNumber: '1234123412341234',
- device: {
- id: '1234',
- name: 'Testing Mole',
- created: new Date(),
- },
- },
- };
-
- private currentVersion: ICurrentAppVersionInfo = {
- gui: '2000.1',
- daemon: '2000.1',
- isConsistent: true,
- isBeta: false,
- };
- private upgradeVersion: IAppVersionInfo = {
- supported: true,
- suggestedUpgrade: undefined,
- };
-
- private location: ILocation = {
- country: 'Sweden',
- city: 'Gothenburg',
- latitude: 58,
- longitude: 12,
- mullvadExitIp: false,
- };
-
- private relayList: IRelayList = {
- countries: [
- {
- name: 'Sweden',
- code: 'se',
- cities: [
- {
- name: 'Gothenburg',
- code: 'got',
- latitude: 58,
- longitude: 12,
- relays: [
- {
- hostname: 'se-got-wg-101',
- provider: 'mullvad',
- ipv4AddrIn: '127.0.0.1',
- includeInCountry: true,
- active: true,
- weight: 0,
- owned: true,
- endpointType: 'wireguard',
- daita: false,
- },
- ],
- },
- ],
- },
- ],
- };
-
- private wireguardEndpointData: IWireguardEndpointData = {
- portRanges: [],
- udp2tcpPorts: [],
- };
-
- public constructor() {
- app.enableSandbox();
- app.on('ready', this.onReady);
- }
-
- private onReady = async () => {
- this.updateCurrentLocale('en');
-
- const window = new BrowserWindow({
- useContentSize: true,
- width: 320,
- height: 568,
- resizable: false,
- maximizable: false,
- fullscreenable: false,
- show: DEBUG,
- frame: true,
- webPreferences: {
- preload: path.join(__dirname, '../../../src/renderer/preloadBundle.js'),
- nodeIntegration: false,
- nodeIntegrationInWorker: false,
- nodeIntegrationInSubFrames: false,
- sandbox: true,
- contextIsolation: true,
- spellcheck: false,
- devTools: DEBUG,
- },
- });
-
- changeIpcWebContents(window.webContents);
-
- this.registerIpcListeners();
-
- const filePath = path.resolve(path.join(__dirname, '../../../src/renderer/index.html'));
- await window.loadFile(filePath);
-
- if (DEBUG) {
- window.webContents.openDevTools({ mode: 'detach' });
- }
- };
-
- private registerIpcListeners() {
- IpcMainEventChannel.state.handleGet(() => ({
- isConnected: this.isConnectedToDaemon,
- autoStart: false,
- accountData: this.accountData,
- accountHistory: undefined,
- tunnelState: { state: 'disconnected', location: this.location },
- settings: this.settings,
- isPerformingPostUpgrade: false,
- deviceState: this.deviceState,
- relayListPair: {
- relays: this.relayList,
- bridges: this.relayList,
- wireguardEndpointData: this.wireguardEndpointData,
- },
- currentVersion: this.currentVersion,
- upgradeVersion: this.upgradeVersion,
- guiSettings: this.guiSettings,
- translations: this.translations,
- splitTunnelingApplications: [],
- macOsScrollbarVisibility: MacOsScrollbarVisibility.whenScrolling,
- changelog: [],
- forceShowChanges: false,
- navigationHistory: undefined,
- scrollPositions: {},
- isMacOs13OrNewer: true,
- }));
-
- IpcMainEventChannel.guiSettings.handleSetPreferredLocale((locale) => {
- this.updateCurrentLocale(locale);
- IpcMainEventChannel.guiSettings.notify?.(this.guiSettings);
- return Promise.resolve(this.translations);
- });
- }
-
- private updateCurrentLocale(locale: string) {
- this.guiSettings.preferredLocale = locale;
-
- const messagesTranslations = loadTranslations(this.guiSettings.preferredLocale, messages);
- const relayLocationsTranslations = loadTranslations(
- this.guiSettings.preferredLocale,
- relayLocations,
- );
-
- this.translations = {
- locale: this.guiSettings.preferredLocale,
- messages: messagesTranslations,
- relayLocations: relayLocationsTranslations,
- };
- }
-}
-
-new ApplicationMain();
diff --git a/gui/test/e2e/shared/tunnel-state.ts b/gui/test/e2e/shared/tunnel-state.ts
deleted file mode 100644
index 6a5fbfaf09..0000000000
--- a/gui/test/e2e/shared/tunnel-state.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { expect } from '@playwright/test';
-import { Page } from 'playwright';
-
-import { colors } from '../../../src/config.json';
-import { anyOf } from '../utils';
-
-const DISCONNECTED_COLOR = colors.red;
-const CONNECTED_COLOR = colors.green;
-const WHITE_COLOR = colors.white;
-
-const DISCONNECTED_BUTTON_COLOR = anyOf(colors.red, colors.red80);
-const DISCONNECTING_BUTTON_COLOR = anyOf(colors.green40);
-const CONNECTED_BUTTON_COLOR = anyOf(colors.green, colors.green90);
-
-const getLabel = (page: Page) => page.locator('span[role="status"]');
-const getHeader = (page: Page) => page.locator('header');
-
-export async function expectDisconnected(page: Page) {
- await expectTunnelState(page, {
- labelText: 'disconnected',
- labelColor: DISCONNECTED_COLOR,
- headerColor: DISCONNECTED_COLOR,
- buttonText: 'connect',
- buttonColor: CONNECTED_BUTTON_COLOR,
- });
-}
-
-export async function expectConnecting(page: Page) {
- await expectTunnelState(page, {
- labelText: 'connecting',
- labelColor: WHITE_COLOR,
- headerColor: CONNECTED_COLOR,
- buttonText: 'cancel',
- buttonColor: DISCONNECTED_BUTTON_COLOR,
- });
-}
-
-export async function expectConnected(page: Page) {
- await expectTunnelState(page, {
- labelText: 'connected',
- labelColor: CONNECTED_COLOR,
- headerColor: CONNECTED_COLOR,
- buttonText: 'disconnect',
- buttonColor: DISCONNECTED_BUTTON_COLOR,
- });
-}
-
-export async function expectDisconnecting(page: Page) {
- await expectTunnelState(page, {
- labelText: 'disconnecting',
- labelColor: WHITE_COLOR,
- headerColor: DISCONNECTED_COLOR,
- buttonText: 'connect',
- buttonColor: DISCONNECTING_BUTTON_COLOR,
- });
-}
-
-export async function expectError(page: Page) {
- await expectTunnelState(page, {
- labelText: 'blocked connection',
- labelColor: WHITE_COLOR,
- headerColor: CONNECTED_COLOR,
- });
-}
-
-interface TunnelStateContent {
- labelText?: string | RegExp;
- labelColor?: string;
- headerColor: string;
- buttonText?: string;
- buttonColor?: string | RegExp;
-}
-
-export async function expectTunnelState(page: Page, content: TunnelStateContent) {
- const statusLabel = getLabel(page);
- if (content.labelText && content.labelColor) {
- await expect(statusLabel).toContainText(new RegExp(content.labelText, 'i'));
- await expect(statusLabel).toHaveCSS('color', content.labelColor);
- } else {
- await expect(statusLabel).toBeEmpty();
- }
-
- const header = getHeader(page);
- await expect(header).toHaveCSS('background-color', content.headerColor);
-
- if (content.buttonText && content.buttonColor) {
- const button = page.locator('button', { hasText: new RegExp(content.buttonText, 'i') });
- await expect(button).toHaveCSS('background-color', content.buttonColor);
- }
-}
diff --git a/gui/test/e2e/utils.ts b/gui/test/e2e/utils.ts
deleted file mode 100644
index 97524f69bc..0000000000
--- a/gui/test/e2e/utils.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import fs from 'fs';
-import { _electron as electron, ElectronApplication, Locator, Page } from 'playwright';
-
-export interface StartAppResponse {
- app: ElectronApplication;
- page: Page;
- util: TestUtils;
-}
-
-export interface TestUtils {
- currentRoute: () => Promise<string>;
- waitForNavigation: (initiateNavigation?: () => Promise<void> | void) => Promise<string>;
- waitForNoTransition: () => Promise<void>;
-}
-
-interface History {
- entries: Array<{ pathname: string }>;
- index: number;
-}
-
-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();
-
- // Wait for initial navigation to finish
- await waitForNoTransition(page);
-
- page.on('pageerror', (error) => console.log(error));
- page.on('console', (msg) => console.log(msg.text()));
-
- const util: TestUtils = {
- currentRoute: currentRouteFactory(app),
- waitForNavigation: waitForNavigationFactory(app, page),
- waitForNoTransition: () => waitForNoTransition(page),
- };
-
- return { app, page, util };
-};
-
-export const launch = (options: LaunchOptions): Promise<ElectronApplication> => {
- process.env.CI = 'e2e';
- return electron.launch(options);
-};
-
-const currentRouteFactory = (app: ElectronApplication) => {
- return () =>
- app.evaluate<string>(({ webContents }) =>
- webContents
- .getAllWebContents()
- // Select window that isn't devtools
- .find((webContents) => webContents.getURL().startsWith('file://'))!
- .executeJavaScript('window.e2e.location'),
- );
-};
-
-const waitForNavigationFactory = (app: ElectronApplication, page: Page) => {
- // 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(app), initiateNavigation?.()]);
-
- // Wait for view corresponding to new route to appear
- await page.getByTestId(route).isVisible();
- await waitForNoTransition(page);
-
- return route;
- };
-};
-
-const waitForNoTransition = async (page: Page) => {
- // Wait until there's only one transitionContents
- let transitionContentsCount;
- do {
- if (transitionContentsCount !== undefined) {
- await new Promise((resolve) => setTimeout(resolve, 5));
- }
-
- try {
- transitionContentsCount = await page.getByTestId('transition-content').count();
- } catch {
- console.log('Transition content count failed');
- break;
- }
- } while (transitionContentsCount !== 1);
-};
-
-// Returns the route when it changes
-const waitForNextRoute = (app: ElectronApplication): Promise<string> => {
- return app.evaluate(
- ({ ipcMain }) =>
- new Promise((resolve) => {
- ipcMain.once('navigation-setHistory', (_event, history: History) => {
- resolve(history.entries[history.index].pathname);
- });
- }),
- );
-};
-
-const getStyleProperty = (locator: Locator, property: string) => {
- return locator.evaluate(
- (el, { property }) => {
- return window.getComputedStyle(el).getPropertyValue(property);
- },
- { property },
- );
-};
-
-export const getColor = (locator: Locator) => {
- return getStyleProperty(locator, 'color');
-};
-
-export const getBackgroundColor = (locator: Locator) => {
- return getStyleProperty(locator, 'background-color');
-};
-
-export function anyOf(...values: string[]): RegExp {
- return new RegExp(values.map(escapeRegExp).join('|'));
-}
-
-export function escapeRegExp(regexp: string): string {
- return regexp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
-}
-
-export function fileExists(filePath: string): boolean {
- try {
- fs.accessSync(filePath);
- return true;
- } catch {
- return false;
- }
-}
diff --git a/gui/test/unit/account-data-cache.spec.ts b/gui/test/unit/account-data-cache.spec.ts
deleted file mode 100644
index d4147a6603..0000000000
--- a/gui/test/unit/account-data-cache.spec.ts
+++ /dev/null
@@ -1,315 +0,0 @@
-import { expect, spy } from 'chai';
-import sinon from 'sinon';
-
-import AccountDataCache, { AccountFetchError } from '../../src/main/account-data-cache';
-import { AccountDataResponse, IAccountData } from '../../src/shared/daemon-rpc-types';
-
-describe('IAccountData cache', () => {
- const dummyAccountNumber = '9876543210';
- const dummyAccountData: AccountDataResponse = {
- type: 'success',
- expiry: new Date('2038-01-01').toISOString(),
- };
-
- let clock: sinon.SinonFakeTimers;
-
- beforeEach(() => {
- clock = sinon.useFakeTimers({ shouldAdvanceTime: true });
- });
-
- afterEach(() => {
- clock.restore();
- });
-
- it('should notify when fetch succeeds on the first attempt', async () => {
- const cache = new AccountDataCache(
- (_number) => Promise.resolve(dummyAccountData),
- (_data) => {},
- );
-
- const watcher = new Promise<void>((resolve, reject) => {
- cache.fetch(dummyAccountNumber, {
- onFinish: () => resolve(),
- onError: (_error: AccountFetchError) => reject(),
- });
- });
-
- return expect(watcher).to.eventually.be.fulfilled;
- });
-
- it('should notify when fetch fails on the first attempt', async () => {
- const cache = new AccountDataCache(
- (_number) => Promise.resolve({ type: 'error', error: 'invalid-account' }),
- (_data) => {},
- );
-
- const watcher = new Promise<void>((resolve, reject) => {
- cache.fetch(dummyAccountNumber, {
- onFinish: () => resolve(),
- onError: (_error: AccountFetchError) => reject(),
- });
- });
-
- return expect(watcher).to.eventually.be.rejected;
- });
-
- it('should update when fetch succeeds on the first attempt', async () => {
- const update = new Promise<void>((resolve, reject) => {
- const cache = new AccountDataCache(
- (_) => Promise.resolve(dummyAccountData),
- () => resolve(),
- );
-
- cache.fetch(dummyAccountNumber, {
- onFinish: () => {},
- onError: (_error: AccountFetchError) => reject(),
- });
- });
-
- return expect(update).to.eventually.be.fulfilled;
- });
-
- it('should update when fetch succeeds on the second attempt', async () => {
- const update = new Promise<void>((resolve, reject) => {
- let firstAttempt = true;
- const fetch = () => {
- if (firstAttempt) {
- firstAttempt = false;
- setTimeout(() => clock.tick(9000), 0);
- return Promise.reject(new Error('First attempt fails'));
- } else {
- resolve();
- return Promise.resolve(dummyAccountData);
- }
- };
-
- const cache = new AccountDataCache(fetch, () => resolve());
-
- cache.fetch(dummyAccountNumber, {
- onFinish: () => reject(),
- onError: (_error: AccountFetchError) => {},
- });
- });
-
- return expect(update).to.eventually.be.fulfilled;
- });
-
- it('should cancel first fetch', async () => {
- const firstError = spy((_error: AccountFetchError) => {});
- const secondSuccess = spy();
-
- const update = new Promise<IAccountData | void>((resolve, reject) => {
- let firstAttempt = true;
- const fetch = (_number: string) => {
- if (firstAttempt) {
- firstAttempt = false;
-
- cache.fetch('1231231231', {
- onFinish: secondSuccess,
- onError: () => reject(),
- });
-
- return new Promise<AccountDataResponse>((resolve) => {
- setTimeout(() => resolve(dummyAccountData), 1000);
- });
- } else {
- reject();
- return Promise.resolve(dummyAccountData);
- }
- };
-
- const cache = new AccountDataCache(fetch, (_accountData?: IAccountData) => {
- resolve();
- });
-
- setTimeout(resolve, 12000);
-
- cache.fetch(dummyAccountNumber, {
- onFinish: reject,
- onError: firstError,
- });
- });
-
- return expect(update).to.eventually.be.fulfilled.then(() => {
- expect(firstError).to.have.been.called.once;
- expect(secondSuccess).to.have.been.called.once;
- return;
- });
- });
-
- it('should clear scheduled retry if another fetch is performed', async () => {
- const firstError = spy();
- const secondSuccess = spy();
- const updateHandler = spy();
-
- const update = new Promise((resolve, reject) => {
- let attempts = 0;
- const fetch = (): Promise<AccountDataResponse> => {
- attempts++;
- if (attempts === 1) {
- return Promise.resolve({ type: 'error', error: 'invalid-account' });
- } else if (attempts === 2) {
- setTimeout(() => clock.tick(8000));
- return Promise.resolve(dummyAccountData);
- } else {
- reject();
- return Promise.resolve(dummyAccountData);
- }
- };
-
- const cache = new AccountDataCache(fetch, updateHandler);
-
- cache.fetch(dummyAccountNumber, {
- onFinish: () => {},
- onError: (_error: AccountFetchError) => firstError(),
- });
- setTimeout(() => {
- cache.fetch(dummyAccountNumber, {
- onFinish: () => {
- secondSuccess();
- setTimeout(resolve);
- },
- onError: (_error: AccountFetchError) => {},
- });
- });
- });
-
- return expect(update).to.eventually.be.fulfilled.then(() => {
- expect(firstError).to.have.been.called.once;
- expect(secondSuccess).to.have.been.called.once;
- expect(updateHandler).to.have.been.called.twice;
- });
- });
-
- it('should not perform a fetch if called twice synchronously', async () => {
- const fetchSpy = spy();
- const update = new Promise<void>((resolve, _reject) => {
- const fetch = () => {
- fetchSpy();
- return Promise.resolve(dummyAccountData);
- };
-
- const cache = new AccountDataCache(fetch, () => {});
- const onError = (_error: AccountFetchError) => {};
- cache.fetch(dummyAccountNumber, { onFinish: () => {}, onError });
- cache.fetch(dummyAccountNumber, { onFinish: () => resolve(), onError });
- });
-
- return expect(update).to.eventually.be.fulfilled.then(() => {
- expect(fetchSpy).to.have.been.called.once;
- });
- });
-
- it('should refetch one minute before expiry', async () => {
- const date = new Date();
- date.setMinutes(date.getMinutes() + 3);
- const expiry = date.toISOString();
-
- const update = new Promise<void>((resolve, reject) => {
- let firstAttempt = true;
- const fetch = (_accountNumber: string): Promise<AccountDataResponse> => {
- if (firstAttempt) {
- firstAttempt = false;
- setTimeout(() => clock.tick(120_000), 0);
- return Promise.resolve({ type: 'success', expiry });
- } else {
- resolve();
- return Promise.resolve({ type: 'success', expiry });
- }
- };
-
- const cache = new AccountDataCache(fetch, () => {});
-
- cache.fetch(dummyAccountNumber, {
- onFinish: () => {},
- onError: (_error: AccountFetchError) => reject(),
- });
- });
-
- return expect(update).to.eventually.be.fulfilled;
- });
-
- it('should invalidate after 60 seconds', async () => {
- const fetchSpy = spy();
- const expiry = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
-
- const update = new Promise<void>((resolve, reject) => {
- const cache = new AccountDataCache(
- (_accountNumber) => {
- fetchSpy();
- return Promise.resolve<AccountDataResponse>({ type: 'success', expiry });
- },
- () => {},
- );
-
- cache.fetch(dummyAccountNumber, {
- onFinish: async () => {
- clock.tick(59_000);
- // Timeout to let asynchronous tasks finish
- await new Promise((resolve) => setTimeout(resolve));
-
- cache.fetch(dummyAccountNumber, {
- onFinish: async () => {
- clock.tick(1_000);
- // Timeout to let asynchronous tasks finish
- await new Promise((resolve) => setTimeout(resolve));
-
- cache.fetch(dummyAccountNumber, {
- onFinish: () => resolve(),
- onError: (_error: AccountFetchError) => reject(),
- });
- },
- onError: (_error: AccountFetchError) => reject(),
- });
- },
- onError: (_error: AccountFetchError) => reject(),
- });
- });
-
- return expect(update).to.eventually.be.fulfilled.then(() => {
- expect(fetchSpy).to.have.been.called.twice;
- });
- });
-
- it('should invalidate after 10 seconds when epired', async () => {
- const fetchSpy = spy();
- const expiry = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
-
- const update = new Promise<void>((resolve, reject) => {
- const cache = new AccountDataCache(
- (_accountNumber) => {
- fetchSpy();
- return Promise.resolve<AccountDataResponse>({ type: 'success', expiry });
- },
- () => {},
- );
-
- cache.fetch(dummyAccountNumber, {
- onFinish: async () => {
- clock.tick(9_000);
- // Timeout to let asynchronous tasks finish
- await new Promise((resolve) => setTimeout(resolve));
-
- cache.fetch(dummyAccountNumber, {
- onFinish: async () => {
- clock.tick(1_000);
- // Timeout to let asynchronous tasks finish
- await new Promise((resolve) => setTimeout(resolve));
-
- cache.fetch(dummyAccountNumber, {
- onFinish: () => resolve(),
- onError: (_error: AccountFetchError) => reject(),
- });
- },
- onError: (_error: AccountFetchError) => reject(),
- });
- },
- onError: (_error: AccountFetchError) => reject(),
- });
- });
-
- return expect(update).to.eventually.be.fulfilled.then(() => {
- expect(fetchSpy).to.have.been.called.twice;
- });
- });
-});
diff --git a/gui/test/unit/changelog.spec.ts b/gui/test/unit/changelog.spec.ts
deleted file mode 100644
index 774dca956b..0000000000
--- a/gui/test/unit/changelog.spec.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { expect } from 'chai';
-import { after, describe, it } from 'mocha';
-
-import { parseChangelog } from '../../src/main/changelog';
-
-// It should be handled the same no matter if the platforms are split with a space or not.
-const changelogItems = [
- 'Changelog item 1',
- '[Windows] Changelog item 2',
- '[macOS] Changelog item 3',
- '[linux] Changelog item 4',
- '[Windows, macOS] Changelog item 5',
- '[Windows,linux] Changelog item 6',
- '[Windows, macOS,linux] Changelog item 7',
-];
-
-const changelogString = changelogItems.join('\n');
-
-const mockPlatform = (platform: string) => {
- Object.defineProperty(process, 'platform', { value: platform });
-};
-
-describe('Changelog parser', () => {
- const platform = process.platform;
-
- after(() => {
- mockPlatform(platform);
- });
-
- it('should show Windows items', () => {
- mockPlatform('win32');
-
- const changelog = parseChangelog(changelogString);
-
- expect(changelog).to.have.length(5);
- expect(changelogItems[0].endsWith(changelog[0])).to.be.true;
- expect(changelogItems[1].endsWith(changelog[1])).to.be.true;
- expect(changelogItems[4].endsWith(changelog[2])).to.be.true;
- expect(changelogItems[5].endsWith(changelog[3])).to.be.true;
- expect(changelogItems[6].endsWith(changelog[4])).to.be.true;
- });
-
- it('should show macOS items', () => {
- mockPlatform('darwin');
-
- const changelog = parseChangelog(changelogString);
-
- expect(changelog).to.have.length(4);
- expect(changelogItems[0].endsWith(changelog[0])).to.be.true;
- expect(changelogItems[2].endsWith(changelog[1])).to.be.true;
- expect(changelogItems[4].endsWith(changelog[2])).to.be.true;
- expect(changelogItems[6].endsWith(changelog[3])).to.be.true;
- });
-
- it('should show Linux items', () => {
- mockPlatform('linux');
-
- const changelog = parseChangelog(changelogString);
-
- expect(changelog).to.have.length(4);
- expect(changelogItems[0].endsWith(changelog[0])).to.be.true;
- expect(changelogItems[3].endsWith(changelog[1])).to.be.true;
- expect(changelogItems[5].endsWith(changelog[2])).to.be.true;
- expect(changelogItems[6].endsWith(changelog[3])).to.be.true;
- });
-});
diff --git a/gui/test/unit/date-helper.spec.ts b/gui/test/unit/date-helper.spec.ts
deleted file mode 100644
index 82c4faeac7..0000000000
--- a/gui/test/unit/date-helper.spec.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-import { expect } from 'chai';
-import { describe, it } from 'mocha';
-
-import * as date from '../../src/shared/date-helper';
-
-describe('Date helper', () => {
- it('should modify minutes', () => {
- const initialDate = new Date('2021-01-01 13:37:10');
- const earlierDate = date.dateByAddingComponent(initialDate, date.DateComponent.minute, -50);
- const laterDate = date.dateByAddingComponent(initialDate, date.DateComponent.minute, 100);
-
- expect(earlierDate.getFullYear()).to.equal(2021);
- expect(earlierDate.getMonth()).to.equal(0);
- expect(earlierDate.getDate()).to.equal(1);
- expect(earlierDate.getHours()).to.equal(12);
- expect(earlierDate.getMinutes()).to.equal(47);
- expect(earlierDate.getSeconds()).to.equal(10);
-
- expect(laterDate.getFullYear()).to.equal(2021);
- expect(laterDate.getMonth()).to.equal(0);
- expect(laterDate.getDate()).to.equal(1);
- expect(laterDate.getHours()).to.equal(15);
- expect(laterDate.getMinutes()).to.equal(17);
- expect(laterDate.getSeconds()).to.equal(10);
- });
-
- it('should modify hours', () => {
- const initialDate = new Date('2021-01-01 13:37:10');
- const earlierDate = date.dateByAddingComponent(initialDate, date.DateComponent.hour, -50);
- const laterDate = date.dateByAddingComponent(initialDate, date.DateComponent.hour, 100);
-
- expect(earlierDate.getFullYear()).to.equal(2020);
- expect(earlierDate.getMonth()).to.equal(11);
- expect(earlierDate.getDate()).to.equal(30);
- expect(earlierDate.getHours()).to.equal(11);
- expect(earlierDate.getMinutes()).to.equal(37);
- expect(earlierDate.getSeconds()).to.equal(10);
-
- expect(laterDate.getFullYear()).to.equal(2021);
- expect(laterDate.getMonth()).to.equal(0);
- expect(laterDate.getDate()).to.equal(5);
- expect(laterDate.getHours()).to.equal(17);
- expect(laterDate.getMinutes()).to.equal(37);
- expect(laterDate.getSeconds()).to.equal(10);
- });
-
- it('should modify days', () => {
- const initialDate = new Date('2021-01-01 13:37:10');
- const earlierDate = date.dateByAddingComponent(initialDate, date.DateComponent.day, -50);
- const laterDate = date.dateByAddingComponent(initialDate, date.DateComponent.day, 100);
-
- expect(earlierDate.getFullYear()).to.equal(2020);
- expect(earlierDate.getMonth()).to.equal(10);
- expect(earlierDate.getDate()).to.equal(12);
- expect(earlierDate.getHours()).to.equal(13);
- expect(earlierDate.getMinutes()).to.equal(37);
- expect(earlierDate.getSeconds()).to.equal(10);
-
- expect(laterDate.getFullYear()).to.equal(2021);
- expect(laterDate.getMonth()).to.equal(3);
- expect(laterDate.getDate()).to.equal(11);
- expect(laterDate.getHours()).to.equal(13);
- expect(laterDate.getMinutes()).to.equal(37);
- expect(laterDate.getSeconds()).to.equal(10);
- });
-
- it('should calculate positive difference between dates', () => {
- const diff1 = new date.DateDiff('2021-01-14 13:37:10', '2021-02-01 14:40:12');
- expect(diff1.years).to.equal(0);
- expect(diff1.months).to.equal(0);
- expect(diff1.days).to.equal(18);
- expect(diff1.hours).to.equal(diff1.days * 24 + 1);
- expect(diff1.minutes).to.equal(diff1.hours * 60 + 3);
- expect(diff1.seconds).to.equal(diff1.minutes * 60 + 2);
-
- const diff2 = new date.DateDiff('2021-01-14 13:37:10', '2021-02-14 14:40:12');
- expect(diff2.years).to.equal(0);
- expect(diff2.months).to.equal(1);
- expect(diff2.days).to.equal(31);
- expect(diff2.hours).to.equal(diff2.days * 24 + 1);
- expect(diff2.minutes).to.equal(diff2.hours * 60 + 3);
- expect(diff2.seconds).to.equal(diff2.minutes * 60 + 2);
-
- const diff3 = new date.DateDiff('2021-01-14 13:37:10', '2022-01-14 13:37:09');
- expect(diff3.years).to.equal(0);
- expect(diff3.months).to.equal(11);
- expect(diff3.days).to.equal(364);
- expect(diff3.hours).to.equal(diff3.days * 24 + 23);
- expect(diff3.minutes).to.equal(diff3.hours * 60 + 59);
- expect(diff3.seconds).to.equal(diff3.minutes * 60 + 59);
- });
-
- it('should calculate negative difference between dates', () => {
- const diff1 = new date.DateDiff('2021-02-01 14:40:12', '2021-01-14 13:37:10');
- expect(diff1.years).to.equal(0);
- expect(diff1.months).to.equal(0);
- expect(diff1.days).to.equal(-18, 'aa');
- expect(diff1.hours).to.equal(diff1.days * 24 - 1);
- expect(diff1.minutes).to.equal(diff1.hours * 60 - 3);
- expect(diff1.seconds).to.equal(diff1.minutes * 60 - 2);
- });
-
- it('should format positive difference as string', () => {
- const diff1 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-01-01 13:37:20', {
- displayMonths: true,
- });
- expect(diff1).to.equal('less than a day');
-
- const diff2 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-01-02 13:37:20', {
- displayMonths: true,
- });
- expect(diff2).to.equal('1 day');
-
- const diff3 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-02-25 13:37:20', {
- displayMonths: true,
- });
- expect(diff3).to.equal('55 days');
-
- const diff4 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-04-25 13:37:20', {
- displayMonths: true,
- });
- expect(diff4).to.equal('3 months');
-
- const diff5 = date.formatRelativeDate('2021-01-01 13:37:10', '2031-04-25 13:37:20', {
- displayMonths: true,
- });
- expect(diff5).to.equal('10 years');
- });
-
- it('should format positive difference as string suffixed with "left"', () => {
- const diff1 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-01-01 13:37:20', {
- suffix: true,
- displayMonths: true,
- });
- expect(diff1).to.equal('less than a day left');
-
- const diff2 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-01-02 13:37:20', {
- suffix: true,
- displayMonths: true,
- });
- expect(diff2).to.equal('1 day left');
-
- const diff3 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-02-25 13:37:20', {
- suffix: true,
- displayMonths: true,
- });
- expect(diff3).to.equal('55 days left');
-
- const diff4 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-04-25 13:37:20', {
- suffix: true,
- displayMonths: true,
- });
- expect(diff4).to.equal('3 months left');
-
- const diff5 = date.formatRelativeDate('2021-01-01 13:37:10', '2031-04-25 13:37:20', {
- suffix: true,
- displayMonths: true,
- });
- expect(diff5).to.equal('10 years left');
- });
-
- it('should format time left correctly', () => {
- expect(date.formatRelativeDate('2022-09-01', '2022-09-01')).to.equal('less than a day');
- expect(date.formatRelativeDate('2022-09-01', '2022-09-02')).to.equal('1 day');
- expect(date.formatRelativeDate('2022-09-01', '2022-09-05')).to.equal('4 days');
- expect(date.formatRelativeDate('2022-09-01', '2022-09-30')).to.equal('29 days');
- expect(date.formatRelativeDate('2022-09-01', '2023-09-01')).to.equal('365 days');
- expect(date.formatRelativeDate('2022-09-01', '2024-08-30')).to.equal('729 days');
- expect(date.formatRelativeDate('2022-09-01', '2024-08-31')).to.equal('2 years');
- expect(date.formatRelativeDate('2022-09-01', '2024-09-05')).to.equal('2 years');
- expect(date.formatRelativeDate('2022-09-01', '2025-08-31')).to.equal('2 years');
- expect(date.formatRelativeDate('2022-09-01', '2025-09-01')).to.equal('3 years');
- });
-});
diff --git a/gui/test/unit/history.spec.ts b/gui/test/unit/history.spec.ts
deleted file mode 100644
index 739c65c5ca..0000000000
--- a/gui/test/unit/history.spec.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { expect, spy } from 'chai';
-import { beforeEach, describe, it } from 'mocha';
-
-import History from '../../src/renderer/lib/history';
-import { RoutePath } from '../../src/renderer/lib/routes';
-
-const BASE_PATH = RoutePath.launch;
-const FIRST_PATH = RoutePath.main;
-const SECOND_PATH = RoutePath.settings;
-const THIRD_PATH = RoutePath.vpnSettings;
-const FOURTH_PATH = RoutePath.userInterfaceSettings;
-const FIFTH_PATH = RoutePath.splitTunneling;
-
-describe('History', () => {
- let history: History;
-
- beforeEach(() => {
- history = new History(BASE_PATH);
- history.push(FIRST_PATH);
- history.push(SECOND_PATH);
- history.push(THIRD_PATH);
- history.push(FOURTH_PATH);
- });
-
- it('should start at the correct location', () => {
- const history2 = new History(BASE_PATH);
-
- expect(history2.location.pathname).to.equal(BASE_PATH);
- expect(history2.length).to.equal(1);
- expect(history.location.pathname).to.equal(FOURTH_PATH);
- expect(history.length).to.equal(5);
- });
-
- it('should pop', () => {
- history.pop();
- expect(history.location.pathname).to.equal(THIRD_PATH);
- expect(history.length).to.equal(4);
- });
-
- it('should fail to pop', () => {
- history.pop();
- history.pop();
- history.pop();
- history.pop();
-
- expect(history.location.pathname).to.equal(BASE_PATH);
- expect(history.length).to.equal(1);
-
- history.pop();
-
- expect(history.location.pathname).to.equal(BASE_PATH);
- expect(history.length).to.equal(1);
- });
-
- it('should push', () => {
- history.push(FIFTH_PATH);
- expect(history.location.pathname).to.equal(FIFTH_PATH);
- expect(history.length).to.equal(6);
- });
-
- it('should go backward to base path', () => {
- history.pop(true);
- expect(history.location.pathname).to.equal(BASE_PATH);
- expect(history.length).to.equal(1);
- });
-
- it('should reset entries with path', () => {
- history.reset(THIRD_PATH);
- expect(history.location.pathname).to.equal(THIRD_PATH);
- expect(history.length).to.equal(1);
- });
-
- it('should add a listener', () => {
- const listenerA = spy();
- history.listen(listenerA);
- history.pop();
- history.push(FIFTH_PATH);
-
- const listenerB = spy();
- history.listen(listenerB);
- history.pop(true);
- history.push(FIRST_PATH);
-
- expect(listenerA).to.have.been.called.exactly(4);
- expect(listenerB).to.have.been.called.exactly(2);
- });
-
- it('should remove a listener', () => {
- const listenerA = spy();
- const removeListenerA = history.listen(listenerA);
- history.pop();
- history.push(FIFTH_PATH);
-
- const listenerB = spy();
- history.listen(listenerB);
- history.pop(true);
-
- removeListenerA();
- history.push(FIRST_PATH);
- history.reset(SECOND_PATH);
-
- expect(listenerA).to.have.been.called.exactly(3);
- expect(listenerB).to.have.been.called.exactly(3);
- });
-});
diff --git a/gui/test/unit/ip.spec.ts b/gui/test/unit/ip.spec.ts
deleted file mode 100644
index 5f6a265d25..0000000000
--- a/gui/test/unit/ip.spec.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-import { expect } from 'chai';
-import { describe, it } from 'mocha';
-
-import * as ip from '../../src/renderer/lib/ip';
-
-const validIpv4Addresses = [
- '127.0.0.1',
- '10.255.255.255',
- '192.168.1.1',
- '192.168.0.10',
- '192.168.1.254',
- '192.168.254.254',
- '10.0.0.1',
- '10.90.90.90',
- '1.1.1.1',
- '193.138.218.74',
-];
-
-const validIpv6Addresses = [
- '0:1:2:3:4:5:6:7',
- '00:11:22:33:44:55:66:77',
- '000:111:222:333:444:555:666:777',
- '0000:1111:2222:3333:4444:5555:6666:7777',
- 'ffff::',
- '::ff:2233',
- 'fee::ff:2233',
-];
-
-const invalidIpv4Addresses = [
- '127.0.0.0.1',
- '10.0.0.256',
- '192.168.1',
- '0.0.0.a1',
- '0.0.0.',
- '0.0..0',
- '0. 0.0.0',
- '0.a.0.0.0',
-];
-
-const invalidIpv6Addresses = [
- '00:11:22:33:44:55:66:77:88',
- '00:11:22:33:44:55:66',
- 'ff::ff::ff',
- 'ff::ff::',
- '::ff::',
- '00:11:22:33:44:55:66:gg',
- '13245:11:22:33:44:55:66:77',
- '::1g',
- 'gg:11:22:33:44:55:66:77:88',
-];
-
-const validIpv4Subnets = ['10.0.0.0/0', '10.0.0.0/8', '10.0.0.0/32'];
-const invalidIpv4Subnets = ['10.0.0.0', '10.0.0.0/', '10.0.0.0/-1', '10.0.0.0/33'];
-
-const validIpv6Subnets = ['::1/0', 'fe::/128', '1:1::1/12', '0:1:2:3:4:5:6:7/64'];
-const invalidIpv6Subnets = ['::1', 'fe::/', '0:0:0:0:0:0:0:0/-1', '::1/129'];
-
-const localIpAddresses = [
- '10.0.0.0',
- '10.255.255.255',
- '172.16.0.0',
- '172.31.255.255',
- '192.168.0.0',
- '192.168.255.255',
-];
-
-const publicIpAddresses = [
- '1.1.1.1',
- '193.138.218.74',
- '9.255.255.255',
- '11.0.0.0',
- '172.15.0.0',
- '172.15.255.255',
- '172.32.0.0',
- '192.167.0.0',
- '192.167.255.255',
- '192.169.0.0',
-];
-
-describe('IP', () => {
- it('should detect that valid IPv4 addresses are valid', () => {
- validIpv4Addresses.forEach((ipAddress) => {
- const valid = ip.IPv4Address.isValid(ipAddress);
- expect(valid).to.be.true;
- expect(() => ip.IPv4Address.fromString(ipAddress)).to.not.throw();
- });
- });
-
- it('should detect that invalid IPv4 addresses are invalid', () => {
- invalidIpv4Addresses.forEach((ipAddress) => {
- const valid = ip.IPv4Address.isValid(ipAddress);
- expect(valid).to.be.false;
- expect(() => ip.IPv4Address.fromString(ipAddress)).to.throw();
- });
- });
-
- it('should detect that valid IPv6 addresses are valid', () => {
- validIpv6Addresses.forEach((ipAddress) => {
- const valid = ip.IPv6Address.isValid(ipAddress);
- expect(valid).to.be.true;
- expect(() => ip.IPv6Address.fromString(ipAddress)).to.not.throw();
- });
- });
-
- it('should detect that invalid IPv6 addresses are invalid', () => {
- invalidIpv6Addresses.forEach((ipAddress) => {
- const valid = ip.IPv6Address.isValid(ipAddress);
- expect(valid).to.be.false;
- expect(() => ip.IPv6Address.fromString(ipAddress)).to.throw();
- });
- });
-
- it('should detect that valid IPv4 subnets are valid', () => {
- validIpv4Subnets.forEach((subnet) => {
- expect(() => ip.IPv4Range.fromString(subnet)).to.not.throw();
- });
- });
-
- it('should detect that invalid IPv4 subnets are invalid', () => {
- invalidIpv4Subnets.forEach((subnet) => {
- expect(() => ip.IPv4Range.fromString(subnet)).to.throw();
- });
- });
-
- it('should detect that valid IPv6 subnets are valid', () => {
- validIpv6Subnets.forEach((subnet) => {
- expect(() => ip.IPv6Range.fromString(subnet)).to.not.throw();
- });
- });
-
- it('should detect that invalid IPv6 subnets are invalid', () => {
- invalidIpv6Subnets.forEach((subnet) => {
- expect(() => ip.IPv6Range.fromString(subnet)).to.throw();
- });
- });
-
- it('should detect that IP addresses are local', () => {
- localIpAddresses.forEach((ipAddress) => {
- expect(ip.IpAddress.fromString(ipAddress).isLocal()).to.be.true;
- });
- });
-
- it('should detect that IP addresses are public', () => {
- publicIpAddresses.forEach((ipAddress) => {
- expect(ip.IpAddress.fromString(ipAddress).isLocal()).to.be.false;
- });
- });
-
- it('should correctly parse IP addresses', () => {
- expect(ip.IpAddress.fromString('127.0.0.1').groups).to.deep.equal([127, 0, 0, 1]);
- expect(ip.IpAddress.fromString('1.1.1.1').groups).to.deep.equal([1, 1, 1, 1]);
- expect(ip.IpAddress.fromString('252.253.254.255').groups).to.deep.equal([252, 253, 254, 255]);
-
- const ip1 = ip.IpAddress.fromString('0:1:2:3:4:5:6:7').groups;
- expect(ip1).to.deep.equal([0, 1, 2, 3, 4, 5, 6, 7]);
-
- const ip2 = ip.IpAddress.fromString('ffff::').groups;
- expect(ip2).to.deep.equal([0xffff, 0, 0, 0, 0, 0, 0, 0]);
-
- const ip3 = ip.IpAddress.fromString('::1').groups;
- expect(ip3).to.deep.equal([0, 0, 0, 0, 0, 0, 0, 1]);
-
- const ip4 = ip.IpAddress.fromString('ffff:1::1').groups;
- expect(ip4).to.deep.equal([0xffff, 1, 0, 0, 0, 0, 0, 1]);
- });
-
- it('should correctly parse IP range prefix sizes', () => {
- expect(ip.IPv4Range.fromString('127.0.0.1/0').prefixSize).to.equal(0);
- expect(ip.IPv4Range.fromString('1.1.1.1/32').prefixSize).to.equal(32);
-
- expect(ip.IPv6Range.fromString('0:1:2:3:4:5:6:7/0').prefixSize).to.equal(0);
- expect(ip.IPv6Range.fromString('ffff::/128').prefixSize).to.equal(128);
- expect(ip.IPv6Range.fromString('::1/32').prefixSize).to.equal(32);
- });
-});
diff --git a/gui/test/unit/keyframe-animation.spec.ts b/gui/test/unit/keyframe-animation.spec.ts
deleted file mode 100644
index 0ba302e65c..0000000000
--- a/gui/test/unit/keyframe-animation.spec.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { expect } from 'chai';
-import { describe, it } from 'mocha';
-
-import KeyframeAnimation from '../../src/main/keyframe-animation';
-
-describe('lib/keyframe-animation', function () {
- this.timeout(1000);
-
- const newAnimation = () => {
- const animation = new KeyframeAnimation();
- animation.speed = 1;
- return animation;
- };
-
- it('should play sequence', (done) => {
- const seq: number[] = [];
- const animation = newAnimation();
- animation.onFrame = (frame) => {
- seq.push(frame);
- };
- animation.onFinish = () => {
- expect(seq).to.be.deep.equal([0, 1, 2, 3, 4]);
- expect(animation.currentFrame).to.be.equal(4);
- done();
- };
-
- animation.play({ end: 4 });
- });
-
- it('should play one frame', (done) => {
- const seq: number[] = [];
- const animation = newAnimation();
- animation.onFrame = (frame) => {
- seq.push(frame);
- };
- animation.onFinish = () => {
- expect(seq).to.be.deep.equal([3]);
- expect(animation.currentFrame).to.be.equal(3);
- done();
- };
-
- animation.play({ start: 3, end: 3 });
- });
-
- it('should play sequence with custom frames', (done) => {
- const seq: number[] = [];
- const animation = newAnimation();
- animation.onFrame = (frame) => {
- seq.push(frame);
- };
- animation.onFinish = () => {
- expect(seq).to.be.deep.equal([2, 3, 4]);
- expect(animation.currentFrame).to.be.equal(4);
- done();
- };
-
- animation.play({ start: 2, end: 4 });
- });
-
- it('should play sequence with custom frames in reverse', (done) => {
- const seq: number[] = [];
- const animation = newAnimation();
- animation.onFrame = (frame) => {
- seq.push(frame);
- };
- animation.onFinish = () => {
- expect(seq).to.be.deep.equal([4, 3, 2]);
- expect(animation.currentFrame).to.be.equal(2);
- done();
- };
-
- animation.play({ start: 4, end: 2 });
- });
-
- it('should begin from current state starting below range', (done) => {
- const seq: number[] = [];
- const animation = newAnimation();
- animation.onFrame = (frame) => {
- seq.push(frame);
- };
- animation.onFinish = () => {
- expect(seq).to.be.deep.equal([0, 1, 2, 3, 4]);
- expect(animation.currentFrame).to.be.equal(4);
- done();
- };
-
- animation.currentFrame = 0;
- animation.play({ end: 4 });
- });
-
- it('should begin from current state starting above range', (done) => {
- const seq: number[] = [];
- const animation = newAnimation();
- animation.onFrame = (frame) => {
- seq.push(frame);
- };
- animation.onFinish = () => {
- expect(seq).to.be.deep.equal([4, 3, 2]);
- expect(animation.currentFrame).to.be.equal(2);
- done();
- };
-
- animation.currentFrame = 4;
- animation.play({ end: 2 });
- });
-
- it('should begin from current state starting above range reverse', (done) => {
- const seq: number[] = [];
- const animation = newAnimation();
- animation.onFrame = (frame) => {
- seq.push(frame);
- };
- animation.onFinish = () => {
- expect(seq).to.be.deep.equal([4, 3, 2, 1]);
- expect(animation.currentFrame).to.be.equal(1);
- done();
- };
-
- animation.currentFrame = 4;
- animation.play({ end: 1 });
- });
-
- it('should play sequence in reverse', (done) => {
- const seq: number[] = [];
- const animation = newAnimation();
- animation.onFrame = (frame) => {
- seq.push(frame);
- };
- animation.onFinish = () => {
- expect(seq).to.be.deep.equal([4, 3, 2, 1, 0]);
- expect(animation.currentFrame).to.be.equal(0);
- done();
- };
-
- animation.play({ start: 4, end: 0 });
- });
-});
diff --git a/gui/test/unit/list-diff.spec.ts b/gui/test/unit/list-diff.spec.ts
deleted file mode 100644
index e64d79b193..0000000000
--- a/gui/test/unit/list-diff.spec.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { expect } from 'chai';
-import { describe, it } from 'mocha';
-
-import { calculateItemList, RowDisplayData } from '../../src/renderer/components/List';
-
-const prevItems: Array<RowDisplayData<undefined>> = [
- { key: 'a', data: undefined, removing: false },
- { key: 'b', data: undefined, removing: false },
- { key: 'c', data: undefined, removing: false },
- { key: 'd', data: undefined, removing: false },
-];
-
-describe('List diff', () => {
- it('Should add item to the beginning', () => {
- const nextItems: Array<RowDisplayData<undefined>> = [
- { key: '1', data: undefined, removing: false },
- ...prevItems,
- ];
- const combinedItems = calculateItemList(prevItems, nextItems);
-
- expect(combinedItems).to.have.length(5);
- expect(combinedItems.slice(1)).to.deep.equal(prevItems);
- expect(combinedItems).to.deep.equal(nextItems);
- });
-
- it('Should add item to the end', () => {
- const nextItems: Array<RowDisplayData<undefined>> = [
- ...prevItems,
- { key: '1', data: undefined, removing: false },
- ];
- const combinedItems = calculateItemList(prevItems, nextItems);
-
- expect(combinedItems).to.have.length(5);
- expect(combinedItems.slice(0, 4)).to.deep.equal(prevItems);
- expect(combinedItems).to.deep.equal(nextItems);
- });
-
- it('Should add item to the middle', () => {
- const nextItems: Array<RowDisplayData<undefined>> = [
- ...prevItems.slice(0, 2),
- { key: '1', data: undefined, removing: false },
- ...prevItems.slice(2),
- ];
- const combinedItems = calculateItemList(prevItems, nextItems);
-
- expect(combinedItems).to.have.length(5);
- expect([...combinedItems.slice(0, 2), ...combinedItems.slice(3)]).to.deep.equal(prevItems);
- expect(combinedItems).to.deep.equal(nextItems);
- });
-
- it('Should remove first item', () => {
- const nextItems = prevItems.slice(1);
- const combinedItems = calculateItemList(prevItems, nextItems);
-
- expect(combinedItems).to.have.length(4);
- expect(combinedItems.slice(1, 4)).to.deep.equal(prevItems.slice(1, 4));
- expect(combinedItems[0]).to.deep.equal({ ...prevItems[0], removing: true });
- });
-
- it('Should remove last item', () => {
- const nextItems = prevItems.slice(0, -1);
- const combinedItems = calculateItemList(prevItems, nextItems);
-
- expect(combinedItems).to.have.length(4);
- expect(combinedItems.slice(0, -1)).to.deep.equal(prevItems.slice(0, -1));
- expect(combinedItems.at(-1)).to.deep.equal({ ...prevItems.at(-1), removing: true });
- });
-
- it('Should remove middle item', () => {
- const nextItems = [...prevItems.slice(0, 1), ...prevItems.slice(2)];
- const combinedItems = calculateItemList(prevItems, nextItems);
-
- expect(combinedItems).to.have.length(4);
- expect(combinedItems.slice(0, 1)).to.deep.equal(prevItems.slice(0, 1));
- expect(combinedItems.slice(2)).to.deep.equal(prevItems.slice(2));
- expect(combinedItems[1]).to.deep.equal({ ...prevItems[1], removing: true });
- });
-
- it('should both add and remove items', () => {
- const nextItems = [
- { key: '1', data: undefined, removing: false },
- ...prevItems.slice(1, -1),
- { key: '2', data: undefined, removing: false },
- ];
- const combinedItems = calculateItemList(prevItems, nextItems);
-
- expect(combinedItems).to.have.length(6);
- expect(combinedItems[0]).to.deep.equal({ ...prevItems[0], removing: true });
- expect(combinedItems[1]).to.deep.equal(nextItems[0]);
- expect(combinedItems.slice(2, -2)).to.deep.equal(prevItems.slice(1, -1));
- expect(combinedItems.at(-2)).to.deep.equal({ ...prevItems.at(-1), removing: true });
- expect(combinedItems.at(-1)).to.deep.equal(nextItems.at(-1));
- });
-
- it('should remove item being removed', () => {
- const prevItems: Array<RowDisplayData<undefined>> = [
- { key: '1', data: undefined, removing: true },
- ];
- const nextItems: Array<RowDisplayData<undefined>> = [];
- const combinedItems = calculateItemList(prevItems, nextItems);
-
- expect(combinedItems).to.deep.equal(prevItems);
- });
-});
diff --git a/gui/test/unit/logging.spec.ts b/gui/test/unit/logging.spec.ts
deleted file mode 100644
index 393b631ba5..0000000000
--- a/gui/test/unit/logging.spec.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-import { expect, spy } from 'chai';
-import fs from 'fs';
-import { after, before, beforeEach, describe, it } from 'mocha';
-import path from 'path';
-import sinon from 'sinon';
-
-import { backupLogFile, rotateOrDeleteFile } from '../../src/main/logging';
-import { Logger } from '../../src/shared/logging';
-import { LogLevel } from '../../src/shared/logging-types';
-
-const aPath = path.normalize('log-directory/a.log');
-const oldAPath = path.normalize('log-directory/a.old.log');
-const bPath = path.normalize('log-directory/b.log');
-const oldBPath = path.normalize('log-directory/b.old.log');
-
-const initialFileState = {
- [aPath]: 'a',
- [bPath]: 'b',
- [oldBPath]: 'old b',
-};
-
-describe('Logging', () => {
- let files: Record<string, string>;
- let sandbox: sinon.SinonSandbox;
-
- before(() => {
- sandbox = sinon.createSandbox();
-
- sandbox.stub(fs, 'accessSync').callsFake((filePath) => {
- const normalizedPath = path.normalize(filePath as string);
- if (files[normalizedPath] === undefined) {
- throw Error('File not found');
- }
- });
-
- sandbox.stub(fs, 'renameSync').callsFake((oldPath, newPath) => {
- const normalizedOldPath = path.normalize(oldPath as string);
- const normalizedNewPath = path.normalize(newPath as string);
- files[normalizedNewPath] = files[normalizedOldPath];
- fs.unlinkSync(normalizedOldPath);
- });
-
- sandbox.stub(fs, 'unlinkSync').callsFake((filePath) => {
- const normalizedPath = path.normalize(filePath as string);
- delete files[normalizedPath];
- });
-
- sandbox.stub(fs, 'readFileSync').callsFake((filePath) => {
- const normalizedPath = path.normalize(filePath as string);
- return files[normalizedPath] ?? '';
- });
- });
-
- after(() => {
- sandbox.restore();
- });
-
- beforeEach(() => {
- files = { ...initialFileState };
- });
-
- it('should backup log file', () => {
- backupLogFile(aPath);
- const oldA = fs.readFileSync(oldAPath).toString();
-
- expect(fs.accessSync.bind(null, aPath)).to.throw();
- expect(oldA).to.equal(initialFileState[aPath]);
- });
-
- it('should replace backup file', () => {
- backupLogFile(bPath);
- const oldB = fs.readFileSync(oldBPath).toString();
-
- expect(fs.accessSync.bind(null, bPath)).to.throw();
- expect(oldB).to.equal(initialFileState[bPath]);
- });
-
- it('should clean up old log files', () => {
- rotateOrDeleteFile(bPath);
- const oldB = fs.readFileSync(oldBPath).toString();
-
- expect(fs.accessSync.bind(null, bPath)).to.throw();
- expect(oldB).to.equal(initialFileState[bPath]);
-
- rotateOrDeleteFile(bPath);
-
- expect(fs.accessSync.bind(null, bPath)).to.throw();
- expect(fs.accessSync.bind(null, oldBPath)).to.throw();
- });
-
- it('should only log for the correct log level', () => {
- const logger = new Logger();
-
- const errorSpy = spy();
- const warningSpy = spy();
- const infoSpy = spy();
- const verboseSpy = spy();
- const debugSpy = spy();
-
- logger.addOutput({ level: LogLevel.error, write: errorSpy });
- logger.addOutput({ level: LogLevel.warning, write: warningSpy });
- logger.addOutput({ level: LogLevel.info, write: infoSpy });
- logger.addOutput({ level: LogLevel.verbose, write: verboseSpy });
- logger.addOutput({ level: LogLevel.debug, write: debugSpy });
-
- logger.error();
- logger.warn();
- logger.info();
- logger.verbose();
- logger.debug();
-
- expect(errorSpy).to.have.been.called.exactly(1);
- expect(warningSpy).to.have.been.called.exactly(2);
- expect(infoSpy).to.have.been.called.exactly(3);
- expect(verboseSpy).to.have.been.called.exactly(4);
- expect(debugSpy).to.have.been.called.exactly(5);
- });
-
- it('should format JavaScript types correctly', async () => {
- const logger = new Logger();
-
- const promise = new Promise((resolve, _reject) => {
- logger.addOutput({
- level: LogLevel.info,
- write: (_, message) => resolve(message.replace(/^.*\[info\] /, '')),
- });
- });
-
- logger.info('zero', 'one two', 3, [4, 5, 'six', { seven: 'eight' }], {
- nine: 10,
- eleven: [true],
- });
- await expect(promise).to.eventually.be.fulfilled.then((result) => {
- expect(result).to.equal(
- 'zero one two 3 [4,5,"six",{"seven":"eight"}] {"nine":10,"eleven":[true]}',
- );
- });
- });
-});
diff --git a/gui/test/unit/notification-evaluation.spec.ts b/gui/test/unit/notification-evaluation.spec.ts
deleted file mode 100644
index d05e967efc..0000000000
--- a/gui/test/unit/notification-evaluation.spec.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-import { expect } from 'chai';
-import { describe, it } from 'mocha';
-import sinon from 'sinon';
-
-import NotificationController from '../../src/main/notification-controller';
-import { TunnelState } from '../../src/shared/daemon-rpc-types';
-import { ErrorStateCause } from '../../src/shared/daemon-rpc-types';
-import { FirewallPolicyErrorType } from '../../src/shared/daemon-rpc-types';
-import {
- UnsupportedVersionNotificationProvider,
- UpdateAvailableNotificationProvider,
-} from '../../src/shared/notifications/notification';
-
-function createController() {
- return new NotificationController({
- openApp: () => {
- /* no-op */
- },
- openLink: (_url: string, _withAuth?: boolean) => Promise.resolve(),
- showNotificationIcon: (_value: boolean) => {
- /* no-op */
- },
- });
-}
-
-describe('System notifications', () => {
- let sandbox: sinon.SinonSandbox;
-
- before(() => {
- sandbox = sinon.createSandbox();
- // @ts-expect-error Way too many methods to mock.
- sandbox.stub(NotificationController.prototype, 'createElectronNotification').returns({
- show: () => {
- /* no-op */
- },
- close: () => {
- /* no-op */
- },
- on: () => {
- /* no-op */
- },
- removeAllListeners: () => {
- /* no-op */
- },
- });
- });
-
- it('should evaluate unspupported version notification to show', () => {
- const controller1 = createController();
- const controller2 = createController();
- const notification = new UnsupportedVersionNotificationProvider({
- supported: false,
- consistent: true,
- suggestedUpgrade: '2100.1',
- suggestedIsBeta: false,
- });
-
- expect(notification.mayDisplay()).to.be.true;
-
- const systemNotification = notification.getSystemNotification();
- const result1 = controller1.notify(systemNotification, false, true);
- const result2 = controller2.notify(systemNotification, false, false);
-
- expect(result1).to.be.true;
- expect(result2).to.be.true;
- });
-
- it('should evaluate update available notification to show', () => {
- const controller1 = createController();
- const controller2 = createController();
- const notification = new UpdateAvailableNotificationProvider({
- suggestedUpgrade: '2100.1',
- suggestedIsBeta: false,
- });
-
- expect(notification.mayDisplay()).to.be.true;
-
- const systemNotification = notification.getSystemNotification();
- const result1 = controller1.notify(systemNotification, false, true);
- const result2 = controller2.notify(systemNotification, false, false);
-
- expect(result1).to.be.true;
- expect(result2).to.be.true;
- });
-
- it('should show unsupported version notification only once', () => {
- const controller = createController();
- const notification = new UnsupportedVersionNotificationProvider({
- supported: false,
- consistent: true,
- suggestedUpgrade: '2100.1',
- suggestedIsBeta: false,
- });
-
- const systemNotification = notification.getSystemNotification();
- const result1 = controller.notify(systemNotification, false, true);
- const result2 = controller.notify(systemNotification, false, true);
-
- expect(result1).to.be.true;
- expect(result2).to.be.false;
- });
-
- it('should not show notification when window is open', () => {
- const controller = createController();
- const notification = new UnsupportedVersionNotificationProvider({
- supported: false,
- consistent: true,
- suggestedUpgrade: '2100.1',
- suggestedIsBeta: false,
- });
-
- const systemNotification = notification.getSystemNotification();
- const result = controller.notify(systemNotification, true, true);
-
- expect(result).to.be.false;
- });
-
- it('Tunnel state notifications should respect notification setting', () => {
- const controller = createController();
-
- const disconnectedState: TunnelState = { state: 'disconnected' };
- const connectingState: TunnelState = { state: 'connecting', featureIndicators: undefined };
- const result1 = controller.notifyTunnelState(disconnectedState, false, false, false, true);
- const result2 = controller.notifyTunnelState(disconnectedState, false, false, false, false);
- const result3 = controller.notifyTunnelState(connectingState, false, false, false, true);
- const result4 = controller.notifyTunnelState(connectingState, false, false, false, false);
-
- expect(result1).to.be.true;
- expect(result2).to.be.false;
- expect(result3).to.be.true;
- expect(result4).to.be.false;
-
- const blockingErrorState: TunnelState = {
- state: 'error',
- details: {
- cause: ErrorStateCause.isOffline,
- },
- };
- const result5 = controller.notifyTunnelState(blockingErrorState, false, false, false, false);
- expect(result5).to.be.false;
-
- const nonBlockingErrorState: TunnelState = {
- state: 'error',
- details: {
- cause: ErrorStateCause.isOffline,
- blockingError: {
- type: FirewallPolicyErrorType.generic,
- },
- },
- };
- const result6 = controller.notifyTunnelState(nonBlockingErrorState, false, false, false, false);
- expect(result6).to.be.true;
- });
-});
diff --git a/gui/test/unit/setup.ts b/gui/test/unit/setup.ts
deleted file mode 100644
index a565ec50ca..0000000000
--- a/gui/test/unit/setup.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import chai from 'chai';
-import chaiAsPromised from 'chai-as-promised';
-import spies from 'chai-spies';
-
-chai.use(spies);
-chai.use(chaiAsPromised);
diff --git a/gui/test/unit/tunnel-state.spec.ts b/gui/test/unit/tunnel-state.spec.ts
deleted file mode 100644
index e751b50c57..0000000000
--- a/gui/test/unit/tunnel-state.spec.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { expect, spy } from 'chai';
-import { describe, it } from 'mocha';
-import sinon from 'sinon';
-
-import TunnelStateHandler from '../../src/main/tunnel-state';
-import { TunnelState } from '../../src/shared/daemon-rpc-types';
-
-const connected: TunnelState = { state: 'connected' } as TunnelState;
-const connecting: TunnelState = { state: 'connecting' } as TunnelState;
-const disconnected: TunnelState = { state: 'disconnected' } as TunnelState;
-const disconnecting: TunnelState = { state: 'disconnecting' } as TunnelState;
-const error: TunnelState = { state: 'error' } as TunnelState;
-
-describe('Tunnel state', () => {
- it('Should allow all updates', () => {
- const stateUpdateSpy = spy();
- // @ts-expect-error stateUpdateSpy doesn't know what type to accept
- const handleTunnelStateUpdate = (tunnelState: TunnelState) => stateUpdateSpy(tunnelState.state);
- const tunnelStateHandler = new TunnelStateHandler({ handleTunnelStateUpdate });
-
- tunnelStateHandler.handleNewTunnelState(disconnecting);
- tunnelStateHandler.handleNewTunnelState(connecting);
- tunnelStateHandler.handleNewTunnelState(error);
- tunnelStateHandler.handleNewTunnelState(disconnected);
-
- expect(stateUpdateSpy).to.have.been.called.exactly(4);
- expect(stateUpdateSpy).on.nth(1).to.have.been.called.with.exactly('disconnecting');
- expect(stateUpdateSpy).on.nth(2).to.have.been.called.with.exactly('connecting');
- expect(stateUpdateSpy).on.nth(3).to.have.been.called.with.exactly('error');
- expect(stateUpdateSpy).on.nth(4).to.have.been.called.with.exactly('disconnected');
- expect(tunnelStateHandler.tunnelState.state).to.equal('disconnected');
- });
-
- it('Should ignore non-expected state update', () => {
- const stateUpdateSpy = spy();
- // @ts-expect-error stateUpdateSpy doesn't know what type to accept
- const handleTunnelStateUpdate = (tunnelState: TunnelState) => stateUpdateSpy(tunnelState.state);
- const tunnelStateHandler = new TunnelStateHandler({ handleTunnelStateUpdate });
-
- tunnelStateHandler.expectNextTunnelState('connecting');
- tunnelStateHandler.handleNewTunnelState(disconnecting);
- tunnelStateHandler.handleNewTunnelState(connecting);
-
- expect(stateUpdateSpy).to.have.been.called.exactly(2);
- expect(stateUpdateSpy).on.nth(1).to.have.been.called.with.exactly('connecting');
- expect(stateUpdateSpy).on.nth(2).to.have.been.called.with.exactly('connecting');
- expect(tunnelStateHandler.tunnelState.state).to.equal('connecting');
- });
-
- it('Should allow new states after expected state is reached', () => {
- const stateUpdateSpy = spy();
- // @ts-expect-error stateUpdateSpy doesn't know what type to accept
- const handleTunnelStateUpdate = (tunnelState: TunnelState) => stateUpdateSpy(tunnelState.state);
- const tunnelStateHandler = new TunnelStateHandler({ handleTunnelStateUpdate });
-
- tunnelStateHandler.expectNextTunnelState('connecting');
- tunnelStateHandler.handleNewTunnelState(disconnected);
- tunnelStateHandler.handleNewTunnelState(connecting);
- tunnelStateHandler.handleNewTunnelState(connected);
-
- expect(stateUpdateSpy).to.have.been.called.exactly(3);
- expect(stateUpdateSpy).on.nth(1).to.have.been.called.with.exactly('connecting');
- expect(stateUpdateSpy).on.nth(2).to.have.been.called.with.exactly('connecting');
- expect(stateUpdateSpy).on.nth(3).to.have.been.called.with.exactly('connected');
- expect(tunnelStateHandler.tunnelState.state).to.equal('connected');
- });
-
- it('Should allow error state update', () => {
- const stateUpdateSpy = spy();
- // @ts-expect-error stateUpdateSpy doesn't know what type to accept
- const handleTunnelStateUpdate = (tunnelState: TunnelState) => stateUpdateSpy(tunnelState.state);
- const tunnelStateHandler = new TunnelStateHandler({ handleTunnelStateUpdate });
-
- tunnelStateHandler.expectNextTunnelState('connecting');
- tunnelStateHandler.handleNewTunnelState(disconnected);
- tunnelStateHandler.handleNewTunnelState(error);
- tunnelStateHandler.handleNewTunnelState(disconnected);
-
- expect(stateUpdateSpy).to.have.been.called.exactly(3);
- expect(stateUpdateSpy).on.nth(1).to.have.been.called.with.exactly('connecting');
- expect(stateUpdateSpy).on.nth(2).to.have.been.called.with.exactly('error');
- expect(stateUpdateSpy).on.nth(3).to.have.been.called.with.exactly('disconnected');
- expect(tunnelStateHandler.tunnelState.state).to.equal('disconnected');
- });
-
- it('Should time out and use last ignored state', () => {
- const clock = sinon.useFakeTimers({ shouldAdvanceTime: true });
- const stateUpdateSpy = spy();
- // @ts-expect-error stateUpdateSpy doesn't know what type to accept
- const handleTunnelStateUpdate = (tunnelState: TunnelState) => stateUpdateSpy(tunnelState.state);
- const tunnelStateHandler = new TunnelStateHandler({ handleTunnelStateUpdate });
-
- tunnelStateHandler.expectNextTunnelState('connecting');
- tunnelStateHandler.handleNewTunnelState(disconnected);
- tunnelStateHandler.handleNewTunnelState(connected);
-
- expect(stateUpdateSpy).to.have.been.called.exactly(1);
- expect(stateUpdateSpy).on.nth(1).to.have.been.called.with.exactly('connecting');
- expect(tunnelStateHandler.tunnelState.state).to.equal('connecting');
-
- clock.tick(3000);
-
- expect(stateUpdateSpy).to.have.been.called.exactly(2);
- expect(stateUpdateSpy).on.nth(2).to.have.been.called.with.exactly('connected');
- expect(tunnelStateHandler.tunnelState.state).to.equal('connected');
-
- clock.restore();
- });
-});