diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 15:01:02 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 15:01:02 -0300 |
| commit | 36486007ccada8c31d30e8f528d21233cabce32c (patch) | |
| tree | d14a108845e4c984ec922ba98d153c8d68da6ebb /test/browser.test.js | |
| download | autofill-36486007ccada8c31d30e8f528d21233cabce32c.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'test/browser.test.js')
| -rw-r--r-- | test/browser.test.js | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/test/browser.test.js b/test/browser.test.js new file mode 100644 index 0000000..634df0d --- /dev/null +++ b/test/browser.test.js @@ -0,0 +1,336 @@ +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); + }); +}); |