diff options
| author | Oliver <oliver@mohlin.dev> | 2025-09-11 08:12:24 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-22 12:35:44 +0200 |
| commit | bf77d7d41a37ad292f6d04e69e34d8b1fcf4f74e (patch) | |
| tree | 549f1de3c3e077ed84267b57367e7ea8bf2c54fa | |
| parent | ee7b617a4a1253322d47e63b6b6fc95de959e7c7 (diff) | |
| download | mullvadvpn-bf77d7d41a37ad292f6d04e69e34d8b1fcf4f74e.tar.xz mullvadvpn-bf77d7d41a37ad292f6d04e69e34d8b1fcf4f74e.zip | |
Add tests for clicking feature indicators
3 files changed, 283 insertions, 80 deletions
diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts index 9043fedff5..98b43bcf7f 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts @@ -1,39 +1,152 @@ -import { expect, test } from '@playwright/test'; +import { expect, Locator, test } from '@playwright/test'; import { Page } from 'playwright'; -import { - FeatureIndicator, - ILocation, - ITunnelEndpoint, -} from '../../../../src/shared/daemon-rpc-types'; +import { FeatureIndicator } from '../../../../src/shared/daemon-rpc-types'; import { RoutePath } from '../../../../src/shared/routes'; -import { expectConnected } from '../../shared/tunnel-state'; +import { RoutesObjectModel } from '../../route-object-models'; import { MockedTestUtils, startMockedApp } from '../mocked-utils'; +import { createHelpers, FeatureIndicatorsHelpers } from './helpers'; -const endpoint: ITunnelEndpoint = { - address: 'wg10:80', - protocol: 'tcp', - quantumResistant: false, - tunnelType: 'wireguard', - daita: false, +let page: Page; +let util: MockedTestUtils; +let routes: RoutesObjectModel; +let helpers: FeatureIndicatorsHelpers; + +type FeatureIndicatorTestOption = { + testId: string; + featureIndicator: FeatureIndicator; + featureIndicatorLabel: string; + route: RoutePath; }; -const mockDisconnectedLocation: ILocation = { - country: 'Sweden', - city: 'Gothenburg', - latitude: 58, - longitude: 12, - mullvadExitIp: false, +type FeatureIndicatorWithOptionTestOption = FeatureIndicatorTestOption & { + option: { + name?: string; + type?: 'switch' | 'listbox' | 'accordion' | 'input'; + }; }; -const mockConnectedLocation: ILocation = { ...mockDisconnectedLocation, mullvadExitIp: true }; +const featureIndicatorWithoutOption: FeatureIndicatorTestOption[] = [ + { + testId: 'DAITA multihop', + featureIndicator: FeatureIndicator.daitaMultihop, + route: RoutePath.daitaSettings, + featureIndicatorLabel: 'DAITA: Multihop', + }, + { + testId: 'split tunneling', + featureIndicator: FeatureIndicator.splitTunneling, + route: RoutePath.splitTunneling, + featureIndicatorLabel: 'Split tunneling', + }, + { + testId: 'server ip override', + featureIndicator: FeatureIndicator.serverIpOverride, + route: RoutePath.settingsImport, + featureIndicatorLabel: 'Server ip override', + }, +]; -let page: Page; -let util: MockedTestUtils; +const featureIndicatorWithOption: FeatureIndicatorWithOptionTestOption[] = [ + { + testId: 'DAITA', + featureIndicator: FeatureIndicator.daita, + route: RoutePath.daitaSettings, + featureIndicatorLabel: 'DAITA', + option: { name: 'Enable', type: 'switch' }, + }, + { + testId: 'UDP over TCP', + featureIndicator: FeatureIndicator.udp2tcp, + route: RoutePath.wireguardSettings, + featureIndicatorLabel: 'Obfuscation', + option: { name: 'Obfuscation', type: 'listbox' }, + }, + { + testId: 'shadowsocks', + featureIndicator: FeatureIndicator.shadowsocks, + route: RoutePath.wireguardSettings, + featureIndicatorLabel: 'Obfuscation', + option: { name: 'Obfuscation', type: 'listbox' }, + }, + { + testId: 'QUIC', + featureIndicator: FeatureIndicator.quic, + route: RoutePath.wireguardSettings, + featureIndicatorLabel: 'Obfuscation', + option: { name: 'Obfuscation', type: 'listbox' }, + }, + { + testId: 'multihop', + featureIndicator: FeatureIndicator.multihop, + route: RoutePath.multihopSettings, + featureIndicatorLabel: 'Multihop', + option: { name: 'Enable', type: 'switch' }, + }, + { + testId: 'custom dns', + featureIndicator: FeatureIndicator.customDns, + route: RoutePath.vpnSettings, + featureIndicatorLabel: 'Custom DNS', + option: { name: 'Use custom DNS server', type: 'switch' }, + }, + { + testId: 'MTU', + featureIndicator: FeatureIndicator.customMtu, + route: RoutePath.wireguardSettings, + featureIndicatorLabel: 'MTU', + option: { name: 'MTU', type: 'input' }, + }, + { + testId: 'bridge mode', + featureIndicator: FeatureIndicator.bridgeMode, + route: RoutePath.openVpnSettings, + featureIndicatorLabel: 'Bridge mode', + option: { name: 'Bridge mode', type: 'listbox' }, + }, + { + testId: 'local network sharing', + featureIndicator: FeatureIndicator.lanSharing, + route: RoutePath.vpnSettings, + featureIndicatorLabel: 'Local network sharing', + option: { name: 'Local network sharing', type: 'switch' }, + }, + { + testId: 'Mssfix', + featureIndicator: FeatureIndicator.customMssFix, + route: RoutePath.openVpnSettings, + featureIndicatorLabel: 'Mssfix', + option: { name: 'Mssfix', type: 'input' }, + }, + { + testId: 'lockdown mode', + featureIndicator: FeatureIndicator.lockdownMode, + route: RoutePath.vpnSettings, + featureIndicatorLabel: 'Lockdown mode', + option: { name: 'Lockdown mode', type: 'switch' }, + }, + { + testId: 'quantum resistance', + featureIndicator: FeatureIndicator.quantumResistance, + route: RoutePath.wireguardSettings, + featureIndicatorLabel: 'Quantum resistance', + option: { name: 'Quantum-resistant tunnel', type: 'listbox' }, + }, + { + testId: 'dns content blockers', + featureIndicator: FeatureIndicator.dnsContentBlockers, + route: RoutePath.vpnSettings, + featureIndicatorLabel: 'DNS content blockers', + option: { name: 'DNS content blockers', type: 'accordion' }, + }, +]; test.describe('Feature indicators', () => { test.beforeAll(async () => { ({ page, util } = await startMockedApp()); + routes = new RoutesObjectModel(page, util); + helpers = createHelpers({ page, routes, utils: util }); + await util.waitForRoute(RoutePath.main); }); @@ -41,56 +154,72 @@ test.describe('Feature indicators', () => { await page.close(); }); - test('App should show no feature indicators', async () => { - await util.ipc.tunnel[''].notify({ - state: 'connected', - details: { endpoint, location: mockConnectedLocation }, - featureIndicators: undefined, - }); + test.afterEach(async () => { + await helpers.disconnect(); + await routes.wireguardSettings.gotoRoot(); + await util.waitForRoute(RoutePath.main); + }); + + async function expectFeatureIndicators(expectedIndicators: Array<string>, only = true) { + const indicators = routes.main.selectors.featureIndicators(); + if (only) { + await expect(indicators).toHaveCount(expectedIndicators.length); + } - await expectConnected(page); - await expectFeatureIndicators(page, []); + for (const indicator of expectedIndicators) { + await expect(routes.main.selectors.featureIndicator(indicator)).toBeVisible(); + } + } + + test('Should show no feature indicators when disconnected', async () => { + await expectFeatureIndicators([]); + await helpers.connectWithFeatures(undefined); - const ellipsis = page.getByText(/^\d more.../); + await expectFeatureIndicators([]); + + const ellipsis = routes.main.selectors.moreFeatureIndicator(); await expect(ellipsis).not.toBeVisible(); await page.getByTestId('connection-panel-chevron').click(); await expect(ellipsis).not.toBeVisible(); - await expectFeatureIndicators(page, []); + await expectFeatureIndicators([]); await page.getByTestId('connection-panel-chevron').click(); }); - test('App should show feature indicators', async () => { - await util.ipc.tunnel[''].notify({ - 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, - ], - }); + test('Should show no feature indicators when connected with no active features', async () => { + await helpers.connectWithFeatures(undefined); + await expectFeatureIndicators([]); - // Make sure panel is collapsed before checking indicator visibility. - const ellipsis = page.getByText(/^\d more.../); - await expect(ellipsis).toBeVisible(); + const ellipsis = routes.main.selectors.moreFeatureIndicator(); + await expect(ellipsis).not.toBeVisible(); + }); - await expectConnected(page); - await expectFeatureIndicators(page, ['DAITA', 'Quantum resistance'], false); - await expectHiddenFeatureIndicator(page, 'Mssfix'); + test('Should show feature indicators when connected with active features', async () => { + await helpers.connectWithFeatures([FeatureIndicator.daita, FeatureIndicator.quantumResistance]); + await expectFeatureIndicators(['DAITA', 'Quantum Resistance']); + }); - await page.getByTestId('connection-panel-chevron').click(); - await expect(ellipsis).not.toBeVisible(); + test('Should show a subset of feature indicators when connected with many active features', async () => { + await helpers.connectWithFeatures([ + FeatureIndicator.daita, + FeatureIndicator.udp2tcp, + FeatureIndicator.customMssFix, + FeatureIndicator.customMtu, + FeatureIndicator.lanSharing, + FeatureIndicator.serverIpOverride, + FeatureIndicator.customDns, + FeatureIndicator.lockdownMode, + FeatureIndicator.quantumResistance, + FeatureIndicator.multihop, + ]); + + const ellipsis = routes.main.selectors.moreFeatureIndicator(); + await expect(ellipsis).toBeVisible(); + + await ellipsis.click(); - await expectFeatureIndicators(page, [ + await expectFeatureIndicators([ 'DAITA', 'Quantum resistance', 'Mssfix', @@ -104,30 +233,47 @@ test.describe('Feature indicators', () => { ]); }); - async function expectHiddenFeatureIndicator(page: Page, hiddenIndicator: string) { - const indicators = page.getByTestId('feature-indicator'); - const indicator = indicators.getByText(hiddenIndicator, { exact: true }); + const clickFeatureIndicator = async (featureIndicatorLabel: string, route: RoutePath) => { + const indicator = routes.main.selectors.featureIndicator(featureIndicatorLabel); + await expect(indicator).toBeVisible(); + await indicator.click(); + await util.waitForRoute(route); + }; - // Make sure at least one is visible to not run the "not visible" check before they become - // visible. - await expect(indicators.first()).toBeVisible(); + featureIndicatorWithoutOption.forEach( + ({ testId, featureIndicator, route, featureIndicatorLabel }) => { + test(`Should navigate to setting when clicking on ${testId} feature indicator`, async () => { + await helpers.connectWithFeatures([featureIndicator]); + await clickFeatureIndicator(featureIndicatorLabel, route); - await expect(indicator).toHaveCount(1); - await expect(indicator).not.toBeVisible(); - } + const currentRoute = await util.currentRoute(); + expect(currentRoute).toBe(route); + }); + }, + ); - async function expectFeatureIndicators( - page: Page, - expectedIndicators: Array<string>, - only = true, - ) { - const indicators = page.getByTestId('feature-indicator'); - if (only) { - await expect(indicators).toHaveCount(expectedIndicators.length); - } + featureIndicatorWithOption.forEach( + ({ testId, featureIndicator, route, featureIndicatorLabel, option }) => { + test(`Should navigate to setting when clicking on ${testId} feature indicator`, async () => { + await helpers.connectWithFeatures([featureIndicator]); + await clickFeatureIndicator(featureIndicatorLabel, route); - for (const indicator of expectedIndicators) { - await expect(indicators.getByText(indicator, { exact: true })).toBeVisible(); - } - } + const { name, type } = option; + let element: Locator | undefined = undefined; + if (type === 'accordion') { + element = page.getByRole('button', { name }); + } else if (type === 'listbox') { + element = page.getByRole('listbox', { name }); + } else if (type === 'input') { + element = page.getByRole('textbox', { name }); + } else { + element = page.getByRole('switch', { name }); + } + await expect(element).toBeInViewport(); + + const currentRoute = await util.currentRoute(); + expect(currentRoute).toBe(route); + }); + }, + ); }); diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/helpers.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/helpers.ts new file mode 100644 index 0000000000..2b7b9e3762 --- /dev/null +++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/helpers.ts @@ -0,0 +1,53 @@ +import { Page } from 'playwright'; + +import { + FeatureIndicator, + ILocation, + ITunnelEndpoint, +} from '../../../../src/shared/daemon-rpc-types'; +import { RoutesObjectModel } from '../../route-object-models'; +import { MockedTestUtils } 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 }; + +export const createHelpers = ({ + utils, +}: { + page: Page; + routes: RoutesObjectModel; + utils: MockedTestUtils; +}) => { + const connectWithFeatures = async (featureIndicators: FeatureIndicator[] | undefined) => { + await utils.ipc.tunnel[''].notify({ + state: 'connected', + details: { endpoint, location: mockConnectedLocation }, + featureIndicators: featureIndicators, + }); + }; + + const disconnect = () => + utils.ipc.tunnel[''].notify({ + state: 'disconnected', + lockedDown: false, + }); + + return { connectWithFeatures, disconnect }; +}; + +export type FeatureIndicatorsHelpers = ReturnType<typeof createHelpers>; diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/main/selectors.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/main/selectors.ts index 0e3cbaaa3b..8a0d96ac18 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/main/selectors.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/main/selectors.ts @@ -5,4 +5,8 @@ export const createSelectors = (page: Page) => ({ selectLocationButton: () => page.getByLabel('Select location'), connectionPanelChevronButton: () => page.getByTestId('connection-panel-chevron'), inIpLabel: () => page.getByTestId('in-ip'), + featureIndicators: () => page.getByTestId('feature-indicator'), + featureIndicator: (name: string) => + page.getByTestId('feature-indicator').filter({ hasText: name }), + moreFeatureIndicator: () => page.getByText(/^\d more.../), }); |
