import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test"; import { chromium } from "playwright-core"; import { startApi, startPage } from "./server.js"; function chromiumPath() { if (process.env.CEPIS_CHROMIUM) { return process.env.CEPIS_CHROMIUM; } const root = `${process.env.HOME}/.cache/ms-playwright`; const found = [ ...new Bun.Glob("chromium-*/chrome-linux64/chrome").scanSync({ cwd: root, absolute: true }), ].sort(); return found.at(-1); } let browser; let context; let page; let api; let pages; beforeAll(async () => { api = startApi(); pages = await startPage(); browser = await chromium.launch({ headless: true, executablePath: chromiumPath() }); context = await browser.newContext(); }); afterAll(async () => { await context?.close(); await browser?.close(); api?.stop(); pages?.stop(); }); afterEach(async () => { await page?.close(); page = undefined; api.reset(); }); async function open(options = {}) { page = await context.newPage(); await page.goto(pages.url); await page.evaluate((given) => window.__attach(given), { baseUrl: api.url, ...options }); return page; } function typeCep(text) { return page.locator("#cep").pressSequentially(text, { delay: 15 }); } async function clearCep() { await page.locator("#cep").press("Control+a"); await page.locator("#cep").press("Backspace"); } function value(selector) { return page.locator(selector).inputValue(); } function statusText() { return page.locator(".cepis-autofill-status").textContent(); } function events() { return page.evaluate(() => window.__events); } async function waitForRequests(count, timeout = 4000) { const deadline = Date.now() + timeout; while (Date.now() < deadline) { if (api.state.requests.length >= count) { return; } await Bun.sleep(20); } throw new Error(`expected ${count} request(s), saw ${api.state.requests.length}`); } describe("filling", () => { test("a typed CEP fills every mapped field", async () => { await open(); await typeCep("01001000"); await page.waitForFunction(() => document.querySelector("#rua").value !== ""); expect(await value("#rua")).toBe("Praça da Sé"); expect(await value("#bairro")).toBe("Sé"); expect(await value("#cidade")).toBe("São Paulo"); expect(await value("#uf")).toBe("SP"); expect(await value("#numero")).toBe(""); expect(await events()).toContain("result:01001000"); }); test("the 01001-000 mask is accepted", async () => { await open(); await typeCep("01001-000"); await page.waitForFunction(() => document.querySelector("#cidade").value !== ""); expect(await value("#cidade")).toBe("São Paulo"); expect(api.state.requests).toEqual(["01001000"]); }); test("nothing is requested before the eighth digit", async () => { await open(); await typeCep("0100100"); await Bun.sleep(300); expect(api.state.requests).toEqual([]); expect(await statusText()).toBe(""); }); test("focusNumero moves focus to the house number", async () => { await open({ focusNumero: true }); await typeCep("01001000"); await page.waitForFunction(() => document.activeElement.id === "numero"); expect(await page.evaluate(() => document.activeElement.id)).toBe("numero"); }); }); describe("requests", () => { test("debounce coalesces a burst of keystrokes into one request", async () => { await open({ debounceMs: 200 }); await page.locator("#cep").pressSequentially("01001000", { delay: 5 }); await page.waitForFunction(() => document.querySelector("#rua").value !== ""); await Bun.sleep(300); expect(api.state.requests).toEqual(["01001000"]); }, 10000); test("a stale response never overwrites a newer one", async () => { api.state.delays["01001000"] = 600; await open(); await typeCep("01001000"); await waitForRequests(1); expect(api.state.requests).toEqual(["01001000"]); await page.locator("#cep").press("Control+a"); await typeCep("20040020"); await page.waitForFunction(() => document.querySelector("#cidade").value === "Rio de Janeiro"); await Bun.sleep(800); expect(await value("#rua")).toBe("Avenida Rio Branco"); expect(await value("#cidade")).toBe("Rio de Janeiro"); expect(await value("#uf")).toBe("RJ"); expect(await events()).toEqual(expect.arrayContaining(["result:20040020"])); expect(await events()).not.toContain("result:01001000"); expect(api.state.aborted).toBeGreaterThan(0); }, 15000); test("a CEP replaced in one shot does not lose the race either", async () => { api.state.delays["01001000"] = 600; await open(); await typeCep("01001000"); await waitForRequests(1); await page.locator("#cep").selectText(); await page.keyboard.insertText("20040020"); await page.waitForFunction(() => document.querySelector("#cidade").value === "Rio de Janeiro"); await Bun.sleep(800); expect(await value("#rua")).toBe("Avenida Rio Branco"); expect(await value("#cidade")).toBe("Rio de Janeiro"); expect(await events()).not.toContain("result:01001000"); expect(await statusText()).toBe("Endereço encontrado."); }, 15000); test("the same CEP is only requested once", async () => { await open(); await typeCep("01001000"); await page.waitForFunction(() => document.querySelector("#rua").value !== ""); await clearCep(); await typeCep("01001000"); await Bun.sleep(400); expect(await value("#cidade")).toBe("São Paulo"); expect(api.state.requests).toEqual(["01001000"]); }, 10000); test("detach() stops every further request", async () => { await open(); await page.evaluate(() => window.__handle.detach()); await typeCep("01001000"); await Bun.sleep(400); expect(api.state.requests).toEqual([]); expect(await value("#rua")).toBe(""); expect(await page.locator(".cepis-autofill-status").count()).toBe(0); }); }); describe("errors", () => { test("a CEP outside the database says so", async () => { await open(); await typeCep("99999999"); await page.waitForFunction(() => document.querySelector(".cepis-autofill-status").textContent.includes("não encontrado"), ); expect(await statusText()).toContain("CEP não encontrado"); expect(await value("#rua")).toBe(""); expect(await events()).toContain("error:not_found"); }); test("an unreachable API says so", async () => { const dead = startApi(); const url = dead.url; dead.stop(); await open({ baseUrl: url }); await typeCep("01001000"); await page.waitForFunction(() => document.querySelector(".cepis-autofill-status").textContent.includes("conexão"), ); expect(await statusText()).toContain("Não foi possível consultar o CEP"); expect(await events()).toContain("error:network"); }, 10000); test("a 500 and a 429 get their own message", async () => { await open(); await typeCep("88888888"); await page.waitForFunction(() => document.querySelector(".cepis-autofill-status").textContent.includes("indisponível"), ); expect(await statusText()).toContain("O serviço de CEP está indisponível"); await clearCep(); await typeCep("77777777"); await page.waitForFunction(() => document.querySelector(".cepis-autofill-status").textContent.includes("Muitas consultas"), ); expect(await events()).toContain("error:rate_limited"); }, 10000); test("more than eight digits is rejected without a request", async () => { await open(); await typeCep("010010009"); await Bun.sleep(300); expect(api.state.requests).toEqual([]); expect(await statusText()).toContain("CEP inválido"); }); }); describe("what the shopper typed", () => { test("overwrite: false leaves a filled field alone", async () => { await open({ overwrite: false }); await page.locator("#rua").pressSequentially("Rua que eu mesmo digitei", { delay: 5 }); await typeCep("01001000"); await page.waitForFunction(() => document.querySelector("#bairro").value !== ""); expect(await value("#rua")).toBe("Rua que eu mesmo digitei"); expect(await value("#bairro")).toBe("Sé"); }, 10000); test("by default a manual correction survives the next CEP", async () => { await open(); await typeCep("01001000"); await page.waitForFunction(() => document.querySelector("#rua").value !== ""); await page.locator("#rua").press("Control+a"); await page.locator("#rua").pressSequentially("Praça da Sé, fundos", { delay: 5 }); await page.locator("#cep").press("Control+a"); await typeCep("20040020"); await page.waitForFunction(() => document.querySelector("#bairro").value === "Centro"); expect(await value("#rua")).toBe("Praça da Sé, fundos"); expect(await value("#cidade")).toBe("Rio de Janeiro"); }, 15000); test("overwrite: true replaces it", async () => { await open({ overwrite: true }); await page.locator("#rua").pressSequentially("Rua qualquer", { delay: 5 }); await typeCep("01001000"); await page.waitForFunction(() => document.querySelector("#rua").value === "Praça da Sé"); expect(await value("#rua")).toBe("Praça da Sé"); }, 10000); }); describe("accessibility", () => { test("the status region announces each state", async () => { api.state.delays["01001000"] = 400; await open(); const region = page.locator(".cepis-autofill-status"); expect(await region.getAttribute("role")).toBe("status"); expect(await region.getAttribute("aria-live")).toBe("polite"); expect(await region.getAttribute("aria-atomic")).toBe("true"); await typeCep("01001000"); await page.waitForFunction(() => document.querySelector(".cepis-autofill-status").textContent.includes("Buscando"), ); expect(await statusText()).toBe("Buscando endereço…"); expect(await page.locator("#cep").getAttribute("aria-busy")).toBe("true"); await page.waitForFunction(() => document.querySelector(".cepis-autofill-status").textContent.includes("encontrado"), ); expect(await statusText()).toBe("Endereço encontrado."); expect(await page.locator("#cep").getAttribute("aria-busy")).toBeNull(); const id = await region.getAttribute("id"); expect(await page.locator("#cep").getAttribute("aria-describedby")).toContain(id); expect(await events()).toEqual(expect.arrayContaining(["status:loading", "status:found"])); }, 10000); test("the region reserves its space before anything is announced", async () => { await open(); const before = await page.locator("#checkout").boundingBox(); await typeCep("01001000"); await page.waitForFunction(() => document.querySelector("#rua").value !== ""); const after = await page.locator("#checkout").boundingBox(); expect(after.height).toBe(before.height); }); });