From: Alexander Moisseev Date: Sun, 31 Aug 2025 14:50:17 +0000 (+0300) Subject: [Test] Add WebUI E2E scan flow test X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=refs%2Fpull%2F5606%2Fhead;p=thirdparty%2Frspamd.git [Test] Add WebUI E2E scan flow test Covers reading counters before scanning, scanning two test messages, verifying history, resetting history, and checking updated counters. --- diff --git a/eslint.config.mjs b/eslint.config.mjs index 4ee3bb4f3a..80eca84197 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -89,6 +89,7 @@ export default [ files: ["test/playwright/tests/*.mjs"], rules: { "no-await-in-loop": "off", // Playwright operations in loops are often sequential and not independent + "no-empty-pattern": ["error", {allowObjectPatternsAsParameters: true}], "sort-keys": ["error", "asc", {minKeys: 4}] }, }, diff --git a/test/playwright/tests/scan.spec.mjs b/test/playwright/tests/scan.spec.mjs new file mode 100644 index 0000000000..2e6447fa5e --- /dev/null +++ b/test/playwright/tests/scan.spec.mjs @@ -0,0 +1,148 @@ +import {expect, test} from "@playwright/test"; +import {login} from "../helpers/auth.mjs"; + +test.describe.serial("Scan flow across WebUI tabs", () => { + let page = null; + let scannedSubjects = []; + const scannedBefore = {scanTab: 0, throughput: 0}; + + async function gotoTab(name) { + await page.locator(`#${name}_nav`).click(); + } + + function extractNumber(text) { + return parseInt(text.replace(/\D/g, ""), 10); + } + + async function readScanTab() { + // Status tab → scanned widget + await gotoTab("status"); + await page.waitForResponse((resp) => resp.url().includes("/stat") && resp.status() === 200); + const scannedWidget = page.locator("#statWidgets .widget[title*='scanned']"); + await expect(scannedWidget).toBeVisible(); + const scannedTitle = await scannedWidget.getAttribute("title"); + return extractNumber(scannedTitle); + } + + async function readThroughput() { + // Throughput tab → id="rrd-total-value" + await gotoTab("throughput"); + await page.waitForResponse((resp) => resp.url().includes("/graph") && resp.status() === 200); + const throughputValue = page.locator("#rrd-total-value"); + await expect(throughputValue).toBeVisible(); + return extractNumber(await throughputValue.textContent()); + } + + async function expectAlertSuccess(expectedText) { + const alert = page.locator(".alert-success, .alert-modal.alert-success"); + await expect(alert).toBeVisible(); + const text = await alert.textContent(); + expect(text).toContain(expectedText); + await expect(alert).not.toBeVisible({timeout: 10000}); + } + + test.beforeAll(async ({browser}, testInfo) => { + const context = await browser.newContext(); + page = await context.newPage(); + const {enablePassword} = testInfo.project.use.rspamdPasswords; + await login(page, enablePassword); + }); + + test.afterAll(async () => { + await page.close(); + }); + + test.describe("Phase 1: before scanning", () => { + test("Read current Scanned counters", async () => { + scannedBefore.scanTab = await readScanTab(); + scannedBefore.throughput = await readThroughput(); + }); + }); + + test.describe("Phase 2: scanning", () => { + test("Scan two test messages", async ({}, testInfo) => { + const {testId} = testInfo; + await gotoTab("scan"); + const scanMessageBtn = page.locator('#scan button[data-upload="checkv2"]'); + await expect(scanMessageBtn).toBeDisabled(); + + scannedSubjects = []; + + for (let i = 1; i <= 2; i++) { + const timestamp = Date.now(); + const msgId = `E2E-${i}-${timestamp}@example.com`; + const subject = `E2E Test ${i} ${testId}-${timestamp}`; + scannedSubjects.push(subject); + + await page.locator("#scanMsgSource").fill( + `Message-Id: ${msgId}\nFrom: test@example.com\nSubject: ${subject}\n\nTest body` + ); + await scanMessageBtn.click(); + + await expectAlertSuccess("Data successfully scanned"); + await expect( + page.locator(`#historyTable_scan tbody tr:first-child td.footable-first-visible:has-text("${msgId}")`) + ).toBeVisible(); + } + }); + }); + + test.describe("Phase 3: after scanning", () => { + test("History shows scanned messages and can be reset", async () => { + await gotoTab("history"); + + // Check both scanned messages are present in reverse order + for (let i = 0; i < scannedSubjects.length; i++) { + const subject = scannedSubjects[scannedSubjects.length - 1 - i]; // reverse order + await expect( + page.locator("#historyTable_history tbody tr").nth(i) + .locator(`td:has-text("${subject}")`) + ).toBeVisible(); + } + + // Reset history + const resetBtn = page.locator("#resetHistory"); + await expect(resetBtn).toBeVisible(); + page.once("dialog", (dialog) => dialog.accept()); + await resetBtn.click(); + + const updateHistoryBtn = page.locator("#updateHistory"); + await expect(updateHistoryBtn).toBeDisabled(); + await expect(updateHistoryBtn).not.toBeDisabled(); + + const rows = await page.locator("#historyTable_history tbody tr").count(); + // Known bug: Rspamd leaves one row after history reset + expect([0, 1]).toContain(rows); + }); + }); + + test.describe("Phase 4: counters after scanning", () => { + test("Status tab `Scanned` counter increased by 2", async () => { + const scanTab = await readScanTab(); + expect(scanTab).toBe(scannedBefore.scanTab + 2); + }); + + test("Throughput `Total messages` counter increased", async ({}, testInfo) => { + testInfo.setTimeout(140000); + // Depending on row boundaries, throughput may show +2 or even +3 + const targetValues = [ + scannedBefore.throughput + 2, + scannedBefore.throughput + 3, + ]; + + let lastValue = null; + try { + await expect.poll(async () => { + lastValue = await readThroughput(); + return targetValues.includes(lastValue); + }, { + interval: 5000, + // step = 1s, pdp_per_row = 60 → next row every 60s + timeout: 125000, + }).toBeTruthy(); + } catch (e) { + throw new Error(`Throughput counter should be one of [${targetValues.join(", ")}], got ${lastValue}`); + } + }); + }); +});