Expand e2e auth coverage, Vitest CI, and shared URL defaults
CI / skip-ci-check (pull_request) Successful in 4s
CI / docker-ci (pull_request) Successful in 6s
CI / secret-scan (pull_request) Successful in 13s
CI / e2e (pull_request) Successful in 2m14s
CI / viewer-unit (pull_request) Successful in 2m38s

Wire viewer-frontend Vitest into Gitea Actions, centralize DEV base URLs
in env-defaults.json, expand playkit API catalog specs, and land auth/mail
flows plus verify-email public-host redirect and idle-logout e2e hooks.
This commit is contained in:
2026-07-14 18:49:42 -04:00
parent 62d5a66ffa
commit 47b15049bc
25 changed files with 602 additions and 125 deletions
+10
View File
@@ -15,4 +15,14 @@ export class AccountMenu extends BasePage {
await this.openMenu();
await this.click(this.page.getByRole('button', { name: 'Sign out' }));
}
async openManageUsers(): Promise<void> {
await this.openMenu();
await this.click(this.page.getByRole('button', { name: 'Manage Users' }));
await waitForVisible(this.page.getByRole('heading', { name: /Manage Users/i }));
}
async closeManageUsers(): Promise<void> {
await this.click(this.page.getByLabel('Close manage users'));
}
}
+23 -3
View File
@@ -8,11 +8,31 @@ export class LoginPage extends BasePage {
async openLogin(): Promise<void> {
await this.open('/login');
await this.page.waitForLoadState('domcontentloaded');
await this.page.locator('#email').waitFor({ state: 'visible' });
// Wait for client bundle / React hydration (native GET submit otherwise)
await this.page.waitForFunction(() => {
const form = document.querySelector('form');
const btn = document.querySelector('button[type="submit"]');
return !!form && !!btn && !!(window as unknown as { next?: unknown }).next;
}).catch(() => undefined);
await this.page.waitForTimeout(600);
}
async signIn(email: string, password: string): Promise<void> {
await this.fill(this.page.locator('#email'), email);
await this.fill(this.page.locator('#password'), password);
await this.click(this.page.locator('button[type="submit"]'));
for (let attempt = 0; attempt < 3; attempt++) {
await this.fill(this.page.locator('#email'), email);
await this.fill(this.page.locator('#password'), password);
await this.page.waitForTimeout(250 * (attempt + 1));
await this.click(this.page.getByRole('button', { name: /Sign in/i }));
await this.page.waitForTimeout(400);
// Native GET fallback leaks credentials into the query string — retry.
if (!this.page.url().includes('password=')) {
return;
}
this.log.warn('login GET fallback detected; retrying after re-open', { attempt });
await this.openLogin();
}
throw new Error('Login form submitted as GET (page not hydrated)');
}
}
+27
View File
@@ -0,0 +1,27 @@
import { type Page } from '@playwright/test';
import { BasePage } from '@levkin/playkit';
export class RegisterPage extends BasePage {
constructor(page: Page, baseUrl: string) {
super(page, baseUrl);
}
async openRegister(): Promise<void> {
await this.open('/register');
await this.page.waitForLoadState('domcontentloaded');
await this.page.locator('#email').waitFor({ state: 'visible' });
await this.page.waitForTimeout(300);
}
async register(input: {
name: string;
email: string;
password: string;
}): Promise<void> {
await this.fill(this.page.locator('#name'), input.name);
await this.fill(this.page.locator('#email'), input.email);
await this.fill(this.page.locator('#password'), input.password);
await this.fill(this.page.locator('#confirmPassword'), input.password);
await this.click(this.page.locator('button[type="submit"]'));
}
}