summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2024-02-06 16:48:21 +0100
committerOskar Nyberg <oskar@mullvad.net>2024-02-13 11:19:50 +0100
commit41db8f1d197aaa6604ca2169a3f353717ed0af48 (patch)
treeff05ab0f29b83be401f84b3f0ed31be2857d483c
parentb0e4315a003e08f2b11160418e1dccac6f462924 (diff)
downloadmullvadvpn-41db8f1d197aaa6604ca2169a3f353717ed0af48.tar.xz
mullvadvpn-41db8f1d197aaa6604ca2169a3f353717ed0af48.zip
Add GUI test for API access methods
-rw-r--r--gui/src/renderer/components/ApiAccessMethods.tsx2
-rw-r--r--gui/src/renderer/components/ContextMenu.tsx10
-rw-r--r--gui/src/renderer/components/EditApiAccessMethod.tsx8
-rw-r--r--gui/src/renderer/components/cell/SettingsSelect.tsx4
-rw-r--r--gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts169
5 files changed, 189 insertions, 4 deletions
diff --git a/gui/src/renderer/components/ApiAccessMethods.tsx b/gui/src/renderer/components/ApiAccessMethods.tsx
index 0774fcc320..b5cffddc94 100644
--- a/gui/src/renderer/components/ApiAccessMethods.tsx
+++ b/gui/src/renderer/components/ApiAccessMethods.tsx
@@ -231,7 +231,7 @@ function ApiAccessMethod(props: ApiAccessMethodProps) {
}, [props.method.id, props.inUse, setApiAccessMethod, testApiAccessMethod, history.push]);
return (
- <Cell.Row>
+ <Cell.Row data-testid="access-method">
<Cell.LabelContainer>
<StyledNameLabel>{props.method.name}</StyledNameLabel>
{testing && (
diff --git a/gui/src/renderer/components/ContextMenu.tsx b/gui/src/renderer/components/ContextMenu.tsx
index f80b456d46..165c284eca 100644
--- a/gui/src/renderer/components/ContextMenu.tsx
+++ b/gui/src/renderer/components/ContextMenu.tsx
@@ -84,10 +84,18 @@ export function ContextMenuContainer(props: React.PropsWithChildren) {
);
}
+const StyledTrigger = styled.button({
+ borderWidth: 0,
+ padding: 0,
+ margin: 0,
+ cursor: 'default',
+ backgroundColor: 'transparent',
+});
+
export function ContextMenuTrigger(props: React.PropsWithChildren) {
const { toggleVisibility } = useContext(menuContext);
- return <div onClick={toggleVisibility}>{props.children}</div>;
+ return <StyledTrigger onClick={toggleVisibility}>{props.children}</StyledTrigger>;
}
interface StyledMenuProps {
diff --git a/gui/src/renderer/components/EditApiAccessMethod.tsx b/gui/src/renderer/components/EditApiAccessMethod.tsx
index ecb633ae98..b5b3bd6d48 100644
--- a/gui/src/renderer/components/EditApiAccessMethod.tsx
+++ b/gui/src/renderer/components/EditApiAccessMethod.tsx
@@ -434,7 +434,13 @@ function EditShadowsocks(props: EditMethodProps<ShadowsocksAccessMethod>) {
</SettingsRow>
<SettingsRow label={messages.gettext('Cipher')}>
- <SettingsSelect direction="up" defaultValue={cipher} onUpdate={setCipher} items={ciphers} />
+ <SettingsSelect
+ data-testid="ciphers"
+ direction="up"
+ defaultValue={cipher}
+ onUpdate={setCipher}
+ items={ciphers}
+ />
</SettingsRow>
</SettingsGroup>
);
diff --git a/gui/src/renderer/components/cell/SettingsSelect.tsx b/gui/src/renderer/components/cell/SettingsSelect.tsx
index dd5f9b3b7e..4286cda1e2 100644
--- a/gui/src/renderer/components/cell/SettingsSelect.tsx
+++ b/gui/src/renderer/components/cell/SettingsSelect.tsx
@@ -87,6 +87,8 @@ interface SettingsSelectProps<T extends string> {
items: Array<SettingsSelectItem<T>>;
onUpdate: (value: T) => void;
direction?: 'down' | 'up';
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'data-testid'?: string;
}
export function SettingsSelect<T extends string>(props: SettingsSelectProps<T>) {
@@ -142,7 +144,7 @@ export function SettingsSelect<T extends string>(props: SettingsSelectProps<T>)
return (
<AriaInput>
<StyledSelect onBlur={closeDropdown} onKeyDown={onKeyDown} role="listbox">
- <StyledSelectedContainer onClick={toggleDropdown}>
+ <StyledSelectedContainer data-testid={props['data-testid']} onClick={toggleDropdown}>
<StyledSelectedContainerInner>
<StyledSelectedText>
{props.items.find((item) => item.value === value)?.label ?? ''}
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
new file mode 100644
index 0000000000..45f8d98cee
--- /dev/null
+++ b/gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts
@@ -0,0 +1,169 @@
+import { expect, test } from '@playwright/test';
+import { Page } from 'playwright';
+
+import { startInstalledApp } from '../installed-utils';
+import { TestUtils } from '../../utils';
+import { RoutePath } from '../../../../src/renderer/lib/routes';
+
+// 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 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(async () => await page.click('button[aria-label="Settings"]'));
+ await util.waitForNavigation(async () => await 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(2);
+
+ const direct = accessMethods.first();
+ const bridges = accessMethods.last();
+ await expect(direct).toContainText(DIRECT_NAME);
+ await expect(bridges).toContainText(BRIDGES_NAME);
+ await expect(page.getByText(IN_USE_LABEL)).toHaveCount(1);
+});
+
+test('App should add invalid access method', async () => {
+ await util.waitForNavigation(async () => await 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(async () => await page.locator('button:has-text("Save")').click())
+ ).toEqual(RoutePath.apiAccessMethods);
+
+ const accessMethods = page.getByTestId('access-method');
+ await expect(accessMethods).toHaveCount(3);
+
+ 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! }).click();
+
+ expect(
+ await util.waitForNavigation(async () => await saveButton.click())
+ ).toEqual(RoutePath.apiAccessMethods);
+
+ const accessMethods = page.getByTestId('access-method');
+ await expect(accessMethods).toHaveCount(3);
+
+ 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 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(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();
+ await expect(accessMethods).toHaveCount(2);
+});