Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GUI test for API access methods #5775

Merged
merged 1 commit into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gui/src/renderer/components/ApiAccessMethods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
Expand Down
10 changes: 9 additions & 1 deletion gui/src/renderer/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 7 additions & 1 deletion gui/src/renderer/components/EditApiAccessMethod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down
4 changes: 3 additions & 1 deletion gui/src/renderer/components/cell/SettingsSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>) {
Expand Down Expand Up @@ -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 ?? ''}
Expand Down
169 changes: 169 additions & 0 deletions gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading