SPB Git

Repository Public

auth-terminal.spec.ts 8.0 KB 157 lines typescript

1import { expect, test } from '@playwright/test';2import { apiLogin, expectTerm, openShell, resetMock, SEED, termStatus, termText, totp, waitTermOpen, watch } from './helpers';34test.describe.configure({ mode: 'serial' });5test.beforeAll(async ({ browser }) => {6  const page = await browser.newPage();7  await resetMock(page);8  await page.close();9});1011test('login: password → TOTP → workspace, then create a shell session and run a command', async ({ page }) => {12  const w = watch(page);13  await page.goto('/');14  await expect(page).toHaveURL(/\/login\//);15  await page.getByTestId('login-username').fill(SEED.username);16  await page.getByTestId('login-password').fill(SEED.password);17  await page.getByTestId('login-submit').click();18  await page.getByTestId('login-totp').fill(totp(SEED.totpSecret));19  await page.getByTestId('totp-submit').click();20  await expect(page).toHaveURL(/\/terminal\//);21  await expect(page.getByTestId('start-view')).toBeVisible();22  await expect(page.getByTestId('greeting')).toContainText('admin');23  await expect(page.getByTestId('no-session')).toContainText('No sessions yet');2425  await page.getByTestId('empty-new-session').click();26  await page.getByTestId('kind-shell').click();27  await page.getByTestId('create-session').click();28  await expect(page).toHaveURL(/\/terminal\/\?s=[0-9a-f-]{36}/);29  const id = new URL(page.url()).searchParams.get('s')!;30  await waitTermOpen(page, id);31  await page.getByTestId('terminal').click();32  await page.keyboard.type('echo cr-$((6*7))\n');33  await expectTerm(page, id, /^cr-42$/m);34  await expect(page.getByTestId('conn-indicator')).toHaveAttribute('data-tone', 'success');3536  // reload → same session re-attaches and the earlier output is repainted37  await page.reload();38  await waitTermOpen(page, id);39  await expectTerm(page, id, /^cr-42$/m);40  expect(new URL(page.url()).searchParams.get('s')).toBe(id);41  w.expectClean();42});4344test('login: wrong credentials give a generic error, then a lockout countdown', async ({ page }) => {45  await page.goto('/login/');46  for (let i = 0; i < 5; i++) {47    await page.getByTestId('login-username').fill(i % 2 ? 'nobody' : SEED.username);48    await page.getByTestId('login-password').fill('wrong-password-000');49    await page.getByTestId('login-submit').click();50    if (i < 4) await expect(page.getByTestId('login-error')).toHaveText('Sign-in failed. Check your credentials and try again.');51  }52  await expect(page.getByTestId('lockout')).toContainText(/Try again in \d+ s/);53  await expect(page.getByTestId('login-submit')).toBeDisabled();54  await expect(page.getByTestId('lockout')).toBeHidden({ timeout: 8000 });55  await expect(page.getByTestId('login-submit')).toBeEnabled();56});5758test('network drop: the terminal reconnects by itself and keeps working', async ({ page }) => {59  const w = watch(page);60  await apiLogin(page);61  const id = await openShell(page, 'drop test');62  await page.getByTestId('terminal').click();63  await page.keyboard.type('echo before-drop\n');64  await expectTerm(page, id, /^before-drop$/m);65  await page.evaluate((i) => window.__crTerm.dropSocket(i), id);66  await expect.poll(() => termStatus(page, id)).toBe('reconnecting');67  await expect(page.getByTestId('term-reconnecting')).toBeVisible();68  await waitTermOpen(page, id);69  await expect(page.getByTestId('back-online')).toHaveText('Back online — caught up');70  await expectTerm(page, id, /^before-drop$/m); // repainted after re-attach71  await page.keyboard.type('echo after-$((40+2))\n');72  await expectTerm(page, id, /^after-42$/m);73  // exactly one session exists for this title: reconnecting never creates sessions74  const sessions = (await (await page.request.get('/api/v1/sessions')).json()) as { title: string }[];75  expect(sessions.filter((s) => s.title === 'drop test')).toHaveLength(1);76  w.expectClean();77});7879test('session isolation: input goes only to the visible terminal; hidden tab does not steal focus', async ({ page }) => {80  await apiLogin(page);81  const a = await openShell(page, 'iso A');82  const b = await openShell(page, 'iso B');83  await expect(page.getByTestId('terminal-tab')).toHaveCount(2);84  expect(await page.getByTestId('terminal').getAttribute('data-session-id')).toBe(b);85  await page.getByTestId('terminal').click();86  await page.keyboard.type('echo only-in-B\n');87  await expectTerm(page, b, /^only-in-B$/m);88  expect(await termText(page, a)).not.toContain('only-in-B');89  expect(await page.evaluate((i) => window.__crTerm.hasFocus(i), a)).toBe(false);9091  await page.locator(`[data-testid="terminal-tab"][data-session-id="${a}"]`).click();92  await expect(page.getByTestId('terminal')).toHaveAttribute('data-session-id', a);93  await expect.poll(() => page.evaluate((i) => window.__crTerm.hasFocus(i), a)).toBe(true);94  await page.keyboard.type('echo only-in-A\n');95  await expectTerm(page, a, /^only-in-A$/m);96  expect(await termText(page, b)).not.toContain('only-in-A');97  expect(await termStatus(page, b)).toBe('open'); // hidden tab keeps its stream98});99100test('session exit: 4410 shows “Session ended (exit N)” with Restart; no automatic new session', async ({ page }) => {101  await apiLogin(page);102  const id = await openShell(page, 'exit test');103  await page.getByTestId('terminal').click();104  await page.keyboard.type('exit 7\n');105  await expect(page.getByTestId('term-overlay')).toHaveAttribute('data-kind', 'ended');106  await expect(page.getByText('Session ended (exit 7)')).toBeVisible();107  await page.waitForTimeout(1500);108  expect(await termStatus(page, id)).toBe('ended');109  await page.getByRole('button', { name: 'Restart' }).click();110  await expect(page).not.toHaveURL(new RegExp(id));111  const next = new URL(page.url()).searchParams.get('s')!;112  await waitTermOpen(page, next);113});114115test('theme: light by default, toggle to dark applies everywhere, persists and is set before first paint', async ({ page }) => {116  const w = watch(page);117  await apiLogin(page);118  const id = await openShell(page, 'theme test');119  const html = page.locator('html');120  await expect(html).toHaveAttribute('data-theme', 'light');121  await expect(page.getByTestId('terminal')).toHaveAttribute('data-term-theme', 'light');122  await expect(page.locator('meta[name="theme-color"]')).toHaveAttribute('content', '#F7F6F3');123  const bgLight = await page.evaluate(() => getComputedStyle(document.body).backgroundColor);124  await page.getByTestId('theme-toggle').click();125  await expect(html).toHaveAttribute('data-theme', 'dark');126  await expect(page.getByTestId('terminal')).toHaveAttribute('data-term-theme', 'dark'); // terminalTheme "auto" follows the app127  await expect(page.locator('meta[name="theme-color"]')).toHaveAttribute('content', '#080B10');128  await expect.poll(() => page.evaluate(() => getComputedStyle(document.body).backgroundColor)).not.toBe(bgLight);129  await expect.poll(async () => (await (await page.request.get('/api/v1/settings')).json()).theme).toBe('dark');130  // reload: /theme-init.js (external, CSP-clean) sets the theme before the app boots131  await page.reload({ waitUntil: 'commit' });132  await expect(html).toHaveAttribute('data-theme', 'dark');133  await waitTermOpen(page, id);134  await page.getByTestId('theme-toggle').click();135  await expect(html).toHaveAttribute('data-theme', 'light');136  w.expectClean();137});138139test('scrollback: scrolling up shows the jump-to-bottom button, with an unread marker when output arrives', async ({ page }) => {140  await apiLogin(page);141  const id = await openShell(page, 'scroll test');142  await page.getByTestId('terminal').click();143  await page.keyboard.type('seq 1 300\n');144  await expectTerm(page, id, /^300$/m);145  await expect(page.getByTestId('scroll-bottom')).toHaveCount(0);146  await page.getByTestId('terminal').hover();147  await page.mouse.wheel(0, -1500);148  const fab = page.getByTestId('scroll-bottom');149  await expect(fab).toBeVisible();150  await expect(fab).not.toHaveAttribute('data-unread', 'true');151  await page.evaluate((i) => window.__crTerm.send(i, 'echo fresh-output\r'), id);152  await expect(fab).toHaveAttribute('data-unread', 'true');153  await fab.click();154  await expect(fab).toHaveCount(0);155  await expectTerm(page, id, /^fresh-output$/m);156});157
Files main
Menu

Theme