diff options
| author | Oskar <oskar@mullvad.net> | 2024-11-05 07:57:08 +0100 |
|---|---|---|
| committer | Oskar <oskar@mullvad.net> | 2024-11-14 16:43:18 +0100 |
| commit | 84f14d79c4f0dde73337820ec94ba8ff928a3797 (patch) | |
| tree | ce468658e5ba7b0a74950c7ad1b09b3a4d00520b /gui/test | |
| parent | e3ce0eb5cd0610dbff6ec98cb8cb388415c74bf6 (diff) | |
| download | mullvadvpn-84f14d79c4f0dde73337820ec94ba8ff928a3797.tar.xz mullvadvpn-84f14d79c4f0dde73337820ec94ba8ff928a3797.zip | |
Move gui directory to desktop/packages/mullvad-vpn
Diffstat (limited to 'gui/test')
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(); - }); -}); |
