Extract PassDashboard Playwright e2e suite into standalone repo
Some checks failed
E2E Tests / test (push) Has been cancelled
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:
176
tests/auth.spec.ts
Normal file
176
tests/auth.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user