Skip to content

Commit 620dd44

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

File tree

5 files changed

+184
-4
lines changed

5 files changed

+184
-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,164 @@
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 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+
expect(addButton).toBeDisabled();
61+
62+
await inputs.first().fill(FUNCTIONING_METHOD_NAME);
63+
expect(addButton).toBeDisabled();
64+
65+
await inputs.nth(1).fill(process.env.SHADOWSOCKS_SERVER_IP!);
66+
expect(addButton).toBeDisabled();
67+
68+
await inputs.nth(2).fill('443');
69+
expect(addButton).toBeEnabled();
70+
71+
await inputs.nth(3).fill('mullvad');
72+
73+
await page.getByTestId('ciphers').click();
74+
await page.getByRole('option', { name: 'aes-256-gcm' }).click();
75+
76+
expect(
77+
await util.waitForNavigation(async () => await addButton.click())
78+
).toEqual(RoutePath.apiAccessMethods);
79+
80+
const accessMethods = page.getByTestId('access-method');
81+
await expect(accessMethods).toHaveCount(3);
82+
83+
await expect(accessMethods.last()).toHaveText(FUNCTIONING_METHOD_NAME);
84+
});
85+
86+
test('App should add invalid access method', async () => {
87+
await util.waitForNavigation(async () => await page.locator('button:has-text("Add")').click());
88+
89+
const title = page.locator('h1')
90+
await expect(title).toHaveText('Add method');
91+
92+
const inputs = page.locator('input');
93+
const addButton = page.locator('button:has-text("Add")');
94+
expect(addButton).toBeDisabled();
95+
96+
await inputs.first().fill(NON_FUNCTIONING_METHOD_NAME);
97+
expect(addButton).toBeDisabled();
98+
99+
await inputs.nth(1).fill(process.env.SHADOWSOCKS_SERVER_IP!);
100+
expect(addButton).toBeDisabled();
101+
102+
await inputs.nth(2).fill('443');
103+
expect(addButton).toBeEnabled();
104+
105+
await addButton.click()
106+
107+
await expect(page.getByText('Testing method...')).toBeVisible();
108+
await expect(page.getByText('API unreachable, add anyway?')).toBeVisible();
109+
110+
expect(
111+
await util.waitForNavigation(async () => await page.locator('button:has-text("Save")').click())
112+
).toEqual(RoutePath.apiAccessMethods);
113+
114+
const accessMethods = page.getByTestId('access-method');
115+
await expect(accessMethods).toHaveCount(4);
116+
117+
await expect(accessMethods.last()).toHaveText(NON_FUNCTIONING_METHOD_NAME);
118+
});
119+
120+
test('App should test and use methods', async () => {
121+
const accessMethods = page.getByTestId('access-method');
122+
123+
const direct = accessMethods.first();
124+
const bridges = accessMethods.nth(1);
125+
const functioningTestMethod = accessMethods.nth(2);
126+
const nonFunctioningTestMethod = accessMethods.last();
127+
128+
await expect(direct).toContainText(DIRECT_NAME);
129+
await expect(direct).toContainText(IN_USE_LABEL);
130+
await expect(bridges).toHaveText(BRIDGES_NAME);
131+
await expect(functioningTestMethod).toHaveText(FUNCTIONING_METHOD_NAME);
132+
133+
await functioningTestMethod.locator('button').last().click();
134+
await functioningTestMethod.getByText('Use').click();
135+
await expect(direct).not.toContainText(IN_USE_LABEL);
136+
await expect(functioningTestMethod).toContainText('API reachable');
137+
await expect(functioningTestMethod).toContainText(IN_USE_LABEL);
138+
139+
await nonFunctioningTestMethod.locator('button').last().click();
140+
await nonFunctioningTestMethod.getByText('Use').click();
141+
await expect(nonFunctioningTestMethod).toContainText('Testing...');
142+
await expect(nonFunctioningTestMethod).toContainText('API unreachable');
143+
await expect(functioningTestMethod).toContainText(IN_USE_LABEL);
144+
});
145+
146+
test('App should delete method', async () => {
147+
const accessMethods = page.getByTestId('access-method');
148+
const functioningTestMethod = accessMethods.nth(2);
149+
const nonFunctioningTestMethod = accessMethods.last();
150+
151+
await nonFunctioningTestMethod.locator('button').last().click();
152+
await nonFunctioningTestMethod.getByText('Delete').click();
153+
154+
await expect(page.getByText(`Delete ${NON_FUNCTIONING_METHOD_NAME}?`)).toBeVisible();
155+
await page.locator('button:has-text("Delete")').click();
156+
await expect(accessMethods).toHaveCount(3);
157+
158+
await functioningTestMethod.locator('button').last().click();
159+
await functioningTestMethod.getByText('Delete').click();
160+
161+
await expect(page.getByText(`Delete ${FUNCTIONING_METHOD_NAME}?`)).toBeVisible();
162+
await page.locator('button:has-text("Delete")').click();
163+
await expect(accessMethods).toHaveCount(2);
164+
});

0 commit comments

Comments
 (0)