import { describe, it, expect, beforeAll, afterAll } from "vitest"; import puppeteer, { Browser, Page } from "puppeteer"; import * as net from "net"; import { build, preview } from "vite"; // Porta padrão do Vite const PORT = 5173; const ORIGIN = `http://127.0.0.1:${PORT}`; function waitForPort(port: number, timeoutMs = 20000): Promise { const start = Date.now(); return new Promise((resolve, reject) => { const tryOnce = () => { const socket = net.connect(port, "127.0.0.1"); socket.on("connect", () => { socket.end(); resolve(); }); socket.on("error", () => { socket.destroy(); if (Date.now() - start > timeoutMs) reject(new Error("Timeout aguardando Vite dev server")); else setTimeout(tryOnce, 300); }); }; tryOnce(); }); } let browser: Browser; let page: Page; let previewServer: Awaited> | undefined; let built = false; async function ensurePreviewServer() { // Se já existe algo na porta (ex dev aberto manualmente), apenas usa try { await waitForPort(PORT, 800); return; } catch { /* inicia preview */ } if (!built) { await build(); built = true; } previewServer = await preview({ preview: { port: PORT, host: "127.0.0.1" }, server: { middlewareMode: false }, } as unknown as Parameters[0]); await waitForPort(PORT); } describe("E2E Accessibility Menu", () => { beforeAll(async () => { await ensurePreviewServer(); browser = await puppeteer.launch({ headless: true }); page = await browser.newPage(); await page.goto(ORIGIN, { waitUntil: "domcontentloaded" }); }, 90000); afterAll(async () => { if (browser) await browser.close(); if (previewServer) { // @ts-expect-error acesso interno não tipado const httpServer = previewServer.httpServer || previewServer.server?.httpServer; if (httpServer) httpServer.close(); } }); it("abre e fecha o diálogo de acessibilidade", async () => { // Botão flutuante await page.waitForSelector('button[aria-label="Menu de Acessibilidade"]', { timeout: 10000, }); await page.click('button[aria-label="Menu de Acessibilidade"]'); await page.waitForSelector('div[role="dialog"][aria-modal="true"]', { timeout: 5000, }); const exists = await page.$('div[role="dialog"][aria-modal="true"]'); expect(exists).not.toBeNull(); // Pressiona ESC para fechar await page.keyboard.press("Escape"); // Pequeno delay await new Promise((r) => setTimeout(r, 150)); const still = await page.$('div[role="dialog"][aria-modal="true"]'); expect(still).toBeNull(); }, 30000); it("ativa dark mode e alto contraste e persiste após reload", async () => { // Abre menu (caso esteja fechado) const trigger = await page.$('button[aria-label="Menu de Acessibilidade"]'); if (trigger) { await trigger.click(); await page.waitForSelector('div[role="dialog"][aria-modal="true"]', { timeout: 5000, }); } // Helper para clicar botão pelo texto visível interno async function clickToggleByAria(label: string) { const selector = `button[aria-label="${label}"]`; await page.waitForSelector(selector, { timeout: 5000 }); await page.click(selector); } // Ativa Modo Escuro e Alto Contraste (aria-label fica igual ao label) await clickToggleByAria("Modo Escuro"); await clickToggleByAria("Alto Contraste"); // Verifica classes aplicadas const classesBefore = await page.evaluate(() => Array.from(document.documentElement.classList) ); expect(classesBefore).toContain("dark"); expect(classesBefore).toContain("high-contrast"); // Recarrega página para validar persistência (localStorage -> rehidratação) await page.reload({ waitUntil: "domcontentloaded" }); const classesAfter = await page.evaluate(() => Array.from(document.documentElement.classList) ); expect(classesAfter).toContain("dark"); expect(classesAfter).toContain("high-contrast"); // (Opcional) Reabre menu e desfaz para não impactar execuções subsequentes const trigger2 = await page.$( 'button[aria-label="Menu de Acessibilidade"]' ); if (trigger2) { await trigger2.click(); await page.waitForSelector('div[role="dialog"][aria-modal="true"]', { timeout: 5000, }); await clickToggleByAria("Modo Escuro"); await clickToggleByAria("Alto Contraste"); await page.keyboard.press("Escape"); } }, 45000); });