Skip to content
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
4 changes: 2 additions & 2 deletions apps/web/e2e/about.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs';

import { type Page, expect, test } from '@playwright/test';

import { createVault, startScreen } from './create';
import { createVault, openSettingsPage, startScreen } from './create';

// Calibrating Argon2id targets about 1 s per derivation (D9).
test.setTimeout(120_000);
Expand Down Expand Up @@ -49,7 +49,7 @@ test('shows the version and the hash of the running build, before unlock and in
await startScreen(page);

await createVault(page, PASSWORD);
await page.getByRole('button', { name: 'Settings' }).first().click();
await openSettingsPage(page, 'about');
await expectAbout(page, hash);

// Opening About before unlock hides the screen without losing what was typed.
Expand Down
10 changes: 10 additions & 0 deletions apps/web/e2e/accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,17 @@ test('reflows at 320 px with no horizontal scroll: start, list, editor and Setti
await expect(page.getByRole('heading', { name: 'Settings', level: 1 })).toBeVisible();
expect(await fits()).toBe(true);
// Longer labels in Brazilian Portuguese.
await page.getByTestId('settings-language').click();
await page.getByRole('combobox', { name: 'Language' }).selectOption('pt-BR');
await expect(page.getByRole('heading', { name: 'Idioma', level: 1 })).toBeVisible();
expect(await fits()).toBe(true);
// The densest pages, in Portuguese too.
await page.getByRole('button', { name: 'Voltar', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Configurações', level: 1 })).toBeVisible();
expect(await fits()).toBe(true);
for (const row of ['devices', 'backup']) {
await page.getByTestId(`settings-${row}`).click();
expect(await fits()).toBe(true);
await page.getByRole('button', { name: 'Voltar', exact: true }).click();
}
});
4 changes: 2 additions & 2 deletions apps/web/e2e/appearance.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';

import { createVault } from './create';
import { createVault, openSettingsPage } from './create';

// Calibrating Argon2id targets about 1 s per derivation (D9), on every browser.
test.setTimeout(120_000);
Expand All @@ -10,7 +10,7 @@ for (const theme of ['light', 'dark'] as const) {
await page.emulateMedia({ colorScheme: theme });
await page.goto('/');
await createVault(page, 'quartz-lantern-velvet-harbor-9214');
await page.getByRole('button', { name: 'Settings' }).first().click();
await openSettingsPage(page, 'appearance');
const swatches = page.locator('fieldset span[data-accent]');
await expect(swatches).toHaveCount(6);
const colors = await swatches.evaluateAll((spans) =>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/e2e/axe-devices.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ async function device(
return page;
}

/** Goes back to Settings from wherever the vault is (on mobile, through Back). */
/** Goes back to Settings › Devices from wherever the vault is (on mobile, through Back). */
async function settings(page: Page): Promise<void> {
const open = page.getByRole('button', { name: 'Settings' }).first();
while (!(await open.isVisible())) await button(page, 'Back').click();
await open.click();
await page.getByTestId('settings-devices').click();
}

for (const [layout, width] of WIDTHS) {
Expand Down
9 changes: 9 additions & 0 deletions apps/web/e2e/axe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ for (const [layout, width] of WIDTHS) {
await settings.click();
await expect(page.getByRole('heading', { name: 'Settings', level: 1 })).toBeVisible();
await audit(page, 'Settings');
// The densest pages (D252).
await page.getByTestId('settings-devices').click();
await expect(page.getByRole('heading', { name: 'Devices', level: 1 })).toBeVisible();
await audit(page, 'Settings, Devices');
await button(page, 'Back').click();
await page.getByTestId('settings-backup').click();
await expect(page.getByRole('heading', { name: 'Backup', level: 1 })).toBeVisible();
await audit(page, 'Settings, Backup');
await button(page, 'Back').click();

// Locked: unlock, and the forgotten password.
await lock(page);
Expand Down
22 changes: 12 additions & 10 deletions apps/web/e2e/backup-merge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type Browser, type BrowserContext, type Page, expect, test } from '@pla

import { FUTURE_CLOCK_BOUND_MS } from '@blindvault/vault';

import { createVault, openVault, startScreen } from './create';
import { createVault, openSettingsPage, openVault, startScreen } from './create';
import { PAIRING_URL } from '../playwright.config';
import { fillPassword } from './password';

Expand Down Expand Up @@ -42,20 +42,17 @@ async function installation(browser: Browser, logged: string[]): Promise<Page> {
return page;
}

async function openSettings(page: Page) {
await page.getByRole('button', { name: 'Settings' }).first().click();
}

/** Exports from Settings with the local password and returns the file the browser received. */
async function exportBackup(page: Page, password: string): Promise<Buffer> {
await openSettings(page);
await openSettingsPage(page, 'backup');
await page.getByTestId('backup-password').fill(password);
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: 60_000 }),
button(page, 'Export').click(),
]);
const bytes = await readFile(await download.path());
await button(page, 'Back').click();
await button(page, 'Back').click();
return bytes;
}

Expand All @@ -70,7 +67,7 @@ async function setPassword(page: Page, value: string) {
/** Settings › Merge a backup: the file, the Recovery Phrase, then the confirmation. */
async function openForMerge(page: Page, file: Buffer, words: readonly string[]) {
// After a merge, Settings still shows it, with no Settings button of its own.
if (!(await page.getByTestId('merge-start').isVisible())) await openSettings(page);
if (!(await page.getByTestId('merge-start').isVisible())) await openSettingsPage(page, 'merge');
await page.getByTestId('merge-start').click();
await page.getByTestId('restore-file').setInputFiles({
name: 'blindvault-backup.bvault',
Expand Down Expand Up @@ -128,6 +125,7 @@ test('merges a backup made on another installation, keeping the losing edit in h

// B's edit won; A's own edit is in the history, above the first state.
await button(a, 'Back').click();
await button(a, 'Back').click();
await button(a, 'ACME').click();
await button(a, 'Show').click();
await expect(a.getByTestId('secret-Password')).toHaveText(ON_B);
Expand Down Expand Up @@ -221,7 +219,7 @@ test('a merge of a backup opened by its export password recommends Check Recover
const logged: string[] = [];
const a = await installation(browser, logged);
await createVault(a, PASSWORD);
await openSettings(a);
await openSettingsPage(a, 'backup');
await a.getByTestId('backup-protect').check();
await button(a, 'Generate a passphrase').click();
const passphrase = await a.getByTestId('export-password').inputValue();
Expand All @@ -235,6 +233,8 @@ test('a merge of a backup opened by its export password recommends Check Recover
await expect(a.getByTestId('after-restore')).toHaveCount(0);

// Its own backup merges with nothing to change; the export password opened it.
await button(a, 'Back').click();
await a.getByTestId('settings-merge').click();
await a.getByTestId('merge-start').click();
await a.getByTestId('restore-file').setInputFiles({
name: 'blindvault-backup.bvault',
Expand Down Expand Up @@ -273,7 +273,7 @@ test.describe('devices a merge brings', () => {

/** A pairs a new installation, same network only; A ends back in its vault. */
async function pair(browser: Browser, a: Page, logged: string[]) {
await openSettings(a);
await openSettingsPage(a, 'devices');
await button(a, 'Pair a device…').click();
await a.getByTestId('pairing-same-network').check();
await fillPassword(a.getByTestId('invite-password'), PASSWORD);
Expand All @@ -300,6 +300,7 @@ test.describe('devices a merge brings', () => {
await expect(a.getByTestId('pairing-done')).toBeVisible({ timeout: 60_000 });
await button(a, 'Close').click();
await button(a, 'Back').click();
await button(a, 'Back').click();
}

test('a merge names a device this one never saw, then one retired elsewhere, as a sign of the Master Key held by someone else (§1.6, D182)', async ({
Expand Down Expand Up @@ -339,11 +340,12 @@ test.describe('devices a merge brings', () => {
await expect(result).not.toContainText('Devices now retired');

// A retires the paired device; its next backup tells C.
await openSettings(a);
await openSettingsPage(a, 'devices');
await a.getByTestId('device-retire').click();
await a.getByRole('button', { name: /^Retire / }).click();
await expect(a.getByTestId('device')).toHaveCount(1);
await button(a, 'Back').click();
await button(a, 'Back').click();
const retired = await exportBackup(a, PASSWORD);

await openForMerge(c, retired, words);
Expand Down
6 changes: 3 additions & 3 deletions apps/web/e2e/backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RecordType } from '@blindvault/crypto';
import { type Page, expect, test } from '@playwright/test';

import { corruptRecords } from './corrupt';
import { createVault } from './create';
import { createVault, openSettingsPage } from './create';

// Calibrating Argon2id targets about 1 s per derivation (D9); an export derives twice.
test.setTimeout(180_000);
Expand Down Expand Up @@ -43,7 +43,7 @@ test('exports a backup, asking for the local password again, then one under a ge
await page.getByTestId('input-Password').fill(SECRET);
await button(page, 'Save').click();
await expect(page.getByRole('heading', { name: 'ACME', level: 1 })).toBeVisible();
await page.getByRole('button', { name: 'Settings' }).first().click();
await openSettingsPage(page, 'backup');
await expect(
page.getByText(
'A backup is an encrypted file with a full copy of this vault. Your Recovery Phrase always opens it. Keep copies away from this device, so you can restore if you lose it.',
Expand Down Expand Up @@ -140,7 +140,7 @@ test('a record that fails verification makes no backup, and the export names it
await page.getByTestId('item-title').fill('ACME');
await button(page, 'Save').click();
await expect(page.getByRole('heading', { name: 'ACME', level: 1 })).toBeVisible();
await page.getByRole('button', { name: 'Settings' }).first().click();
await openSettingsPage(page, 'backup');
// The export reads every record again from storage and verifies it (D181).
expect(await corruptRecords(page, RecordType.item)).toBeGreaterThan(0);
let downloaded = false;
Expand Down
7 changes: 5 additions & 2 deletions apps/web/e2e/clock-behind.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type Page, expect, test } from '@playwright/test';

import { FUTURE_CLOCK_BOUND_MS } from '@blindvault/vault';

import { createVault } from './create';
import { createVault, openSettingsPage } from './create';

// The device clock goes back after the vault was written ahead (§9.2, D73, D149, D221): the vault is
// read-only until the clock catches up, every screen that writes says why, and writing resumes on
Expand Down Expand Up @@ -73,19 +73,22 @@ test('a clock gone back makes the vault read-only, each screen says why, and wri
await expect(button(page, 'Edit')).toHaveCount(0);

// Settings: no pairing, no re-admission and no new Recovery Phrase while read-only.
await button(page, 'Settings').click();
await openSettingsPage(page, 'devices');
await expect(page.getByTestId('invite')).toContainText(
'The device clock went back, so the vault is read-only: pairing comes back once writing does.',
);
await expect(button(page, 'Pair a device…')).toHaveCount(0);
await expect(page.getByTestId('readmit-device')).toContainText(
'The device clock went back, so the vault is read-only: re-admission comes back once writing does.',
);
await button(page, 'Back').click();
await page.getByTestId('settings-recovery').click();
await expect(page.getByTestId('regenerate')).toContainText(
"This device's clock went back, so the vault is read-only. The Recovery Phrase can be regenerated from",
);
await expect(page.getByTestId('regenerate-start')).toHaveCount(0);
await button(page, 'Back').click();
await button(page, 'Back').click();

// A minute before writing resumes: within the bound, so only the date is shown.
await page.clock.setSystemTime(UNTIL - MINUTE);
Expand Down
29 changes: 29 additions & 0 deletions apps/web/e2e/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,32 @@ export async function createVault(page: Page, password: string): Promise<string[
await openVault(page);
return words;
}

/** A page of Settings (D252): the index lists them as rows with a `settings-<page>` test id. */
export type SettingsPageName =
| 'appearance'
| 'language'
| 'locking'
| 'password'
| 'passkey'
| 'recovery'
| 'devices'
| 'backup'
| 'merge'
| 'import'
| 'storage'
| 'refound'
| 'erase'
| 'about';

/** Opens Settings from the vault, then one of its pages. */
export async function openSettingsPage(page: Page, name: SettingsPageName): Promise<void> {
await page.getByRole('button', { name: 'Settings', exact: true }).first().click();
await page.getByTestId(`settings-${name}`).click();
}

/** From a page of Settings, back to the index and on to another page. */
export async function switchSettingsPage(page: Page, name: SettingsPageName): Promise<void> {
await page.getByRole('button', { name: 'Back', exact: true }).click();
await page.getByTestId(`settings-${name}`).click();
}
6 changes: 3 additions & 3 deletions apps/web/e2e/device-rename.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';

import { createVault } from './create';
import { createVault, openSettingsPage } from './create';

// Creation and unlock each derive Argon2id at about 1 s (D9).
test.setTimeout(120_000);
Expand All @@ -12,7 +12,7 @@ test('renames this device from Settings, refusing an empty or too long name, and
}) => {
await page.goto('/');
await createVault(page, PASSWORD);
await page.getByRole('button', { name: 'Settings' }).first().click();
await openSettingsPage(page, 'devices');

const device = page.getByTestId('devices').getByTestId('device');
await device.getByTestId('device-rename').click();
Expand All @@ -33,7 +33,7 @@ test('renames this device from Settings, refusing an empty or too long name, and
await page.getByTestId('unlock-password').fill(PASSWORD);
await page.getByRole('button', { name: 'Unlock' }).click();
await expect(page.getByTestId('lock-state')).toHaveText('Unlocked', { timeout: 30_000 });
await page.getByRole('button', { name: 'Settings' }).first().click();
await openSettingsPage(page, 'devices');
await expect(
page.getByTestId('devices').getByTestId('device').locator('span.font-medium'),
).toHaveText('Work laptop (this device)');
Expand Down
9 changes: 6 additions & 3 deletions apps/web/e2e/erase.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Page, expect, test } from '@playwright/test';

import { createVault, startScreen } from './create';
import { createVault, startScreen, openSettingsPage } from './create';

// Erase (§7, D41, D154, D183, D205): the warning, the Device record advice, the confirmation, then
// no vault. Creation derives Argon2id at about 1 s (D9).
Expand Down Expand Up @@ -45,7 +45,7 @@ test('erases the unlocked vault from Settings, saying it is the only device (D11
const words = await createVault(page, PASSWORD);
expect(await databases(page)).toHaveLength(1);

await page.getByRole('button', { name: 'Settings' }).first().click();
await openSettingsPage(page, 'erase');
await expect(page.getByText(INTRO)).toBeVisible();
await button(page, 'Erase this vault from this device…').click();
const warning = page.getByTestId('erase-warning');
Expand All @@ -54,6 +54,9 @@ test('erases the unlocked vault from Settings, saying it is the only device (D11
// The backup is one click away: the export password field takes the cursor.
await button(page, 'Make a backup now').click();
await expect(page.getByTestId('backup-password')).toBeFocused();
await button(page, 'Back').click();
await page.getByTestId('settings-erase').click();
await button(page, 'Erase this vault from this device…').click();
await confirmErase(page);

await startScreen(page);
Expand Down Expand Up @@ -114,7 +117,7 @@ test('a second app window that takes over an erased vault shows the start screen
await second.goto('/');
await expect(second.getByText('BlindVault is open in another window.')).toBeVisible();

await page.getByRole('button', { name: 'Settings' }).first().click();
await openSettingsPage(page, 'erase');
await button(page, 'Erase this vault from this device…').click();
await confirmErase(page);
await startScreen(page);
Expand Down
3 changes: 2 additions & 1 deletion apps/web/e2e/feedback.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ test('the empty vault shows its first steps, each opening where it is done (D250
await expect(steps.getByRole('img', { name: 'Done' })).toBeVisible();

await steps.getByRole('button', { name: 'Import', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Import', exact: true, level: 2 })).toBeFocused();
await expect(page.getByRole('heading', { name: 'Import', exact: true, level: 1 })).toBeFocused();
await button(page, 'Back').click();
await button(page, 'Back').click();

await steps.getByRole('button', { name: 'Add an item', exact: true }).click();
Expand Down
11 changes: 9 additions & 2 deletions apps/web/e2e/form-history.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { type Page, expect, test } from '@playwright/test';

import { answerConfirmation, openVault, readPhrase } from './create';
import {
answerConfirmation,
openSettingsPage,
openVault,
readPhrase,
switchSettingsPage,
} from './create';

// Calibrating Argon2id targets about 1 s per derivation (D9), on every browser.
test.setTimeout(180_000);
Expand Down Expand Up @@ -121,9 +127,10 @@ test('keeps vault content out of the form history and the spellchecker (D174, D1
await page.getByTestId('search').fill('harbor');
await expectNoneKept(page);
await page.getByTestId('search').fill('');
await button(page, 'Settings').click();
await openSettingsPage(page, 'password');
await expectNoneKept(page);
await expect(page.getByText(BROWSER_SAVE)).toBeVisible();
await switchSettingsPage(page, 'backup');
// The export password fields, with a generated passphrase shown as text (D181).
await page.getByTestId('backup-protect').check();
await button(page, 'Generate a passphrase').click();
Expand Down
5 changes: 3 additions & 2 deletions apps/web/e2e/import.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Page, expect, test } from '@playwright/test';

import { createVault } from './create';
import { createVault, openSettingsPage } from './create';
import { storedPlaintext } from './stored';

// Import (overview §15, D123, D125, D146): preview, one commit into a landing folder, the delete
Expand Down Expand Up @@ -39,7 +39,7 @@ function exportFile(encrypted: boolean) {
}

async function preview(page: Page) {
await page.getByRole('button', { name: 'Settings' }).first().click();
await openSettingsPage(page, 'import');
await page.getByTestId('import-start').click();
await page.getByTestId('import-source').selectOption('bitwarden');
await expect(page.getByText('holds every secret in plaintext')).toBeVisible();
Expand Down Expand Up @@ -76,6 +76,7 @@ test('imports a Bitwarden export into a landing folder in one commit, after a pr
);
await expect(page.getByTestId('import-result')).toContainText('Delete bitwarden_export.json now');

await button(page, 'Back').click();
await button(page, 'Back').click();
await page.getByRole('button', { name: FOLDER }).click();
await expect(button(page, 'Wifi')).toBeVisible();
Expand Down
Loading
Loading