Files
sencho/e2e/helpers.ts
T
Anso 6f45a3b788 fix(frontend): align sidebar brand box with top nav chrome (#928)
* fix(frontend): align sidebar brand box with top nav chrome

Match the TopBar's 56px height so the bottom border of the brand
box meets the bottom border under the nav, removing the 7px seam.
Drop the duplicate uppercase "SENCHO" label and place the version
number baseline-aligned next to the italic wordmark. Center the
brand row, enlarge the logo (28→36px) and wordmark (22→28px), and
mark the logo as decorative since the wordmark already names it.

* test(e2e): key dashboard sentinel off logo src instead of alt text

The brand box change made the sidebar logo decorative (alt=""), since
the adjacent wordmark already names the brand. The Playwright helper
keyed loginAs() off img[alt="Sencho Logo"], so every login wait timed
out and 11 specs failed (with 25 cascading skips via suite teardown).

Switch DASHBOARD_INDICATOR to img[src*="sencho-logo"]: same DOM target,
unaffected by accessibility wording. The selector is unique to the
authenticated sidebar — login/setup screens do not render it.
2026-05-05 07:58:30 -04:00

108 lines
4.7 KiB
TypeScript

/**
* Shared helpers for Sencho E2E tests.
*
* CREDENTIALS: Set E2E_USERNAME and E2E_PASSWORD env vars to match
* your dev instance's admin account. Defaults assume the initial setup
* was completed with username "admin" and password "password123".
*
* E2E_USERNAME=admin E2E_PASSWORD=mypassword npx playwright test
*/
import { Page, expect } from '@playwright/test';
import { OTP } from 'otplib';
const totp = new OTP({ strategy: 'totp' });
const TOTP_PARAMS = { algorithm: 'sha1' as const, digits: 6, period: 30 };
export const TEST_USERNAME = process.env.E2E_USERNAME ?? 'admin';
export const TEST_PASSWORD = process.env.E2E_PASSWORD ?? 'password123';
/**
* Generate a current TOTP code for the given base32 secret. Used by the MFA
* E2E tests to drive the login challenge without a real authenticator app.
*/
export function totpNow(secret: string): string {
return totp.generateSync({ secret: secret.replace(/\s+/g, ''), ...TOTP_PARAMS });
}
/** Selector for the dashboard - only present in EditorLayout, not on login/setup pages */
const DASHBOARD_INDICATOR = 'img[src*="sencho-logo"]';
/** Returns true if the current page is the first-run setup screen */
async function isSetupPage(page: Page): Promise<boolean> {
return page.locator('#confirmPassword, input[placeholder*="Confirm"]').isVisible().catch(() => false);
}
/** Returns true if the current page is the login screen */
async function isLoginPage(page: Page): Promise<boolean> {
return page.locator('button:has-text("Login"), button:has-text("Sign in")').isVisible().catch(() => false);
}
/** Returns true if the dashboard (EditorLayout) is loaded */
export async function isDashboard(page: Page): Promise<boolean> {
return page.locator(DASHBOARD_INDICATOR).isVisible().catch(() => false);
}
/**
* Wait for the stacks sidebar to finish loading. Waits for the Create Stack
* button and the data-stacks-loaded sentinel set by the CommandList after its
* async refreshStacks() call resolves.
*/
export async function waitForStacksLoaded(page: Page): Promise<void> {
await expect(page.getByRole('button', { name: 'Create Stack' })).toBeVisible({ timeout: 15_000 });
await expect(page.locator('[data-stacks-loaded="true"]')).toBeAttached({ timeout: 15_000 });
}
/**
* Navigate to the app root, complete first-run setup if needed, then log in.
* After this call the dashboard is guaranteed to be visible.
*/
export async function loginAs(page: Page, username = TEST_USERNAME, password = TEST_PASSWORD) {
await page.goto('/');
// Wait for the app to finish its auth check (loading spinner disappears)
await page.waitForTimeout(500);
// ── First-run setup ───────────────────────────────────────────────────────
if (await isSetupPage(page)) {
await page.locator('#username').fill(username);
await page.locator('#password').fill(password);
const confirmInput = page.locator('#confirmPassword');
if (await confirmInput.isVisible()) await confirmInput.fill(password);
await page.locator('button[type="submit"]').click();
// After setup, the app logs in automatically and shows the dashboard
await expect(page.locator(DASHBOARD_INDICATOR)).toBeVisible({ timeout: 10_000 });
return;
}
// ── Login screen ─────────────────────────────────────────────────────────
if (await isLoginPage(page)) {
// Confirm the username field is actually visible before filling. If
// isLoginPage was a transient false positive (e.g. login form briefly
// rendered before auth check redirected to dashboard), the fill would
// hang forever waiting for #username to come back.
const usernameField = page.locator('#username');
const usernameVisible = await usernameField
.waitFor({ state: 'visible', timeout: 2_000 })
.then(() => true)
.catch(() => false);
if (usernameVisible) {
await usernameField.fill(username);
await page.locator('#password').fill(password);
await page.locator('button:has-text("Login"), button:has-text("Sign in")').first().click();
await expect(page.locator(DASHBOARD_INDICATOR)).toBeVisible({ timeout: 10_000 });
return;
}
// Fall through to the dashboard check below.
}
// ── Already on the dashboard ──────────────────────────────────────────────
if (await isDashboard(page)) {
return;
}
throw new Error(
'loginAs: could not determine page state - expected setup, login, or dashboard. ' +
'Check that E2E_USERNAME and E2E_PASSWORD are set correctly.',
);
}