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

99
tests/settings.spec.ts Normal file
View File

@@ -0,0 +1,99 @@
import { test, expect } from '@playwright/test';
import { login } from './helpers/auth';
import { epic, feature, severity, description, tag } from './helpers/allure';
import { navigateToSettings } from './helpers/navigation';
test.describe('Settings Tests', () => {
test.setTimeout(90000);
test.beforeEach(async ({ page }) => {
epic('Settings');
await login(page);
await navigateToSettings(page);
await page.waitForTimeout(2000);
});
test('should display Discount Rate Configuration for the Discounts settings tab', async ({ page }) => {
feature('Settings Operations');
tag('functional');
severity('normal');
description('Open the Discount rates settings tab and verify the configuration table renders correctly, whether rates are configured or not');
console.log('Testing Discount rates settings tab...');
await test.step('Open Discount rates tab', async () => {
const discountRatesTab = page.getByText('Discount rates', { exact: true }).first();
await expect(discountRatesTab, 'Discount rates tab should be visible').toBeVisible({ timeout: 10000 });
await discountRatesTab.click();
await page.waitForTimeout(1500);
console.log('✓ Opened Discount rates');
});
await test.step('Verify the Discount Rate Configuration table', async () => {
const ratesTable = page.locator('xpath=//*[contains(text(),"Discount Rate Configuration")]/following::table[1]').first();
await expect(ratesTable, 'Discount Rate Configuration table should be visible').toBeVisible({ timeout: 10000 });
const headers = await ratesTable.locator('thead th').allTextContents();
console.log('Headers:', JSON.stringify(headers));
let cardIdx = -1, mdrIdx = -1, vatIdx = -1, capIdx = -1, feeIdx = -1;
for (let i = 0; i < headers.length; i++) {
const t = headers[i].trim().toLowerCase();
if (t === 'card') cardIdx = i;
else if (t === 'mdr') mdrIdx = i;
else if (t === 'vat') vatIdx = i;
else if (t.includes('cap')) capIdx = i;
else if (t.includes('processing') || t === 'fee') feeIdx = i;
}
expect(cardIdx, 'A Card column should exist').toBeGreaterThanOrEqual(0);
expect(mdrIdx, 'An MDR column should exist').toBeGreaterThanOrEqual(0);
// Real data rows only - excludes the hidden measure row (aria-hidden)
// and an empty-state "No data" placeholder row (both previously got
// miscounted by earlier code as bogus zero-value rate entries).
const rows = ratesTable.locator('tbody tr:not([aria-hidden="true"])');
const rowCount = await rows.count();
console.log(`Row count (excluding hidden measure row): ${rowCount}`);
const bodyText = await ratesTable.locator('tbody').innerText();
const isEmptyState = /no data/i.test(bodyText) && rowCount <= 1;
if (isEmptyState) {
console.log('✓ No discount rates configured - "No data" empty state correctly shown');
// Scope to a real text-bearing element - a plain `text=/no data/i`
// locator also matches a hidden SVG <title> icon label with the same
// text, which is never actually visible.
await expect(ratesTable.locator('article, span, div').filter({ hasText: /^No data$/i }).first(), 'Empty state should show "No data"').toBeVisible();
} else {
console.log(`✓ Found ${rowCount} configured discount rate row(s), validating each`);
expect(rowCount, 'There should be at least one configured rate row').toBeGreaterThan(0);
// Read a numeric value the same safe way used for the Discounts page
// breakdown table: the value sits in the last <article> in the cell,
// separate from an embedded currency/percent icon's <style> content.
const readCellNum = async (row: import('@playwright/test').Locator, idx: number): Promise<number> => {
if (idx < 0) return 0;
const cell = row.locator('td').nth(idx);
const valArticle = cell.locator('article').last();
const txt = (await valArticle.textContent().catch(() => '')) || (await cell.textContent().catch(() => '')) || '';
const n = parseFloat(txt.replace(/[^0-9.\-]/g, ''));
return isNaN(n) ? 0 : n;
};
for (let r = 0; r < rowCount; r++) {
const row = rows.nth(r);
const cardText = (await row.locator('td').nth(cardIdx).innerText().catch(() => '')).trim();
const mdr = await readCellNum(row, mdrIdx);
const vat = vatIdx >= 0 ? await readCellNum(row, vatIdx) : 0;
const cap = capIdx >= 0 ? await readCellNum(row, capIdx) : 0;
const fee = feeIdx >= 0 ? await readCellNum(row, feeIdx) : 0;
console.log(` ${cardText}: MDR ${mdr}% | VAT ${vat}% | Cap ${cap} | Fee ${fee}%`);
expect(cardText.length, `Row ${r + 1} should have a card scheme name`).toBeGreaterThan(0);
expect(mdr, `Row ${r + 1} (${cardText}) MDR should be a plausible percentage`).toBeGreaterThanOrEqual(0);
expect(mdr, `Row ${r + 1} (${cardText}) MDR should be a plausible percentage`).toBeLessThanOrEqual(100);
}
}
});
console.log('✓ Discount rates settings test completed');
});
});