Extract PassDashboard Playwright e2e suite into standalone repo
Some checks failed
E2E Tests / test (push) Has been cancelled

Copies the Playwright end-to-end suite out of the PassDashboard app repo so
it can be run and deployed independently. Nothing was removed from the app
repo - this is a copy.

Carried over verbatim: tests/ (10 spec files, 4 helper modules, README) plus
playwright.config.ts and .env.test.example. The suite drives the deployed app
over HTTP and imports nothing from src/, which is what makes it standalone.

New for this repo:
- package.json with only the deps the suite actually uses (@playwright/test,
  allure-playwright, xlsx, typescript, @types/node) and the e2e scripts.
- tsconfig.json covering tests/ - the app repo's tsconfig.app.json only
  included src/, so these files were never typechecked before.
- .gitea/workflows/e2e-tests.yml for Gitea Actions. The app repo derived ENV
  from github.ref_name; that doesn't apply here since this repo has no
  per-environment branches, so the target is an explicit workflow_dispatch
  input and scheduled/push runs default to develop. Reports are uploaded as
  artifacts rather than deleted.
- .gitignore and README covering local setup and the required CI secrets.

The Jest unit tests under src/__tests__/ were deliberately left in the app
repo: they import application source directly (AuthContext, ProtectedRoute,
cryptoUtils, api) and cannot run without it.

Verified: npx tsc --noEmit is clean and playwright collects all 43 tests
across all 10 spec files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 15:50:50 +03:00
commit 7bf4343f88
23 changed files with 6568 additions and 0 deletions

176
tests/auth.spec.ts Normal file
View File

@@ -0,0 +1,176 @@
import { test, expect } from '@playwright/test';
import { TEST_CREDENTIALS } from './helpers/auth';
import { epic, feature, severity, description } from './helpers/allure';
test.describe('Authentication Tests', () => {
test.setTimeout(90000);
test.beforeEach(async ({ page }) => {
epic('User Authentication');
feature('Login');
// Retry the initial navigation in place - on a loaded local machine a
// single headless navigation can occasionally stall well past one
// timeout even though the server itself responds in ~1-3s.
let lastError: unknown;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await page.goto(TEST_CREDENTIALS.loginPath, { waitUntil: 'domcontentloaded' });
lastError = undefined;
break;
} catch (error) {
lastError = error;
console.log(`Login page navigation attempt ${attempt} failed, retrying...`);
}
}
if (lastError) throw lastError;
await page.waitForTimeout(2000);
// Switch to English by clicking the article element
console.log('Checking for language switcher...');
await page.waitForTimeout(1000);
const languageArticle = page.locator('article.ant-typography.font-poppins-500.css-1kfsfla').first();
const isVisible = await languageArticle.isVisible({ timeout: 2000 }).catch(() => false);
if (isVisible) {
const text = await languageArticle.textContent();
console.log(`Found language button with text: "${text}"`);
console.log('Clicking to switch language...');
await languageArticle.click();
await page.waitForTimeout(2000);
console.log('✓ Language switched');
} else {
console.log('Language button not found');
}
});
test('should successfully login with valid credentials', async ({ page }) => {
severity('critical');
description('Test successful login flow with valid user credentials');
console.log('Testing login with valid credentials...');
await test.step('Fill in username', async () => {
await page.fill('input[type="email"], input[name="email"], input[placeholder*="email" i]', TEST_CREDENTIALS.username);
});
await test.step('Fill in password', async () => {
await page.fill('input[type="password"], input[name="password"]', TEST_CREDENTIALS.password);
});
await test.step('Click login button', async () => {
await page.click('button[type="submit"], button:has-text("Login"), button:has-text("Sign in")');
});
await test.step('Verify redirect to dashboard', async () => {
await page.waitForURL('**/dashboard**', { timeout: 30000 }).catch(() => {
console.log('Checking for successful navigation...');
});
await page.waitForLoadState('domcontentloaded').catch(() => {});
await page.waitForTimeout(2000);
const currentUrl = page.url();
console.log('Current URL after login:', currentUrl);
expect(currentUrl).toContain('dashboard');
});
console.log('Login successful');
});
test('should show error with invalid credentials', async ({ page }) => {
severity('critical');
description('Verify error handling for invalid login credentials');
console.log('Testing login with invalid credentials...');
await test.step('Fill in invalid username', async () => {
await page.fill('input[type="email"], input[name="email"], input[placeholder*="email" i]', 'invalid@example.com');
});
await test.step('Fill in invalid password', async () => {
await page.fill('input[type="password"], input[name="password"]', 'wrongpassword');
});
await test.step('Click login button', async () => {
await page.click('button[type="submit"], button:has-text("Login"), button:has-text("Sign in")');
});
await test.step('Verify error message appears', async () => {
await page.waitForTimeout(2000);
const errorMessage = await page.locator('text=/error|invalid|incorrect|failed/i').first().isVisible().catch(() => false);
const stillOnLoginPage = page.url().includes('login');
console.log('Error message visible:', errorMessage);
console.log('Still on login page:', stillOnLoginPage);
expect(errorMessage || stillOnLoginPage).toBeTruthy();
});
console.log('Invalid credentials test completed');
});
test('should validate required fields', async ({ page }) => {
severity('normal');
description('Verify that required field validation works correctly');
console.log('Testing required field validation...');
await test.step('Click login without filling fields', async () => {
await page.click('button[type="submit"], button:has-text("Login"), button:has-text("Sign in")');
await page.waitForTimeout(1000);
});
await test.step('Verify validation or staying on login page', async () => {
const stillOnLoginPage = page.url().includes('login');
console.log('Still on login page:', stillOnLoginPage);
expect(stillOnLoginPage).toBeTruthy();
});
console.log('Required field validation test completed');
});
test('should successfully logout', async ({ page }) => {
test.setTimeout(90000);
severity('critical');
description('Verify that clicking Logout redirects to login page and clears session');
// First login
await test.step('Login first', async () => {
await page.fill('input[type="email"], input[name="email"], input[placeholder*="email" i]', TEST_CREDENTIALS.username);
await page.fill('input[type="password"], input[name="password"]', TEST_CREDENTIALS.password);
await page.click('button[type="submit"], button:has-text("Login"), button:has-text("Sign in")');
await page.waitForURL('**/dashboard**', { timeout: 30000 }).catch(() => {});
await page.waitForLoadState('domcontentloaded').catch(() => {});
console.log('✓ Logged in, URL:', page.url());
});
await test.step('Click Logout', async () => {
const logoutLink = page.locator('text=Logout').first();
if (!(await logoutLink.isVisible({ timeout: 2500 }).catch(() => false))) {
// Mobile layout might hide logout under a menu; try opening it.
const openMenu = page.locator('button[aria-label="Open menu"], button[aria-label="Open navigation"], button:has-text("Menu"), button:has-text("Open")');
if (await openMenu.isVisible({ timeout: 2500 }).catch(() => false)) {
await openMenu.click();
await page.waitForTimeout(1000);
}
}
await logoutLink.waitFor({ state: 'visible', timeout: 10000 });
await logoutLink.click();
await page.waitForTimeout(3000);
console.log('✓ Logout clicked');
});
await test.step('Verify redirected to login page', async () => {
await page.waitForURL('**/login**', { timeout: 10000 }).catch(() => {});
const currentUrl = page.url();
console.log('URL after logout:', currentUrl);
expect(currentUrl).toContain('login');
});
console.log('✓ Logout test completed');
});
});