summaryrefslogtreecommitdiffstats
path: root/test/browser.test.js
blob: 634df0de7b2b8ae091591741a109250384c8f5a2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
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);
  });
});