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 tests for custom bridge GUI #6136

Merged
merged 7 commits into from
Apr 19, 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
8 changes: 8 additions & 0 deletions gui/locales/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ msgctxt "accessibility"
msgid "%(title)s, View loaded"
msgstr ""

msgctxt "accessibility"
msgid "Add new custom bridge"
msgstr ""

msgctxt "accessibility"
msgid "Close notification"
msgstr ""
Expand All @@ -275,6 +279,10 @@ msgctxt "accessibility"
msgid "Copy account number"
msgstr ""

msgctxt "accessibility"
msgid "Edit custom bridge"
msgstr ""

msgctxt "accessibility"
msgid "Expand %(location)s"
msgstr ""
Expand Down
6 changes: 5 additions & 1 deletion gui/src/renderer/components/EditCustomBridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ function CustomBridgeForm() {
<SmallButton key="cancel" onClick={hideDeleteDialog}>
{messages.gettext('Cancel')}
</SmallButton>,
<SmallButton key="delete" color={SmallButtonColor.red} onClick={onDelete}>
<SmallButton
key="delete"
color={SmallButtonColor.red}
onClick={onDelete}
data-testid="delete-confirm">
{messages.gettext('Delete')}
</SmallButton>,
]}
Expand Down
3 changes: 2 additions & 1 deletion gui/src/renderer/components/OpenVpnSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ function BridgeModeSelector() {
label: messages.gettext('On'),
value: 'on',
disabled: tunnelProtocol !== 'openvpn' || transportProtocol === 'udp',
'data-testid': 'bridge-mode-on',
},
{
label: messages.gettext('Off'),
Expand Down Expand Up @@ -367,7 +368,7 @@ function BridgeModeSelector() {
<SmallButton key="cancel" onClick={hideConfirmationDialog}>
{messages.gettext('Cancel')}
</SmallButton>,
<SmallButton key="confirm" onClick={confirmBridgeState}>
<SmallButton key="confirm" onClick={confirmBridgeState} data-testid="enable-confirm">
{messages.gettext('Enable')}
</SmallButton>,
]}
Expand Down
10 changes: 8 additions & 2 deletions gui/src/renderer/components/cell/Selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface SelectorItem<T> {
label: string;
value: T;
disabled?: boolean;
// eslint-disable-next-line @typescript-eslint/naming-convention
'data-testid'?: string;
}

// T represents the available values and U represent the value of "Automatic"/"Any" if there is one.
Expand Down Expand Up @@ -51,7 +53,8 @@ export default function Selector<T, U>(props: SelectorProps<T, U>) {
isSelected={selected}
disabled={props.disabled || item.disabled}
forwardedRef={ref}
onSelect={props.onSelect}>
onSelect={props.onSelect}
data-testid={item['data-testid']}>
{item.label}
</SelectorCell>
);
Expand Down Expand Up @@ -133,6 +136,8 @@ interface SelectorCellProps<T> {
onSelect: (value: T) => void;
children: React.ReactNode | Array<React.ReactNode>;
forwardedRef?: React.Ref<HTMLButtonElement>;
// eslint-disable-next-line @typescript-eslint/naming-convention
'data-testid'?: string;
}

function SelectorCell<T>(props: SelectorCellProps<T>) {
Expand All @@ -150,7 +155,8 @@ function SelectorCell<T>(props: SelectorCellProps<T>) {
disabled={props.disabled}
role="option"
aria-selected={props.isSelected}
aria-disabled={props.disabled}>
aria-disabled={props.disabled}
data-testid={props['data-testid']}>
<StyledCellIcon
$visible={props.isSelected}
source="icon-tick"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ export function CustomBridgeLocationRow(
const history = useHistory();

const bridgeSettings = useSelector((state) => state.settings.bridgeSettings);
const icon = bridgeSettings.custom !== undefined ? 'icon-edit' : 'icon-add';
const bridgeConfigured = bridgeSettings.custom !== undefined;
const icon = bridgeConfigured ? 'icon-edit' : 'icon-add';

const selectedRef = props.source.selected ? props.selectedElementRef : undefined;
const background = getButtonColor(props.source.selected, 0, props.source.disabled);
Expand All @@ -135,7 +136,14 @@ export function CustomBridgeLocationRow(
'A custom bridge server can be used to circumvent censorship when regular Mullvad bridge servers don’t work.',
)}
/>
<StyledLocationRowIcon {...background} onClick={navigate}>
<StyledLocationRowIcon
{...background}
aria-label={
bridgeConfigured
? messages.pgettext('accessibility', 'Edit custom bridge')
: messages.pgettext('accessibility', 'Add new custom bridge')
}
onClick={navigate}>
<StyledSpecialLocationSideButton
source={icon}
width={18}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export function useOnSelectBridgeLocation() {
case SpecialBridgeLocationType.closestToExit:
return setLocation(
bridgeSettingsModifier((bridgeSettings) => {
bridgeSettings.type = 'normal';
bridgeSettings.normal.location = 'any';
return bridgeSettings;
}),
Expand Down
173 changes: 173 additions & 0 deletions gui/test/e2e/installed/state-dependent/custom-bridge.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { expect, test } from '@playwright/test';
import { Page } from 'playwright';

import { startInstalledApp } from '../installed-utils';
import { TestUtils } from '../../utils';
import { colors } from '../../../../src/config.json';
import { RoutePath } from '../../../../src/renderer/lib/routes';

// This test expects the daemon to be logged in and not have a custom bridge configured.
// Env parameters:
// `SHADOWSOCKS_SERVER_IP`
// `SHADOWSOCKS_SERVER_PORT`
// `SHADOWSOCKS_SERVER_CIPHER`
// `SHADOWSOCKS_SERVER_PASSWORD`

let page: Page;
let util: TestUtils;

test.beforeAll(async () => {
({ page, util } = await startInstalledApp());
});

test.afterAll(async () => {
await page.close();
});

test('App should enable bridge mode', async () => {
await util.waitForNavigation(async () => await page.click('button[aria-label="Settings"]'));
expect(
await util.waitForNavigation(async () => await page.getByText('VPN settings').click()),
).toBe(RoutePath.vpnSettings);

await page.getByRole('option', { name: 'OpenVPN' }).click();

expect(
await util.waitForNavigation(async () => await page.getByText('OpenVPN settings').click()),
).toBe(RoutePath.openVpnSettings);

await page.getByTestId('bridge-mode-on').click();
await expect(page.getByText('Enable bridge mode?')).toBeVisible();

page.getByTestId('enable-confirm').click();

await util.waitForNavigation(async () => await page.click('button[aria-label="Back"]'));
await util.waitForNavigation(async () => await page.click('button[aria-label="Back"]'));
expect(
await util.waitForNavigation(async () => await page.click('button[aria-label="Close"]')),
).toBe(RoutePath.main);
});

test('App display disabled custom bridge', async () => {
expect(
await util.waitForNavigation(
async () => await page.click('button[aria-label^="Select location"]'),
),
).toBe(RoutePath.selectLocation);

const title = page.locator('h1')
await expect(title).toHaveText('Select location');

await page.getByText(/^Entry$/).click();

const customBridgeButton = page.getByText('Custom bridge');
await expect(customBridgeButton).toBeDisabled();
});

test('App should add new custom bridge', async () => {
expect(
await util.waitForNavigation(
async () => await page.click('button[aria-label="Add new custom bridge"]'),
),
).toBe(RoutePath.editCustomBridge);

const title = page.locator('h1')
await expect(title).toHaveText('Add custom bridge');

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(process.env.SHADOWSOCKS_SERVER_IP!);
await expect(addButton).toBeDisabled();

await inputs.nth(1).fill('443');
await expect(addButton).toBeEnabled();

await inputs.nth(2).fill(process.env.SHADOWSOCKS_SERVER_PASSWORD!);

await page.getByTestId('ciphers').click();
await page.getByRole('option', { name: process.env.SHADOWSOCKS_SERVER_CIPHER!, exact: true }).click();

expect(
await util.waitForNavigation(async () => await addButton.click())
).toEqual(RoutePath.selectLocation);

const customBridgeButton = page.getByText('Custom bridge');
await expect(customBridgeButton).toBeEnabled();

await expect(page.locator('button[aria-label="Edit custom bridge"]')).toBeVisible();
});

test('App should select custom bridge', async () => {
const customBridgeButton = page.locator('button:has-text("Custom bridge")');
await expect(customBridgeButton).toHaveCSS('background-color', colors.green);

const automaticButton = page.getByText('Automatic');
await automaticButton.click();
await page.getByText(/^Entry$/).click();
await expect(customBridgeButton).not.toHaveCSS('background-color', colors.green);


await customBridgeButton.click();
await page.getByText(/^Entry$/).click();
await expect(customBridgeButton).toHaveCSS('background-color', colors.green);

});

test('App should edit custom bridge', async () => {
const automaticButton = page.getByText('Automatic');
await automaticButton.click();
await page.getByText(/^Entry$/).click();

expect(
await util.waitForNavigation(
async () => await page.click('button[aria-label="Edit custom bridge"]'),
),
).toBe(RoutePath.editCustomBridge);

const title = page.locator('h1')
await expect(title).toHaveText('Edit custom bridge');

const inputs = page.locator('input');
const saveButton = page.locator('button:has-text("Save")');
await expect(saveButton).toBeVisible();
await expect(saveButton).toBeEnabled();

await inputs.nth(1).fill(process.env.SHADOWSOCKS_SERVER_PORT!);
await expect(saveButton).toBeEnabled();


expect(
await util.waitForNavigation(async () => await saveButton.click())
).toEqual(RoutePath.selectLocation);

const customBridgeButton = page.locator('button:has-text("Custom bridge")');
await expect(customBridgeButton).toBeEnabled();
await expect(customBridgeButton).toHaveCSS('background-color', colors.green);
});

test('App should delete custom bridge', async () => {
expect(
await util.waitForNavigation(
async () => await page.click('button[aria-label="Edit custom bridge"]'),
),
).toBe(RoutePath.editCustomBridge);

const deleteButton = page.locator('button:has-text("Delete")');
await expect(deleteButton).toBeVisible();
await expect(deleteButton).toBeEnabled();

await deleteButton.click();
await expect(page.getByText('Delete custom bridge?')).toBeVisible();

const confirmButton = page.getByTestId('delete-confirm');
expect(
await util.waitForNavigation(async () => await confirmButton.click())
).toEqual(RoutePath.selectLocation);

const customBridgeButton = page.locator('button:has-text("Custom bridge")');
await expect(customBridgeButton).toBeDisabled();
await expect(customBridgeButton).not.toHaveCSS('background-color', colors.green);
});
15 changes: 15 additions & 0 deletions test/test-manager/src/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,21 @@ pub async fn login_with_retries(
}
}

/// Ensure that the test runner is logged in to an account.
///
/// This will first check whether we are logged in. If not, it will also try to login
/// on your behalf. If this function returns without any errors, we are logged in to a valid
/// account.
pub async fn ensure_logged_in(
mullvad_client: &mut MullvadProxyClient,
) -> Result<(), mullvad_management_interface::Error> {
if mullvad_client.get_device().await?.is_logged_in() {
return Ok(());
}
// We are apparently not logged in already.. Try to log in.
login_with_retries(mullvad_client).await
}

/// Try to connect to a Mullvad Tunnel.
///
/// # Returns
Expand Down
Loading
Loading