Skip to content

Commit

Permalink
Merge branch 'add-api-access-method-gui-tests-des-608'
Browse files Browse the repository at this point in the history
  • Loading branch information
raksooo committed Feb 13, 2024
2 parents b0e4315 + 41db8f1 commit e79f77a
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 4 deletions.
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);
});

0 comments on commit e79f77a

Please sign in to comment.