Files
cinny/e2e/security-nudge.spec.ts
T
Lotus CIandClaude Opus 5.5 6a7627fa26
CI / Build & Quality Checks (pull_request) Successful in 1m48s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 12s
CI / Playwright smoke (e2e) (pull_request) Successful in 11m38s
feat: device-security nudge — verify this device / key backup (#110, #123)
One "security" strip, in the same top slot and style as the status banner
(#124), for this device, in priority order:
- "Verify this device" — cross-signing exists but this device isn't
  verified (can't unlock backed-up history, untrusted to others). First,
  because verifying with the recovery key also connects the backup.
- "Connect this device to your key backup" — a backup exists, this device
  isn't using it.
- "Set up key backup" — no backup at all (includes accounts without
  cross-signing; the setup flow does both).
The button opens Settings → Devices (existing flows); "Not now" snoozes.

Rules: nothing in a device's first 24 h; "Not now" snoozes that nudge 7
days; 3 dismissals stop it; never on a healthy device; waits until sync
has settled (never under "Connecting…"); outage/maintenance/connection
strips win. Per-device record in localStorage, wiped on logout. Mounted
inside the Matrix client context and an error boundary (an early version
outside the context crashed the app on load — caught before pushing).

Also: the status strip wraps its text on phones instead of truncating it
when there's no Details button (benefits #124's strips too).

Design approved on #123 (real-client screenshots there). Tests: 4 unit
(the #123 state table, loading, timing, stored record); e2e: nothing during
the grace period, "Set up key backup" → Settings → Devices, "Not now"
holds across a reload. Unit 1319, Playwright 29 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-30 20:28:32 -04:00

75 lines
2.7 KiB
TypeScript

import { test, expect, Page } from '@playwright/test';
import { HS, ensureUser, hsReachable, loginUI, TestUser, uniq } from './localHs';
// [Gitea #110, #123] The device-security nudge. A fresh account has no key
// backup and no cross-signing → "Set up key backup", but only after the 24 h
// grace period on this device; "Not now" snoozes it.
const strip = (page: Page) => page.getByRole('status').filter({ hasText: /encryption keys/ });
/** Back-date this device's first-seen record past the 24 h grace period, then reload. */
async function pastGrace(page: Page) {
await page.evaluate(() => {
const { deviceId } = JSON.parse(localStorage.getItem('cinny_session_v1') ?? '{}');
localStorage.setItem(
`lotus-security-nudge-${deviceId}`,
JSON.stringify({ firstSeen: Date.now() - 2 * 86_400_000, dismissals: {} }),
);
});
await page.reload();
}
/** The nudge waits until sync has settled (after "Connecting…"). */
async function settled(page: Page) {
await page
.getByText('Connecting...')
.first()
.waitFor({ timeout: 30_000 })
.catch(() => undefined);
await page.getByText('Connecting...').first().waitFor({ state: 'detached', timeout: 90_000 });
await page.waitForTimeout(2000);
}
test.describe('security nudge (#110, #123)', () => {
let user: TestUser;
test.beforeAll(async () => {
test.skip(!(await hsReachable()), `no local homeserver at ${HS} (set E2E_LOCAL_HS)`);
});
test.beforeEach(async () => {
user = await ensureUser(uniq('e2e_nudge_'));
});
test('nothing during the first 24 h on a device', async ({ page }) => {
test.setTimeout(150_000);
await loginUI(page, user);
await settled(page);
// Past the point where the nudge shows once the grace period is over (~5 s).
await page.waitForTimeout(8000);
await expect(strip(page)).toHaveCount(0);
});
test('no key backup: "Set up key backup" opens Settings → Devices', async ({ page }) => {
test.setTimeout(150_000);
await loginUI(page, user);
await pastGrace(page);
await settled(page);
const nudge = strip(page);
await expect(nudge).toContainText("Your encryption keys aren't backed up");
await nudge.getByRole('button', { name: 'Set up' }).click();
await expect(page.getByText('Device Verification').first()).toBeVisible();
});
test('"Not now" snoozes it across a reload', async ({ page }) => {
test.setTimeout(200_000);
await loginUI(page, user);
await pastGrace(page);
await settled(page);
await strip(page).getByRole('button', { name: 'Not now' }).click();
await expect(strip(page)).toHaveCount(0);
await page.reload();
await settled(page);
await expect(strip(page)).toHaveCount(0);
});
});