Skip to content

Commit d73a3cc

Browse files
committed
Add GUI test for API access methods
1 parent 15b5cb5 commit d73a3cc

File tree

5 files changed

+189
-4
lines changed

5 files changed

+189
-4
lines changed

gui/src/renderer/components/ApiAccessMethods.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ function ApiAccessMethod(props: ApiAccessMethodProps) {
231231
}, [props.method.id, props.inUse, setApiAccessMethod, testApiAccessMethod, history.push]);
232232

233233
return (
234-
<Cell.Row>
234+
<Cell.Row data-testid="access-method">
235235
<Cell.LabelContainer>
236236
<StyledNameLabel>{props.method.name}</StyledNameLabel>
237237
{testing && (

gui/src/renderer/components/ContextMenu.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,18 @@ export function ContextMenuContainer(props: React.PropsWithChildren) {
8484
);
8585
}
8686

87+
const StyledTrigger = styled.button({
88+
borderWidth: 0,
89+
padding: 0,
90+
margin: 0,
91+
cursor: 'default',
92+
backgroundColor: 'transparent',
93+
});
94+
8795
export function ContextMenuTrigger(props: React.PropsWithChildren) {
8896
const { toggleVisibility } = useContext(menuContext);
8997

90-
return <div onClick={toggleVisibility}>{props.children}</div>;
98+
return <StyledTrigger onClick={toggleVisibility}>{props.children}</StyledTrigger>;
9199
}
92100

93101
interface StyledMenuProps {

gui/src/renderer/components/EditApiAccessMethod.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,13 @@ function EditShadowsocks(props: EditMethodProps<ShadowsocksAccessMethod>) {
434434
</SettingsRow>
435435

436436
<SettingsRow label={messages.gettext('Cipher')}>
437-
<SettingsSelect direction="up" defaultValue={cipher} onUpdate={setCipher} items={ciphers} />
437+
<SettingsSelect
438+
data-testid="ciphers"
439+
direction="up"
440+
defaultValue={cipher}
441+
onUpdate={setCipher}
442+
items={ciphers}
443+
/>
438444
</SettingsRow>
439445
</SettingsGroup>
440446
);

gui/src/renderer/components/cell/SettingsSelect.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ interface SettingsSelectProps<T extends string> {
8787
items: Array<SettingsSelectItem<T>>;
8888
onUpdate: (value: T) => void;
8989
direction?: 'down' | 'up';
90+
// eslint-disable-next-line @typescript-eslint/naming-convention
91+
'data-testid'?: string;
9092
}
9193

9294
export function SettingsSelect<T extends string>(props: SettingsSelectProps<T>) {
@@ -142,7 +144,7 @@ export function SettingsSelect<T extends string>(props: SettingsSelectProps<T>)
142144
return (
143145
<AriaInput>
144146
<StyledSelect onBlur={closeDropdown} onKeyDown={onKeyDown} role="listbox">
145-
<StyledSelectedContainer onClick={toggleDropdown}>
147+
<StyledSelectedContainer data-testid={props['data-testid']} onClick={toggleDropdown}>
146148
<StyledSelectedContainerInner>
147149
<StyledSelectedText>
148150
{props.items.find((item) => item.value === value)?.label ?? ''}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { expect, test } from '@playwright/test';
2+
import { Page } from 'playwright';
3+
4+
import { startInstalledApp } from '../installed-utils';
5+
import { TestUtils } from '../../utils';
6+
import { RoutePath } from '../../../../src/renderer/lib/routes';
7+
8+
// This test expects the daemon to be logged in and only have "Direct" and "Mullvad Bridges"
9+
// access methods.
10+
// Env parameters:
11+
// `SHADOWSOCKS_SERVER_IP`: IP of the shadowsocks server to connect to.
12+
13+
const DIRECT_NAME = 'Direct';
14+
const BRIDGES_NAME = 'Mullvad Bridges';
15+
const IN_USE_LABEL = 'In use';
16+
const FUNCTIONING_METHOD_NAME = 'Test method';
17+
const NON_FUNCTIONING_METHOD_NAME = 'Non functioning test method';
18+
19+
let page: Page;
20+
let util: TestUtils;
21+
22+
test.beforeAll(async () => {
23+
({ page, util } = await startInstalledApp());
24+
});
25+
26+
test.afterAll(async () => {
27+
await page.close();
28+
});
29+
30+
async function navigateToAccessMethods() {
31+
await util.waitForNavigation(async () => await page.click('button[aria-label="Settings"]'));
32+
await util.waitForNavigation(async () => await page.getByText('API access').click());
33+
34+
const title = page.locator('h1')
35+
await expect(title).toHaveText('API access');
36+
}
37+
38+
test('App should display access methods', async () => {
39+
await navigateToAccessMethods();
40+
41+
const accessMethods = page.getByTestId('access-method');
42+
await expect(accessMethods).toHaveCount(2);
43+
44+
const direct = accessMethods.first();
45+
const bridges = accessMethods.last();
46+
await expect(direct).toContainText(DIRECT_NAME);
47+
await expect(direct).toContainText(IN_USE_LABEL);
48+
await expect(bridges).toHaveText(BRIDGES_NAME);
49+
await expect(bridges).not.toContainText(IN_USE_LABEL);
50+
});
51+
52+
test('App should add invalid access method', async () => {
53+
await util.waitForNavigation(async () => await page.locator('button:has-text("Add")').click());
54+
55+
const title = page.locator('h1')
56+
await expect(title).toHaveText('Add method');
57+
58+
const inputs = page.locator('input');
59+
const addButton = page.locator('button:has-text("Add")');
60+
await expect(addButton).toBeVisible();
61+
await expect(addButton).toBeDisabled();
62+
63+
await inputs.first().fill(NON_FUNCTIONING_METHOD_NAME);
64+
await expect(addButton).toBeDisabled();
65+
66+
await inputs.nth(1).fill(process.env.SHADOWSOCKS_SERVER_IP!);
67+
await expect(addButton).toBeDisabled();
68+
69+
await inputs.nth(2).fill('443');
70+
await expect(addButton).toBeEnabled();
71+
72+
await addButton.click()
73+
74+
await expect(page.getByText('Testing method...')).toBeVisible();
75+
await expect(page.getByText('API unreachable, add anyway?')).toBeVisible();
76+
77+
expect(
78+
await util.waitForNavigation(async () => await page.locator('button:has-text("Save")').click())
79+
).toEqual(RoutePath.apiAccessMethods);
80+
81+
const accessMethods = page.getByTestId('access-method');
82+
await expect(accessMethods).toHaveCount(3);
83+
84+
await expect(accessMethods.last()).toHaveText(NON_FUNCTIONING_METHOD_NAME);
85+
});
86+
87+
test('App should use invalid method', async () => {
88+
const accessMethods = page.getByTestId('access-method');
89+
90+
const direct = accessMethods.first();
91+
const bridges = accessMethods.nth(1);
92+
const nonFunctioningTestMethod = accessMethods.last();
93+
94+
await expect(direct).toContainText(DIRECT_NAME);
95+
await expect(direct).toContainText(IN_USE_LABEL);
96+
await expect(bridges).toHaveText(BRIDGES_NAME);
97+
98+
await nonFunctioningTestMethod.locator('button').last().click();
99+
await nonFunctioningTestMethod.getByText('Use').click();
100+
await expect(nonFunctioningTestMethod).toContainText('Testing...');
101+
await expect(nonFunctioningTestMethod).toContainText('API unreachable');
102+
await expect(direct).toContainText(IN_USE_LABEL);
103+
});
104+
105+
test('App should edit access method', async () => {
106+
const customMethod = page.getByTestId('access-method').last();
107+
await customMethod.locator('button').last().click();
108+
await util.waitForNavigation(() => customMethod.getByText('Edit').click());
109+
110+
const title = page.locator('h1')
111+
await expect(title).toHaveText('Edit method');
112+
113+
const inputs = page.locator('input');
114+
const saveButton = page.locator('button:has-text("Save")');
115+
await expect(saveButton).toBeVisible();
116+
await expect(saveButton).toBeEnabled();
117+
118+
await expect(inputs.first()).toHaveValue(NON_FUNCTIONING_METHOD_NAME);
119+
await expect(inputs.nth(1)).toHaveValue(process.env.SHADOWSOCKS_SERVER_IP!);
120+
await expect(inputs.nth(2)).toHaveValue('443');
121+
122+
await inputs.first().fill(FUNCTIONING_METHOD_NAME);
123+
await expect(saveButton).toBeEnabled();
124+
125+
await inputs.nth(3).fill('mullvad');
126+
127+
await page.getByTestId('ciphers').click();
128+
await page.getByRole('option', { name: 'aes-256-gcm' }).click();
129+
130+
expect(
131+
await util.waitForNavigation(async () => await saveButton.click())
132+
).toEqual(RoutePath.apiAccessMethods);
133+
134+
const accessMethods = page.getByTestId('access-method');
135+
await expect(accessMethods).toHaveCount(3);
136+
137+
await expect(accessMethods.last()).toHaveText(FUNCTIONING_METHOD_NAME);
138+
});
139+
140+
test('App should use valid method', async () => {
141+
const accessMethods = page.getByTestId('access-method');
142+
143+
const direct = accessMethods.first();
144+
const bridges = accessMethods.nth(1);
145+
const functioningTestMethod = accessMethods.last();
146+
147+
await expect(direct).toContainText(DIRECT_NAME);
148+
await expect(direct).toContainText(IN_USE_LABEL);
149+
await expect(bridges).toHaveText(BRIDGES_NAME);
150+
await expect(functioningTestMethod).toHaveText(FUNCTIONING_METHOD_NAME);
151+
152+
await functioningTestMethod.locator('button').last().click();
153+
await functioningTestMethod.getByText('Use').click();
154+
await expect(direct).not.toContainText(IN_USE_LABEL);
155+
await expect(functioningTestMethod).toContainText('API reachable');
156+
await expect(functioningTestMethod).toContainText(IN_USE_LABEL);
157+
});
158+
159+
test('App should delete method', async () => {
160+
const accessMethods = page.getByTestId('access-method');
161+
const customMethod = accessMethods.last();
162+
163+
await customMethod.locator('button').last().click();
164+
await customMethod.getByText('Delete').click();
165+
166+
await expect(page.getByText(`Delete ${FUNCTIONING_METHOD_NAME}?`)).toBeVisible();
167+
await page.locator('button:has-text("Delete")').click();
168+
await expect(accessMethods).toHaveCount(2);
169+
});

0 commit comments

Comments
 (0)