summaryrefslogtreecommitdiffstats
path: root/test/browser.test.js
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 15:01:02 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 15:01:02 -0300
commit36486007ccada8c31d30e8f528d21233cabce32c (patch)
treed14a108845e4c984ec922ba98d153c8d68da6ebb /test/browser.test.js
downloadautofill-0.1.0.tar.gz
Initial importv0.1.0
Diffstat (limited to 'test/browser.test.js')
-rw-r--r--test/browser.test.js336
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);
+ });
+});