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); } function lastError() { return page.evaluate(() => window.__error); } /** Every field the fixture maps, so a test can prove none of them was written. */ async function filledFields() { const filled = {}; for (const selector of ["#rua", "#bairro", "#cidade", "#uf", "#numero"]) { const current = await value(selector); if (current !== "") { filled[selector] = current; } } return filled; } 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()).toBe( "CEP não encontrado. Confira o número ou preencha o endereço manualmente.", ); expect(await filledFields()).toEqual({}); 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("the municipality behind a CEP that does not exist", () => { async function lookFor(cep) { await open(); await typeCep(cep); await page.waitForFunction(() => document.querySelector(".cepis-autofill-status").textContent.includes("não encontrado"), ); } test("the message names the city and where its CEPs start and end", async () => { await lookFor("25845000"); expect(await statusText()).toBe( "CEP não encontrado. Esse número seria de Areal/RJ, " + "que tem CEPs de 25845-001 a 25849-899.", ); expect(await events()).toContain("error:not_found"); }); test("naming the city fills nothing — it is a failure, not an address", async () => { await lookFor("25845000"); expect(await filledFields()).toEqual({}); expect(await events()).not.toContain("result:25845000"); expect(await page.locator("#cep").getAttribute("aria-invalid")).toBe("true"); }); test("the faixa reaches onError, whole", async () => { await lookFor("25845000"); const error = await lastError(); expect(error.code).toBe("not_found"); expect(error.faixa).toEqual({ localidade: "Areal", uf: "RJ", estado: "Rio de Janeiro", regiao: "Sudeste", ibge: "3300225", cep_ini: "25845001", cep_fim: "25849899", ceps: 118, }); }); test("a CEP in no municipality keeps the plain message", async () => { await lookFor("99999999"); expect(await statusText()).toBe( "CEP não encontrado. Confira o número ou preencha o endereço manualmente.", ); expect(await filledFields()).toEqual({}); expect((await lastError()).faixa).toBeUndefined(); }); test("a malformed faixa falls back to the plain message", async () => { await lookFor("25845999"); expect(await statusText()).toBe( "CEP não encontrado. Confira o número ou preencha o endereço manualmente.", ); expect(await filledFields()).toEqual({}); expect((await lastError()).faixa).toBeUndefined(); }); test("the wording is overridable like every other message", async () => { await open({ messages: { not_found_city_range: "Nada aqui. Tente um CEP de {localidade} ({uf})." }, }); await typeCep("25845000"); await page.waitForFunction(() => document.querySelector(".cepis-autofill-status").textContent.includes("Nada aqui"), ); expect(await statusText()).toBe("Nada aqui. Tente um CEP de Areal (RJ)."); expect(await filledFields()).toEqual({}); }); test("lookup() hands the faixa over without touching the form", async () => { await open(); const thrown = await page.evaluate(async (baseUrl) => { try { await window.CepisAutofill.lookup("25845-000", { baseUrl }); return null; } catch (error) { return { code: error.code, message: error.message, localidade: error.faixa.localidade }; } }, api.url); expect(thrown.code).toBe("not_found"); expect(thrown.message).toContain("Areal/RJ"); expect(thrown.localidade).toBe("Areal"); expect(await filledFields()).toEqual({}); }); }); 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); }); });