summaryrefslogtreecommitdiffhomepage
path: root/gui
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2023-03-27 10:18:51 +0200
committerOskar Nyberg <oskar@mullvad.net>2023-03-27 10:18:51 +0200
commit29b31e9702e6a88f98e0f328c369da9bb1eb2f8d (patch)
treed6ae2c20818a1e6deabc504c5ebc8a356fd3faaf /gui
parentf0c2c90698175db6ffa61de962cc677c9a52dbb2 (diff)
parent50dc0f9025b5b97539841b3c2b58c5f4c0c41909 (diff)
downloadmullvadvpn-29b31e9702e6a88f98e0f328c369da9bb1eb2f8d.tar.xz
mullvadvpn-29b31e9702e6a88f98e0f328c369da9bb1eb2f8d.zip
Merge branch 'create-automated-tests-for-login-des-60'
Diffstat (limited to 'gui')
-rw-r--r--gui/src/renderer/components/AccountTokenLabel.tsx1
-rw-r--r--gui/src/renderer/components/ClipboardLabel.tsx18
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorView.tsx8
-rw-r--r--gui/src/renderer/components/Login.tsx2
-rw-r--r--gui/src/renderer/components/MainView.tsx1
-rw-r--r--gui/test/e2e/installed/requires-input/login.spec.ts32
-rw-r--r--gui/test/e2e/installed/state-dependent/login.spec.ts150
7 files changed, 168 insertions, 44 deletions
diff --git a/gui/src/renderer/components/AccountTokenLabel.tsx b/gui/src/renderer/components/AccountTokenLabel.tsx
index 704f41af10..479beed9a1 100644
--- a/gui/src/renderer/components/AccountTokenLabel.tsx
+++ b/gui/src/renderer/components/AccountTokenLabel.tsx
@@ -14,6 +14,7 @@ export default function AccountTokenLabel(props: IAccountTokenLabelProps) {
displayValue={formatAccountToken(props.accountToken)}
obscureValue={props.obscureValue}
className={props.className}
+ data-testid="account-number"
/>
);
}
diff --git a/gui/src/renderer/components/ClipboardLabel.tsx b/gui/src/renderer/components/ClipboardLabel.tsx
index 98fcede85e..582e03b02d 100644
--- a/gui/src/renderer/components/ClipboardLabel.tsx
+++ b/gui/src/renderer/components/ClipboardLabel.tsx
@@ -10,12 +10,11 @@ import ImageView from './ImageView';
const COPIED_ICON_DURATION = 2000;
-interface IProps {
+interface IProps extends React.HTMLAttributes<HTMLElement> {
value: string;
displayValue?: string;
obscureValue?: boolean;
message?: string;
- className?: string;
}
const StyledLabelContainer = styled.div({
@@ -42,29 +41,30 @@ const StyledCopyButton = styled(StyledButton)({
});
export default function ClipboardLabel(props: IProps) {
- const [obscured, , , toggleObscured] = useBoolean(props.obscureValue ?? true);
+ const { value, obscureValue, displayValue, message, ...otherProps } = props;
+
+ const [obscured, , , toggleObscured] = useBoolean(obscureValue ?? true);
const [justCopied, setJustCopied, resetJustCopied] = useBoolean(false);
const copiedScheduler = useScheduler();
const onCopy = useCallback(async () => {
try {
- await navigator.clipboard.writeText(props.value);
+ await navigator.clipboard.writeText(value);
copiedScheduler.schedule(resetJustCopied, COPIED_ICON_DURATION);
setJustCopied();
} catch (e) {
const error = e as Error;
log.error(`Failed to copy to clipboard: ${error.message}`);
}
- }, [props.value, copiedScheduler, setJustCopied, resetJustCopied]);
+ }, [value, copiedScheduler, setJustCopied, resetJustCopied]);
- const value = props.displayValue ?? props.value;
return (
<StyledLabelContainer>
- <StyledLabel className={props.className} aria-hidden={obscured}>
- {obscured ? '●●●● ●●●● ●●●● ●●●●' : value}
+ <StyledLabel aria-hidden={obscured} {...otherProps}>
+ {obscured ? '●●●● ●●●● ●●●● ●●●●' : displayValue ?? value}
</StyledLabel>
- {props.obscureValue !== false && (
+ {obscureValue !== false && (
<StyledButton
onClick={toggleObscured}
aria-label={
diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
index 8cfcdb1d46..d5af378cca 100644
--- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx
+++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
@@ -107,7 +107,9 @@ function WelcomeView() {
return (
<>
- <StyledTitle>{messages.pgettext('connect-view', 'Congrats!')}</StyledTitle>
+ <StyledTitle data-testid="title">
+ {messages.pgettext('connect-view', 'Congrats!')}
+ </StyledTitle>
<StyledAccountTokenMessage>
{messages.pgettext('connect-view', 'Here’s your account number. Save it!')}
<StyledAccountTokenContainer>
@@ -136,7 +138,9 @@ function Content() {
<StyledStatusIcon>
<ImageView source="icon-fail" height={60} width={60} />
</StyledStatusIcon>
- <StyledTitle>{messages.pgettext('connect-view', 'Out of time')}</StyledTitle>
+ <StyledTitle data-testid="title">
+ {messages.pgettext('connect-view', 'Out of time')}
+ </StyledTitle>
<StyledMessage>
{sprintf('%(introduction)s %(recoveryMessage)s', {
introduction: messages.pgettext(
diff --git a/gui/src/renderer/components/Login.tsx b/gui/src/renderer/components/Login.tsx
index b1d2a1392b..4219c0572f 100644
--- a/gui/src/renderer/components/Login.tsx
+++ b/gui/src/renderer/components/Login.tsx
@@ -274,7 +274,7 @@ export default class Login extends React.Component<IProps, IState> {
return (
<>
- <StyledSubtitle>{this.formSubtitle()}</StyledSubtitle>
+ <StyledSubtitle data-testid="subtitle">{this.formSubtitle()}</StyledSubtitle>
<StyledAccountInputGroup
active={allowInteraction && this.state.isActive}
editable={allowInteraction}
diff --git a/gui/src/renderer/components/MainView.tsx b/gui/src/renderer/components/MainView.tsx
index 983a33d376..5a252c4d8b 100644
--- a/gui/src/renderer/components/MainView.tsx
+++ b/gui/src/renderer/components/MainView.tsx
@@ -34,6 +34,7 @@ export default function MainView() {
setShowAccountExpired({ show: true, expiry: accountExpiry });
} else if (
showAccountExpired.show &&
+ accountExpiry &&
accountExpiry !== showAccountExpired.expiry &&
!accountHasExpired
) {
diff --git a/gui/test/e2e/installed/requires-input/login.spec.ts b/gui/test/e2e/installed/requires-input/login.spec.ts
deleted file mode 100644
index 2ff0fd576c..0000000000
--- a/gui/test/e2e/installed/requires-input/login.spec.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { Page } from 'playwright';
-import { RoutePath } from '../../../../src/renderer/lib/routes';
-import { TestUtils } from '../../utils';
-import { assertDisconnected } from '../../shared/tunnel-state';
-
-import { startInstalledApp } from '../installed-utils';
-
-// This test expects the daemon to be logged out and then log in.
-
-let page: Page;
-let util: TestUtils;
-
-test.beforeAll(async () => {
- ({ page, util } = await startInstalledApp());
-});
-
-test.afterAll(async () => {
- await page.close();
-});
-
-// Disables timeout since it's handled by the rust test
-test.setTimeout(0);
-
-test('App should go from login view to main view when daemon logs in', async () => {
- expect(await util.currentRoute()).toEqual(RoutePath.login);
-
- // Waiting for the daemon to log in
- expect(await util.waitForNavigation()).toEqual(RoutePath.main);
-
- await assertDisconnected(page);
-});
diff --git a/gui/test/e2e/installed/state-dependent/login.spec.ts b/gui/test/e2e/installed/state-dependent/login.spec.ts
new file mode 100644
index 0000000000..4a4edeadf8
--- /dev/null
+++ b/gui/test/e2e/installed/state-dependent/login.spec.ts
@@ -0,0 +1,150 @@
+import { exec, execSync } from 'child_process';
+import { expect, test } from '@playwright/test';
+import { Locator, Page } from 'playwright';
+import { RoutePath } from '../../../../src/renderer/lib/routes';
+import { TestUtils } from '../../utils';
+
+import { startInstalledApp } from '../installed-utils';
+import { assertDisconnected } from '../../shared/tunnel-state';
+
+// This test expects the daemon to be logged out.
+// Env parameters:
+// `ACCOUNT_NUMBER`: Account number to use when logging in
+
+let page: Page;
+let util: TestUtils;
+
+let accountNumber: string;
+
+test.beforeAll(async () => {
+ ({ page, util } = await startInstalledApp());
+});
+
+test.afterAll(async () => {
+ await page.close();
+});
+
+test('App should fail to login', async () => {
+ expect(await util.currentRoute()).toEqual(RoutePath.login);
+
+ const title = page.locator('h1')
+ const subtitle = page.getByTestId('subtitle');
+ const loginInput = getInput(page);
+
+ await expect(title).toHaveText('Login');
+ await expect(subtitle).toHaveText('Enter your account number');
+
+ await loginInput.fill('1234 1234 1324 1234');
+ await loginInput.press('Enter');
+
+ await expect(title).toHaveText('Logging in...');
+ await expect(subtitle).toHaveText('Checking account number');
+ await expect(title).toHaveText('Login failed');
+ await expect(subtitle).toHaveText('Invalid account number');
+
+ loginInput.fill('');
+});
+
+test('App should create account', async () => {
+ expect(await util.currentRoute()).toEqual(RoutePath.login);
+
+ const title = page.locator('h1')
+ const subtitle = page.getByTestId('subtitle');
+
+ await page.getByText('Create account').click();
+ await expect(title).toHaveText('Creating account...');
+ await expect(subtitle).toHaveText('Please wait');
+
+ await expect(title).toHaveText('Account created');
+ await expect(subtitle).toHaveText('Logged in');
+
+ expect(await util.waitForNavigation()).toEqual(RoutePath.main);
+
+ const outOfTimeTitle = page.getByTestId('title');
+ await expect(outOfTimeTitle).toHaveText('Congrats!');
+
+ const inputValue = await page.getByTestId('account-number').textContent();
+ expect(inputValue).toHaveLength(19);
+ accountNumber = inputValue!.replaceAll(' ', '');
+});
+
+test('App should become logged out', async () => {
+ expect(await util.waitForNavigation(() => {
+ exec('mullvad account logout');
+ })).toEqual(RoutePath.login);
+});
+
+test('App should log in', async () => {
+ expect(await util.currentRoute()).toEqual(RoutePath.login);
+
+ const title = page.locator('h1')
+ const subtitle = page.getByTestId('subtitle');
+ const loginInput = getInput(page);
+
+ await expect(title).toHaveText('Login');
+ await expect(subtitle).toHaveText('Enter your account number');
+
+ await loginInput.type(process.env.ACCOUNT_NUMBER!);
+ await loginInput.press('Enter');
+
+ await expect(title).toHaveText('Logging in...');
+ await expect(subtitle).toHaveText('Checking account number');
+ await expect(title).toHaveText('Logged in');
+ await expect(subtitle).toHaveText('Valid account number');
+
+ expect(await util.waitForNavigation()).toEqual(RoutePath.main);
+
+ // Prevent the connect-button from being hovered, and therefore not have the correct color.
+ await page.hover('div');
+
+ await assertDisconnected(page);
+});
+
+test('App should log out', async () => {
+ expect(await util.waitForNavigation(() => {
+ void page.getByLabel('Settings').click();
+ })).toEqual(RoutePath.settings);
+
+ expect(await util.waitForNavigation(() => {
+ void page.getByText('Account').click();
+ })).toEqual(RoutePath.accountSettings);
+
+ expect(await util.waitForNavigation(() => {
+ void page.getByText('Log out').click();
+ })).toEqual(RoutePath.login);
+
+ const title = page.locator('h1')
+ const subtitle = page.getByTestId('subtitle');
+ await expect(title).toHaveText('Login');
+ await expect(subtitle).toHaveText('Enter your account number');
+});
+
+test('App should log in to expired account', async () => {
+ expect(await util.currentRoute()).toEqual(RoutePath.login);
+
+ const title = page.locator('h1')
+ const subtitle = page.getByTestId('subtitle');
+ const loginInput = getInput(page);
+
+ await expect(title).toHaveText('Login');
+ await expect(subtitle).toHaveText('Enter your account number');
+
+ await loginInput.type(accountNumber);
+ await loginInput.press('Enter');
+
+ await expect(title).toHaveText('Logging in...');
+ await expect(subtitle).toHaveText('Checking account number');
+ await expect(title).toHaveText('Logged in');
+ await expect(subtitle).toHaveText('Valid account number');
+
+ expect(await util.waitForNavigation()).toEqual(RoutePath.main);
+
+ const outOfTimeTitle = page.getByTestId('title');
+ await expect(outOfTimeTitle).toHaveText('Out of time');
+
+ execSync('mullvad account logout');
+});
+
+function getInput(page: Page): Locator {
+ return page.getByPlaceholder('0000 0000 0000 0000');
+}