summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOliver <oliver@mohlin.dev>2025-09-11 08:12:24 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-22 12:35:44 +0200
commitbf77d7d41a37ad292f6d04e69e34d8b1fcf4f74e (patch)
tree549f1de3c3e077ed84267b57367e7ea8bf2c54fa
parentee7b617a4a1253322d47e63b6b6fc95de959e7c7 (diff)
downloadmullvadvpn-bf77d7d41a37ad292f6d04e69e34d8b1fcf4f74e.tar.xz
mullvadvpn-bf77d7d41a37ad292f6d04e69e34d8b1fcf4f74e.zip
Add tests for clicking feature indicators
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts306
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/helpers.ts53
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/route-object-models/main/selectors.ts4
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.../),
});