Skip to content

Commit e79f77a

Browse files
committed
Merge branch 'add-api-access-method-gui-tests-des-608'
2 parents b0e4315 + 41db8f1 commit e79f77a

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`
12+
// `SHADOWSOCKS_SERVER_PORT`
13+
// `SHADOWSOCKS_SERVER_CIPHER`
14+
// `SHADOWSOCKS_SERVER_PASSWORD`
15+
16+
const DIRECT_NAME = 'Direct';
17+
const BRIDGES_NAME = 'Mullvad Bridges';
18+
const IN_USE_LABEL = 'In use';
19+
const FUNCTIONING_METHOD_NAME = 'Test method';
20+
const NON_FUNCTIONING_METHOD_NAME = 'Non functioning test method';
21+
22+
let page: Page;
23+
let util: TestUtils;
24+
25+
test.beforeAll(async () => {
26+
({ page, util } = await startInstalledApp());
27+
});
28+
29+
test.afterAll(async () => {
30+
await page.close();
31+
});
32+
33+
async function navigateToAccessMethods() {
34+
await util.waitForNavigation(async () => await page.click('button[aria-label="Settings"]'));
35+
await util.waitForNavigation(async () => await page.getByText('API access').click());
36+
37+
const title = page.locator('h1')
38+
await expect(title).toHaveText('API access');
39+
}
40+
41+
test('App should display access methods', async () => {
42+
await navigateToAccessMethods();
43+
44+
const accessMethods = page.getByTestId('access-method');
45+
await expect(accessMethods).toHaveCount(2);
46+
47+
const direct = accessMethods.first();
48+
const bridges = accessMethods.last();
49+
await expect(direct).toContainText(DIRECT_NAME);
50+
await expect(bridges).toContainText(BRIDGES_NAME);
51+
await expect(page.getByText(IN_USE_LABEL)).toHaveCount(1);
52+
});
53+
54+
test('App should add invalid access method', async () => {
55+
await util.waitForNavigation(async () => await page.locator('button:has-text("Add")').click());
56+
57+
const title = page.locator('h1')
58+
await expect(title).toHaveText('Add method');
59+
60+
const inputs = page.locator('input');
61+
const addButton = page.locator('button:has-text("Add")');
62+
await expect(addButton).toBeVisible();
63+
await expect(addButton).toBeDisabled();
64+
65+
await inputs.first().fill(NON_FUNCTIONING_METHOD_NAME);
66+
await expect(addButton).toBeDisabled();
67+
68+
await inputs.nth(1).fill(process.env.SHADOWSOCKS_SERVER_IP!);
69+
await expect(addButton).toBeDisabled();
70+
71+
await inputs.nth(2).fill(process.env.SHADOWSOCKS_SERVER_PORT!);
72+
await expect(addButton).toBeEnabled();
73+
74+
await addButton.click()
75+
76+
await expect(page.getByText('Testing method...')).toBeVisible();
77+
await expect(page.getByText('API unreachable, add anyway?')).toBeVisible();
78+
79+
expect(
80+
await util.waitForNavigation(async () => await page.locator('button:has-text("Save")').click())
81+
).toEqual(RoutePath.apiAccessMethods);
82+
83+
const accessMethods = page.getByTestId('access-method');
84+
await expect(accessMethods).toHaveCount(3);
85+
86+
await expect(accessMethods.last()).toHaveText(NON_FUNCTIONING_METHOD_NAME);
87+
});
88+
89+
test('App should use invalid method', async () => {
90+
const accessMethods = page.getByTestId('access-method');
91+
const nonFunctioningTestMethod = accessMethods.last();
92+
93+
await expect(page.getByText(IN_USE_LABEL)).toHaveCount(1);
94+
await expect(nonFunctioningTestMethod).not.toContainText(IN_USE_LABEL);
95+
96+
await nonFunctioningTestMethod.locator('button').last().click();
97+
await nonFunctioningTestMethod.getByText('Use').click();
98+
await expect(nonFunctioningTestMethod).toContainText('Testing...');
99+
await expect(nonFunctioningTestMethod).toContainText('API unreachable');
100+
101+
await expect(page.getByText(IN_USE_LABEL)).toHaveCount(1);
102+
await expect(nonFunctioningTestMethod).not.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(process.env.SHADOWSOCKS_SERVER_PORT!);
121+
122+
await inputs.first().fill(FUNCTIONING_METHOD_NAME);
123+
await expect(saveButton).toBeEnabled();
124+
125+
await inputs.nth(3).fill(process.env.SHADOWSOCKS_SERVER_PASSWORD!);
126+
127+
await page.getByTestId('ciphers').click();
128+
await page.getByRole('option', { name: process.env.SHADOWSOCKS_SERVER_CIPHER! }).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(page.getByText(IN_USE_LABEL)).toHaveCount(1);
148+
await expect(functioningTestMethod).not.toContainText(IN_USE_LABEL);
149+
await expect(functioningTestMethod).toHaveText(FUNCTIONING_METHOD_NAME);
150+
151+
await functioningTestMethod.locator('button').last().click();
152+
await functioningTestMethod.getByText('Use').click();
153+
await expect(direct).not.toContainText(IN_USE_LABEL);
154+
await expect(bridges).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)