Files
Randify.pro/tests/dm/final-qa-script.ts
T

707 lines
28 KiB
TypeScript

import { chromium, type Browser, type Page } from "playwright";
import * as fs from "fs";
import * as path from "path";
function errMsg(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}
const BASE_URL = "http://localhost:4322";
const EVIDENCE_DIR = ".sisyphus/evidence/final-qa";
fs.mkdirSync(EVIDENCE_DIR, { recursive: true });
let browser: Browser;
let page: Page;
const results: {
scenario: string;
status: "PASS" | "FAIL" | "SKIP";
details: string;
}[] = [];
function log(scenario: string, status: "PASS" | "FAIL" | "SKIP", details: string) {
results.push({ scenario, status, details });
const icon = status === "PASS" ? "✓" : status === "FAIL" ? "✗" : "○";
console.log(`[${icon}] ${scenario}: ${details}`);
}
async function screenshot(name: string) {
const filePath = path.join(EVIDENCE_DIR, `${name}.png`);
try {
await page.screenshot({ path: filePath, fullPage: true });
} catch {
// ignore screenshot errors (e.g., page not ready)
}
return filePath;
}
async function setup() {
browser = await chromium.launch();
}
async function newPage(viewport?: { width: number; height: number }) {
if (page) await page.close();
page = await browser.newPage(viewport ? { viewport } : undefined);
}
async function teardown() {
await browser.close();
}
async function safeGoto(url: string) {
try {
await page.goto(url, { waitUntil: "load", timeout: 15000 });
await page.waitForTimeout(500);
return true;
} catch {
return false;
}
}
async function testTabs() {
console.log("\n=== T2: Tabs ===");
try {
await newPage({ width: 375, height: 812 });
const ok = await safeGoto(`${BASE_URL}/dm/`);
if (!ok) throw new Error("Failed to load page");
const tab = page.locator('[data-tab="initiative"]:visible');
await tab.click();
await page.waitForTimeout(300);
const url = page.url();
const hasHash = url.includes("#initiative");
await page.locator('[data-tab="dice"]:visible').focus();
await page.keyboard.press("ArrowRight");
await page.waitForTimeout(200);
const focusedAfterArrow = await page.evaluate(() =>
document.activeElement?.getAttribute("data-tab")
);
const pass = hasHash && focusedAfterArrow === "initiative";
log("T2: Tab switching and URL hash", pass ? "PASS" : "FAIL", `hash=${hasHash}, keyboardFocused=${focusedAfterArrow}`);
await screenshot("t2-tabs-navigation");
} catch (e) {
log("T2: Tab switching and URL hash", "FAIL", errMsg(e));
}
try {
await newPage({ width: 375, height: 812 });
const ok = await safeGoto(`${BASE_URL}/dm/`);
if (!ok) throw new Error("Failed to load page");
const tabList = page.locator(".dm-tabs:visible");
const box = await tabList.boundingBox();
const tabs = await page.locator(".dm-tab:visible").all();
let allInRow = true;
let minHeightOk = true;
for (const t of tabs) {
const tbox = await t.boundingBox();
if (tbox && box && Math.abs(tbox.y - box.y) > 5) allInRow = false;
if (tbox && tbox.height < 44) minHeightOk = false;
}
log("T2: Mobile scrollable tabs", allInRow && minHeightOk ? "PASS" : "FAIL", `allInRow=${allInRow}, minHeightOk=${minHeightOk}, tabCount=${tabs.length}`);
await screenshot("t2-tabs-mobile");
} catch (e) {
log("T2: Mobile scrollable tabs", "FAIL", errMsg(e));
}
try {
await newPage({ width: 375, height: 812 });
const ok = await safeGoto(`${BASE_URL}/dm/#initiative`);
if (!ok) throw new Error("Failed to load page");
const ariaSelected = await page.locator('[data-tab="initiative"]:visible').getAttribute("aria-selected");
log("T2: Initial tab state", ariaSelected === "true" ? "PASS" : "FAIL", `aria-selected=${ariaSelected}`);
await screenshot("t2-tabs-initial");
} catch (e) {
log("T2: Initial tab state", "FAIL", errMsg(e));
}
}
async function testDice() {
console.log("\n=== T9: Dice ===");
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#dice`);
if (!ok) throw new Error("Failed to load page");
await page.locator("[data-testid='dice-clear-btn']").first().click().catch(() => {});
await page.waitForTimeout(200);
await page.locator('[data-dice="d20"]').first().click();
await page.waitForTimeout(300);
const resultText = await page.locator("[data-testid='dice-result']").first().textContent();
const resultNum = parseInt(resultText || "0", 10);
const resultValid = resultNum >= 1 && resultNum <= 20;
await page.fill("[data-testid='dice-input']", "2d6+3");
await page.click("[data-testid='dice-roll-btn']");
await page.waitForTimeout(300);
const customResult = await page.locator("[data-testid='dice-result']").first().textContent();
const customNum = parseInt(customResult || "0", 10);
const customValid = customNum >= 5 && customNum <= 15;
const historyItems = await page.locator("[data-testid='dice-history-item']").count();
await page.locator("[data-testid='dice-clear-btn']").first().click();
await page.waitForTimeout(200);
const emptyVisible = await page.locator("[data-testid='dice-empty-state']").first().isVisible().catch(() => false);
const pass = resultValid && customValid && historyItems >= 2 && emptyVisible;
log("T9: Dice rolling and history", pass ? "PASS" : "FAIL", `d20=${resultNum}(valid=${resultValid}), 2d6+3=${customNum}(valid=${customValid}), history=${historyItems}, empty=${emptyVisible}`);
await screenshot("t9-dice-roller");
} catch (e) {
log("T9: Dice rolling and history", "FAIL", errMsg(e));
}
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#dice`);
if (!ok) throw new Error("Failed to load page");
let foundCrit = false;
let foundFail = false;
for (let i = 0; i < 30; i++) {
await page.locator('[data-dice="d20"]').first().click();
await page.waitForTimeout(200);
const resultText = await page.locator("[data-testid='dice-result']").first().textContent();
const resultNum = parseInt(resultText || "0", 10);
if (resultNum === 20) foundCrit = true;
if (resultNum === 1) foundFail = true;
if (foundCrit && foundFail) break;
}
const historyItems = await page.locator("[data-testid='dice-history-item']").all();
let colorClassesFound = false;
for (const item of historyItems) {
const html = await item.innerHTML();
if (html.includes("text-[#6ab04c]") || html.includes("text-[#e06060]") || html.includes("text-[var(--gold)]")) {
colorClassesFound = true;
break;
}
}
const pass = colorClassesFound;
log("T9: Critical hit/fail colors", pass ? "PASS" : "FAIL", `foundCrit=${foundCrit}, foundFail=${foundFail}, colorClasses=${colorClassesFound}`);
await screenshot("t9-dice-crit");
} catch (e) {
log("T9: Critical hit/fail colors", "FAIL", errMsg(e));
}
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#dice`);
if (!ok) throw new Error("Failed to load page");
const d20Btn = page.locator('[data-dice="d20"]').first();
const hoverClassCheck = await d20Btn.evaluate((el) => el.classList.contains("hover:bg-[var(--gold)]/10"));
const activeClassCheck = await d20Btn.evaluate((el) => el.classList.contains("active:scale-[0.97]"));
const pass = hoverClassCheck && activeClassCheck;
log("T9: Hover and active states", pass ? "PASS" : "FAIL", `hoverClass=${hoverClassCheck}, activeClass=${activeClassCheck}`);
await screenshot("t9-dice-states");
} catch (e) {
log("T9: Hover and active states", "FAIL", errMsg(e));
}
}
async function testInitiative() {
console.log("\n=== T10: Initiative ===");
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#initiative`);
if (!ok) throw new Error("Failed to load page");
await page.locator("#it-clear-btn").first().click().catch(() => {});
await page.waitForTimeout(200);
await page.fill("[data-testid='init-name-input']", "Гоблин");
await page.fill("[data-testid='init-mod-input']", "2");
await page.fill("[data-testid='init-initiative-input']", "18");
await page.click("[data-testid='init-add-btn']");
await page.waitForTimeout(300);
const goblinVisible = await page.locator("[data-testid='init-combatant']").first().isVisible().catch(() => false);
const goblinName = await page.locator("[data-testid='init-combatant-name']").first().textContent();
await page.fill("[data-testid='init-name-input']", "Орк");
await page.fill("[data-testid='init-mod-input']", "0");
await page.fill("[data-testid='init-initiative-input']", "12");
await page.click("[data-testid='init-add-btn']");
await page.waitForTimeout(300);
const scores = await page.locator("[data-testid='init-score']").allTextContents();
const sortedCorrectly = parseInt(scores[0]) > parseInt(scores[1]);
const firstActive = await page.locator("[data-testid='init-combatant'].active").first().textContent();
await page.click("[data-testid='init-next-btn']");
await page.waitForTimeout(200);
const secondActive = await page.locator("[data-testid='init-combatant'].active").first().textContent();
const turnChanged = firstActive !== secondActive;
await page.locator("[data-testid='init-delete-btn']").first().click();
await page.waitForTimeout(200);
const countAfterDelete = await page.locator("[data-testid='init-combatant']").count();
const pass = goblinVisible && goblinName?.includes("Гоблин") && sortedCorrectly && turnChanged && countAfterDelete === 1;
log("T10: Initiative tracking flow", pass ? "PASS" : "FAIL", `goblin=${goblinVisible}, sorted=${sortedCorrectly}, turnChanged=${turnChanged}, afterDelete=${countAfterDelete}`);
await screenshot("t10-initiative");
} catch (e) {
log("T10: Initiative tracking flow", "FAIL", errMsg(e));
}
}
async function testOpen5e() {
console.log("\n=== T11: Open5e ===");
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#reference`);
if (!ok) throw new Error("Failed to load page");
await page.waitForTimeout(3000);
const resultsVisible = await page.locator("[data-testid='ref-result-card']:visible").first().isVisible().catch(() => false);
const emptyVisible = await page.locator("#o5-empty:visible").first().isVisible().catch(() => false);
await page.fill("[data-testid='ref-search-input']:visible", "goblin");
await page.waitForTimeout(800);
const searchResults = await page.locator("[data-testid='ref-result-card']:visible").count();
let modalOpened = false;
let modalClosed = false;
if (searchResults > 0) {
const firstCard = page.locator("[data-testid='ref-result-card']:visible").first();
await firstCard.scrollIntoViewIfNeeded();
await firstCard.click();
await page.waitForTimeout(1500);
modalOpened = await page.locator("[data-testid='ref-modal']:visible").first().isVisible().catch(() => false);
if (modalOpened) {
await page.keyboard.press("Escape");
await page.waitForTimeout(500);
modalClosed = !(await page.locator("[data-testid='ref-modal']:visible").first().isVisible().catch(() => false));
}
}
const pass = (resultsVisible || emptyVisible) && (searchResults > 0 || emptyVisible);
log("T11: Open5e search and detail", pass ? "PASS" : "FAIL", `results=${resultsVisible}, empty=${emptyVisible}, searchResults=${searchResults}, modalOpened=${modalOpened}, modalClosed=${modalClosed}`);
await screenshot("t11-open5e");
} catch (e) {
log("T11: Open5e search and detail", "FAIL", errMsg(e));
}
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#reference`);
if (!ok) throw new Error("Failed to load page");
await page.waitForTimeout(3000);
await page.fill("[data-testid='ref-search-input']:visible", "xyznonexistent12345");
await page.waitForTimeout(1200);
const emptyVisible = await page.locator("#o5-empty:visible").first().isVisible().catch(() => false);
const noResults = await page.locator("[data-testid='ref-result-card']:visible").count() === 0;
log("T11: Empty search state", emptyVisible || noResults ? "PASS" : "FAIL", `emptyVisible=${emptyVisible}, noResults=${noResults}`);
await screenshot("t11-open5e-empty");
} catch (e) {
log("T11: Empty search state", "FAIL", errMsg(e));
}
}
async function testNotes() {
console.log("\n=== T12: Notes ===");
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#notes`);
if (!ok) throw new Error("Failed to load page");
await page.fill("[data-testid='notes-textarea']:visible", "Тестовая заметка");
await page.waitForTimeout(1000);
const savedIndicator = page.locator("[data-testid='notes-saved-indicator']:visible").first();
const savedVisible = await savedIndicator.isVisible().catch(() => false);
const savedOpacity = await savedIndicator.evaluate((el) => {
const style = window.getComputedStyle(el);
return style.opacity;
});
const charCount = await page.locator("[data-testid='notes-char-count']:visible").first().textContent();
const page2 = await browser.newPage();
await page2.goto(`${BASE_URL}/dm/#notes`, { waitUntil: "load" });
await page2.waitForTimeout(1500);
const value2 = await page2.locator("[data-testid='notes-textarea']:visible").inputValue();
const syncWorked = value2 === "Тестовая заметка";
await page.click("[data-testid='notes-clear-btn']:visible");
await page.waitForTimeout(800);
const cleared = await page.locator("[data-testid='notes-textarea']:visible").inputValue() === "";
await page2.close();
const pass = savedVisible && parseFloat(savedOpacity) > 0.1 && charCount === "16" && syncWorked && cleared;
log("T12: Notes auto-save and cross-tab sync", pass ? "PASS" : "FAIL", `savedVisible=${savedVisible}, savedOpacity=${savedOpacity}, chars=${charCount}, sync=${syncWorked}, cleared=${cleared}`);
await screenshot("t12-notes-sync");
} catch (e) {
log("T12: Notes auto-save and cross-tab sync", "FAIL", errMsg(e));
}
}
async function testPage() {
console.log("\n=== T6: Page ===");
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/`);
if (!ok) throw new Error("Failed to load page");
const sections = ["dice", "initiative", "reference", "notes"];
let allAccessible = true;
for (const id of sections) {
const section = page.locator(`section#${id}:visible`).first();
const visible = await section.isVisible().catch(() => false);
if (!visible) {
allAccessible = false;
console.log(` Section #${id} not visible`);
}
}
log("T6: All sections accessible", allAccessible ? "PASS" : "FAIL", `allAccessible=${allAccessible}`);
await screenshot("t6-sections");
} catch (e) {
log("T6: All sections accessible", "FAIL", errMsg(e));
}
}
async function testIntegration() {
console.log("\n=== Integration Tests ===");
try {
await newPage({ width: 375, height: 812 });
const ok = await safeGoto(`${BASE_URL}/dm/`);
if (!ok) throw new Error("Failed to load page");
const tabs = ["dice", "initiative", "reference", "notes"];
let pass = true;
for (const tabId of tabs) {
await page.locator(`[data-tab="${tabId}"]:visible`).click();
await page.waitForTimeout(400);
const url = page.url();
if (!url.includes(`#${tabId}`)) pass = false;
}
log("INT-1: Switch between all tabs", pass ? "PASS" : "FAIL", `urlHashOk=${pass}`);
} catch (e) {
log("INT-1: Switch between all tabs", "FAIL", errMsg(e));
}
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#dice`);
if (!ok) throw new Error("Failed to load page");
await page.locator("[data-testid='dice-clear-btn']").first().click().catch(() => {});
await page.waitForTimeout(200);
await page.locator('[data-dice="d20"]').first().click();
await page.waitForTimeout(200);
await page.locator('[data-dice="d6"]').first().click();
await page.waitForTimeout(200);
const historyCount = await page.locator("[data-testid='dice-history-item']").count();
await page.locator("[data-testid='dice-clear-btn']").first().click();
await page.waitForTimeout(200);
const emptyVisible = await page.locator("[data-testid='dice-empty-state']").first().isVisible().catch(() => false);
const pass = historyCount >= 2 && emptyVisible;
log("INT-2: Roll dice, check history, clear", pass ? "PASS" : "FAIL", `history=${historyCount}, empty=${emptyVisible}`);
} catch (e) {
log("INT-2: Roll dice, check history, clear", "FAIL", errMsg(e));
}
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#initiative`);
if (!ok) throw new Error("Failed to load page");
await page.locator("#it-clear-btn").first().click().catch(() => {});
await page.waitForTimeout(200);
await page.fill("[data-testid='init-name-input']", "A");
await page.fill("[data-testid='init-initiative-input']", "25");
await page.click("[data-testid='init-add-btn']");
await page.waitForTimeout(200);
await page.fill("[data-testid='init-name-input']", "B");
await page.fill("[data-testid='init-initiative-input']", "10");
await page.click("[data-testid='init-add-btn']");
await page.waitForTimeout(200);
const scores = await page.locator("[data-testid='init-score']").allTextContents();
const sorted = parseInt(scores[0]) > parseInt(scores[1]);
const firstActive = await page.locator("[data-testid='init-combatant'].active").first().textContent();
await page.click("[data-testid='init-next-btn']");
await page.waitForTimeout(200);
const secondActive = await page.locator("[data-testid='init-combatant'].active").first().textContent();
const turnChanged = firstActive !== secondActive;
await page.locator("[data-testid='init-delete-btn']").first().click();
await page.waitForTimeout(200);
const countAfterDelete = await page.locator("[data-testid='init-combatant']").count();
const pass = sorted && turnChanged && countAfterDelete === 1;
log("INT-3: Initiative flow", pass ? "PASS" : "FAIL", `sorted=${sorted}, turnChanged=${turnChanged}, afterDelete=${countAfterDelete}`);
} catch (e) {
log("INT-3: Initiative flow", "FAIL", errMsg(e));
}
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#reference`);
if (!ok) throw new Error("Failed to load page");
await page.waitForTimeout(3000);
await page.fill("[data-testid='ref-search-input']:visible", "dragon");
await page.waitForTimeout(600);
const cards = await page.locator("[data-testid='ref-result-card']:visible").count();
let modalOpened = false;
let modalClosed = false;
if (cards > 0) {
const firstCard = page.locator("[data-testid='ref-result-card']:visible").first();
await firstCard.scrollIntoViewIfNeeded();
await firstCard.click();
await page.waitForTimeout(1500);
modalOpened = await page.locator("[data-testid='ref-modal']:visible").first().isVisible().catch(() => false);
if (modalOpened) {
await page.keyboard.press("Escape");
await page.waitForTimeout(500);
modalClosed = !(await page.locator("[data-testid='ref-modal']:visible").first().isVisible().catch(() => false));
}
}
const pass = cards > 0 && modalOpened && modalClosed;
log("INT-4: Open5e search and modal", pass ? "PASS" : "FAIL", `cards=${cards}, modalOpened=${modalOpened}, modalClosed=${modalClosed}`);
} catch (e) {
log("INT-4: Open5e search and modal", "FAIL", errMsg(e));
}
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#notes`);
if (!ok) throw new Error("Failed to load page");
await page.fill("[data-testid='notes-textarea']:visible", "Persistent note test");
await page.waitForTimeout(800);
await page.reload({ waitUntil: "load" });
await page.waitForTimeout(500);
const value = await page.locator("[data-testid='notes-textarea']:visible").inputValue();
const pass = value === "Persistent note test";
log("INT-5: Notes persistence", pass ? "PASS" : "FAIL", `value="${value}"`);
} catch (e) {
log("INT-5: Notes persistence", "FAIL", errMsg(e));
}
try {
await newPage({ width: 1440, height: 900 });
const ok = await safeGoto(`${BASE_URL}/dm/`);
if (!ok) throw new Error("Failed to load page");
await page.waitForTimeout(500);
const sidebarDesktop = await page.locator("aside >> visible=true").count() === 2;
await page.setViewportSize({ width: 375, height: 812 });
await page.waitForTimeout(500);
const sidebarMobile = await page.locator("aside >> visible=true").count() === 0;
await page.setViewportSize({ width: 1440, height: 900 });
await page.waitForTimeout(500);
const sidebarBack = await page.locator("aside >> visible=true").count() === 2;
const pass = sidebarDesktop && sidebarMobile && sidebarBack;
log("INT-6: Responsive layout", pass ? "PASS" : "FAIL", `desktop=${sidebarDesktop}, mobile=${sidebarMobile}, back=${sidebarBack}`);
await screenshot("int6-responsive-desktop");
await page.setViewportSize({ width: 375, height: 812 });
await page.waitForTimeout(300);
await screenshot("int6-responsive-mobile");
} catch (e) {
log("INT-6: Responsive layout", "FAIL", errMsg(e));
}
}
async function testEdgeCases() {
console.log("\n=== Edge Cases ===");
try {
await newPage({ width: 1280, height: 720 });
let ok = await safeGoto(`${BASE_URL}/dm/#dice`);
if (!ok) throw new Error("Failed to load dice");
await page.locator("[data-testid='dice-clear-btn']").first().click().catch(() => {});
await page.waitForTimeout(200);
const diceEmpty = await page.locator("[data-testid='dice-empty-state']").first().isVisible().catch(() => false);
ok = await safeGoto(`${BASE_URL}/dm/#initiative`);
if (!ok) throw new Error("Failed to load initiative");
await page.locator("#it-clear-btn").first().click().catch(() => {});
await page.waitForTimeout(200);
const initEmpty = await page.locator("[data-testid='init-empty-state']").first().isVisible().catch(() => false);
ok = await safeGoto(`${BASE_URL}/dm/#notes`);
if (!ok) throw new Error("Failed to load notes");
await page.click("[data-testid='notes-clear-btn']:visible").catch(() => {});
await page.waitForTimeout(200);
const notesEmpty = await page.locator("[data-testid='notes-textarea']:visible").inputValue() === "";
const pass = diceEmpty && initEmpty && notesEmpty;
log("EDGE-1: Empty states", pass ? "PASS" : "FAIL", `dice=${diceEmpty}, init=${initEmpty}, notes=${notesEmpty}`);
await screenshot("edge1-empty-states");
} catch (e) {
log("EDGE-1: Empty states", "FAIL", errMsg(e));
}
try {
await newPage({ width: 375, height: 812 });
const ok = await safeGoto(`${BASE_URL}/dm/`);
if (!ok) throw new Error("Failed to load page");
const tabs = ["dice", "initiative", "reference", "notes"];
for (let i = 0; i < 10; i++) {
const tabId = tabs[i % tabs.length];
await page.locator(`[data-tab="${tabId}"]:visible`).click().catch(() => {});
}
await page.waitForTimeout(500);
const url = page.url();
const hasValidHash = tabs.some((t) => url.includes(`#${t}`));
log("EDGE-2: Rapid tab switching", hasValidHash ? "PASS" : "FAIL", `url=${url}`);
} catch (e) {
log("EDGE-2: Rapid tab switching", "FAIL", errMsg(e));
}
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#dice`);
if (!ok) throw new Error("Failed to load page");
await page.fill("[data-testid='dice-input']", "invalid!!!");
await page.click("[data-testid='dice-roll-btn']");
await page.waitForTimeout(300);
const resultText = await page.locator("[data-testid='dice-result']").first().textContent();
const detailsText = await page.locator("#result-details").first().textContent();
const pass = resultText === "?" || detailsText?.includes("Invalid");
log("EDGE-3: Invalid dice notation", pass ? "PASS" : "FAIL", `result="${resultText}", details="${detailsText}"`);
} catch (e) {
log("EDGE-3: Invalid dice notation", "FAIL", errMsg(e));
}
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#reference`);
if (!ok) throw new Error("Failed to load page");
await page.waitForTimeout(3000);
await page.fill("[data-testid='ref-search-input']:visible", "");
await page.waitForTimeout(500);
const hasResults = await page.locator("[data-testid='ref-result-card']:visible").count() > 0;
const hasEmptyState = await page.locator("#o5-empty:visible").first().isVisible().catch(() => false);
const noError = !(await page.locator("#o5-error:visible").first().isVisible().catch(() => false));
const pass = (hasResults || hasEmptyState) && noError;
log("EDGE-4: Empty search in Open5e", pass ? "PASS" : "FAIL", `results=${hasResults}, empty=${hasEmptyState}, noError=${noError}`);
} catch (e) {
log("EDGE-4: Empty search in Open5e", "FAIL", errMsg(e));
}
try {
await newPage({ width: 1280, height: 720 });
const ok = await safeGoto(`${BASE_URL}/dm/#notes`);
if (!ok) throw new Error("Failed to load page");
await page.fill("[data-testid='notes-textarea']:visible", "Note to clear");
await page.waitForTimeout(200);
await page.click("[data-testid='notes-clear-btn']:visible");
await page.waitForTimeout(200);
const value = await page.locator("[data-testid='notes-textarea']:visible").inputValue();
const charCount = await page.locator("[data-testid='notes-char-count']:visible").first().textContent();
const pass = value === "" && charCount === "0";
log("EDGE-5: Clear notes after typing", pass ? "PASS" : "FAIL", `value="${value}", chars=${charCount}`);
} catch (e) {
log("EDGE-5: Clear notes after typing", "FAIL", errMsg(e));
}
}
async function main() {
console.log("Starting Final QA Execution...");
console.log(`Base URL: ${BASE_URL}`);
await setup();
await testTabs();
await testDice();
await testInitiative();
await testOpen5e();
await testNotes();
await testPage();
await testIntegration();
await testEdgeCases();
await teardown();
console.log("\n========== FINAL QA REPORT ==========");
const scenarioResults = results.filter((r) => !r.scenario.startsWith("INT-") && !r.scenario.startsWith("EDGE-"));
const integrationResults = results.filter((r) => r.scenario.startsWith("INT-"));
const edgeResults = results.filter((r) => r.scenario.startsWith("EDGE-"));
console.log(`Scenarios: ${scenarioResults.filter((r) => r.status === "PASS").length}/${scenarioResults.length} pass`);
console.log(`Integration: ${integrationResults.filter((r) => r.status === "PASS").length}/${integrationResults.length}`);
console.log(`Edge Cases: ${edgeResults.filter((r) => r.status === "PASS").length}/${edgeResults.length}`);
for (const r of results) {
const icon = r.status === "PASS" ? "✓" : "✗";
console.log(` ${icon} ${r.scenario}: ${r.details}`);
}
const verdict = results.every((r) => r.status === "PASS") ? "APPROVE" : "REJECT";
console.log(`\nVERDICT: ${verdict}`);
const reportPath = path.join(EVIDENCE_DIR, "report.txt");
const reportLines = [
`Scenarios [${scenarioResults.filter((r) => r.status === "PASS").length}/${scenarioResults.length} pass] | Integration [${integrationResults.filter((r) => r.status === "PASS").length}/${integrationResults.length}] | Edge Cases [${edgeResults.length} tested] | VERDICT: ${verdict}`,
"",
"Details:",
...results.map((r) => `${r.status}: ${r.scenario} — ${r.details}`),
];
fs.writeFileSync(reportPath, reportLines.join("\n"));
console.log(`Report saved to: ${reportPath}`);
}
main().catch((err) => {
console.error("QA script failed:", err);
process.exit(1);
});