summaryrefslogtreecommitdiffstats
path: root/test/checkout.test.js
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 16:41:48 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 16:47:14 -0300
commit535dbda6b48171f8115b455e54790de94d97d42a (patch)
tree5c0410011e61a883bdf063ee612875a637fb1d22 /test/checkout.test.js
downloadwoocommerce-0.1.0.tar.gz
Initial importv0.1.0
Diffstat (limited to 'test/checkout.test.js')
-rw-r--r--test/checkout.test.js598
1 files changed, 598 insertions, 0 deletions
diff --git a/test/checkout.test.js b/test/checkout.test.js
new file mode 100644
index 0000000..074b13a
--- /dev/null
+++ b/test/checkout.test.js
@@ -0,0 +1,598 @@
+/**
+ * The plugin driven through a real browser against a real WooCommerce.
+ *
+ * Nothing here is stubbed except the API itself: WordPress, WooCommerce, the
+ * theme, the block checkout and its data store all run for real in the
+ * containers `compose.yml` describes, and Chromium drives them. The one fake is
+ * `fake-api.js` standing in for cep.is, so a test never depends on the network.
+ *
+ * Chromium is told to resolve `fakecep` to the loopback address; the WordPress
+ * container resolves the same name to the host gateway. Both sides therefore
+ * spell the API's address the same way, which is what lets one `base_url`
+ * setting serve the direct and the proxy mode.
+ *
+ * @module
+ */
+
+import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
+import { readdirSync } from "node:fs";
+import { chromium } from "playwright-core";
+import { startFakeApi } from "./fake-api.js";
+import { FAKE_PORT, FAKE_URL, SITE, drainDebugLog, setOption, wp, wpEval } from "./wp.js";
+
+const PRODUCT = wp(["option", "get", "cepis_test_product"]);
+const BLOCKS_PAGE = wp(["option", "get", "cepis_test_blocks_page"]);
+const CLASSIC_PAGE = wp(["option", "get", "cepis_test_classic_page"]);
+
+let browser;
+let api;
+
+/**
+ * Locate the Chromium that Playwright installed.
+ *
+ * @returns {string} Path to the executable.
+ */
+function chromiumPath() {
+ if (process.env.CEPIS_CHROMIUM) {
+ return process.env.CEPIS_CHROMIUM;
+ }
+
+ const root = `${process.env.HOME}/.cache/ms-playwright`;
+ const build = readdirSync(root)
+ .filter((name) => name.startsWith("chromium-"))
+ .sort()
+ .pop();
+
+ return `${root}/${build}/chrome-linux64/chrome`;
+}
+
+beforeAll(async () => {
+ api = startFakeApi(FAKE_PORT);
+ browser = await chromium.launch({
+ executablePath: chromiumPath(),
+ args: ["--no-sandbox", "--host-resolver-rules=MAP fakecep 127.0.0.1"],
+ });
+
+ setOption("cepis_woo_enabled", "yes");
+ setOption("cepis_woo_base_url", FAKE_URL);
+ setOption("cepis_woo_mode", "direct");
+ setOption("cepis_woo_overwrite", "auto");
+ setOption("cepis_woo_focus_number", "yes");
+ setOption("cepis_test_br_fields", "no");
+ drainDebugLog();
+});
+
+afterAll(async () => {
+ setOption("woocommerce_checkout_page_id", BLOCKS_PAGE);
+ await browser?.close();
+ api?.stop();
+});
+
+beforeEach(() => {
+ api.reset();
+});
+
+/**
+ * Open a checkout with one product in the cart, collecting what the page asks
+ * the network for.
+ *
+ * @param {string} path Path of the checkout page.
+ * @returns {Promise<{page: import("playwright-core").Page, requests: Array<{url: string, body: string}>, close: () => Promise<void>}>}
+ */
+async function checkout(path) {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+ const requests = [];
+ const errors = [];
+
+ page.on("request", (request) => {
+ requests.push({ url: request.url(), body: request.postData() || "" });
+ });
+ page.on("pageerror", (error) => errors.push(String(error)));
+
+ await page.goto(`${SITE}/?add-to-cart=${PRODUCT}`, { waitUntil: "domcontentloaded" });
+ await page.goto(`${SITE}${path}`, { waitUntil: "domcontentloaded" });
+
+ return {
+ page,
+ requests,
+ errors,
+ async close() {
+ await context.close();
+ },
+ };
+}
+
+/**
+ * Wait until the Checkout block has hydrated and stopped talking to the Store
+ * API.
+ *
+ * The block seeds its address fields from the cart the server returns, and
+ * typing while that is in flight loses characters — the response resets the
+ * controlled input. That is WooCommerce's own behaviour on a freshly opened
+ * checkout, not something this plugin causes, so the test waits it out.
+ *
+ * @param {import("playwright-core").Page} page Page showing the block checkout.
+ */
+async function settled(page) {
+ await page.waitForSelector("#shipping-postcode");
+ await page.waitForSelector(".wc-block-checkout:not(.is-loading)", { timeout: 30000 });
+ await page.waitForFunction(
+ () => {
+ const store = window.wp && window.wp.data && window.wp.data.select("wc/store/cart");
+ return !!(store && store.getCustomerData && store.getCustomerData().shippingAddress);
+ },
+ undefined,
+ { timeout: 30000 },
+ );
+ await page.waitForLoadState("networkidle");
+ await page.waitForTimeout(500);
+}
+
+/**
+ * Wait until the classic checkout has stopped moving.
+ *
+ * WooCommerce announces "added to your cart" in a banner that takes focus for
+ * screen-reader users a moment after the page loads. Typing before that has
+ * happened means the banner steals focus mid-CEP, which a real shopper never
+ * experiences — they are already looking at the form by then.
+ *
+ * @param {import("playwright-core").Page} page Page showing the classic checkout.
+ */
+async function ready(page) {
+ await page.waitForSelector("#billing_postcode");
+ await page.waitForLoadState("networkidle");
+ await page.waitForTimeout(500);
+}
+
+/**
+ * Type a CEP the way a shopper does.
+ *
+ * The block checkout's inputs are React-controlled and setting `value` in one
+ * go is not reliably seen; real key events are, and they exercise the widget's
+ * debounce as well.
+ *
+ * @param {import("playwright-core").Page} page Page under test.
+ * @param {string} selector CEP field.
+ * @param {string} cep Eight digits.
+ */
+async function typeCep(page, selector, cep) {
+ await page.click(selector);
+ await page.keyboard.press("ControlOrMeta+A");
+ await page.locator(selector).pressSequentially(cep, { delay: 40 });
+}
+
+describe("plugin", () => {
+ test("is active", () => {
+ expect(wp(["plugin", "get", "cepis-woocommerce", "--field=status"])).toBe("active");
+ });
+
+ test("declares the version WooCommerce reports", () => {
+ expect(wp(["plugin", "get", "cepis-woocommerce", "--field=version"])).toBe("0.1.0");
+ });
+});
+
+describe("block checkout", () => {
+ beforeAll(() => {
+ setOption("woocommerce_checkout_page_id", BLOCKS_PAGE);
+ setOption("cepis_woo_mode", "direct");
+ });
+
+ test("fills the address and WooCommerce sees the state change", async () => {
+ const { page, requests, errors, close } = await checkout("/checkout/");
+
+ await settled(page);
+ await typeCep(page, "#shipping-postcode", "01001000");
+
+ await page.waitForFunction(
+ () => document.querySelector("#shipping-address_1").value !== "",
+ undefined,
+ { timeout: 15000 },
+ );
+
+ const filled = await page.evaluate(() => ({
+ street: document.querySelector("#shipping-address_1").value,
+ city: document.querySelector("#shipping-city").value,
+ state: document.querySelector("#shipping-state").value,
+ stateTag: document.querySelector("#shipping-state").tagName,
+ store: window.wp.data.select("wc/store/cart").getCustomerData().shippingAddress,
+ }));
+
+ expect(filled.street).toBe("Praça da Sé");
+ expect(filled.city).toBe("São Paulo");
+ expect(filled.stateTag).toBe("SELECT");
+ expect(filled.state).toBe("SP");
+ expect(filled.store.state).toBe("SP");
+ expect(filled.store.city).toBe("São Paulo");
+
+ await page.waitForFunction(
+ () =>
+ performance
+ .getEntriesByType("resource")
+ .some((entry) => entry.name.includes("/wc/store/v1/")),
+ undefined,
+ { timeout: 15000 },
+ );
+
+ const pushed = requests.filter(
+ (request) => request.url.includes("/wc/store/v1/") && request.body.includes('"state":"SP"'),
+ );
+ expect(pushed.length).toBeGreaterThan(0);
+ expect(pushed.some((request) => request.body.includes("São Paulo"))).toBe(true);
+
+ expect(api.state.requests.filter((entry) => entry.cep === "01001000")).toHaveLength(1);
+ expect(errors).toEqual([]);
+
+ await close();
+ }, 90000);
+
+ test("replaces the state WooCommerce pre-selected", async () => {
+ const { page, close } = await checkout("/checkout/");
+
+ await settled(page);
+ expect(await page.inputValue("#shipping-state")).toBe("SP");
+
+ await typeCep(page, "#shipping-postcode", "20040020");
+ await page.waitForFunction(
+ () => document.querySelector("#shipping-state").value === "RJ",
+ undefined,
+ { timeout: 15000 },
+ );
+
+ const filled = await page.evaluate(() => ({
+ street: document.querySelector("#shipping-address_1").value,
+ city: document.querySelector("#shipping-city").value,
+ store: window.wp.data.select("wc/store/cart").getCustomerData().shippingAddress,
+ }));
+
+ expect(filled.street).toBe("Avenida Rio Branco");
+ expect(filled.city).toBe("Rio de Janeiro");
+ expect(filled.store.state).toBe("RJ");
+
+ await close();
+ }, 90000);
+
+ test("a CEP that does not exist shows a message and clears nothing", async () => {
+ const { page, close } = await checkout("/checkout/");
+
+ await settled(page);
+ await typeCep(page, "#shipping-postcode", "01001000");
+ await page.waitForFunction(
+ () => document.querySelector("#shipping-address_1").value !== "",
+ undefined,
+ { timeout: 15000 },
+ );
+
+ await typeCep(page, "#shipping-postcode", "99999999");
+ await page.waitForFunction(() => document.body.innerText.includes("CEP not found"), undefined, {
+ timeout: 15000,
+ });
+
+ const after = await page.evaluate(() => ({
+ street: document.querySelector("#shipping-address_1").value,
+ city: document.querySelector("#shipping-city").value,
+ state: document.querySelector("#shipping-state").value,
+ }));
+
+ expect(after.street).toBe("Praça da Sé");
+ expect(after.city).toBe("São Paulo");
+ expect(after.state).toBe("SP");
+
+ await close();
+ }, 90000);
+});
+
+describe("classic checkout", () => {
+ beforeAll(() => {
+ setOption("woocommerce_checkout_page_id", CLASSIC_PAGE);
+ setOption("cepis_woo_mode", "direct");
+ });
+
+ test("fills the address and WooCommerce recalculates with the new state", async () => {
+ setOption("cepis_test_br_fields", "no");
+ const { page, requests, errors, close } = await checkout("/checkout-classico/");
+
+ await ready(page);
+ await typeCep(page, "#billing_postcode", "01001000");
+
+ await page.waitForFunction(
+ () => document.querySelector("#billing_address_1").value !== "",
+ undefined,
+ { timeout: 15000 },
+ );
+
+ const filled = await page.evaluate(() => ({
+ street: document.querySelector("#billing_address_1").value,
+ city: document.querySelector("#billing_city").value,
+ state: document.querySelector("#billing_state").value,
+ stateTag: document.querySelector("#billing_state").tagName,
+ shown: (document.querySelector("#select2-billing_state-container") || {}).textContent || "",
+ status: (document.querySelector(".cepis-autofill-status") || {}).textContent || "",
+ }));
+
+ expect(filled.street).toBe("Praça da Sé");
+ expect(filled.city).toBe("São Paulo");
+ expect(filled.stateTag).toBe("SELECT");
+ expect(filled.state).toBe("SP");
+ expect(filled.shown).toContain("São Paulo");
+ expect(filled.status).toBe("Address found.");
+
+ await page.waitForFunction(
+ () => window.jQuery && window.jQuery("form.checkout").length > 0,
+ undefined,
+ { timeout: 10000 },
+ );
+ await page.waitForTimeout(3000);
+
+ const reviews = requests.filter(
+ (request) =>
+ request.url.includes("wc-ajax=update_order_review") &&
+ decodeURIComponent(request.body).includes("billing_state=SP"),
+ );
+ expect(reviews.length).toBeGreaterThan(0);
+
+ expect(errors).toEqual([]);
+ await close();
+ }, 90000);
+
+ test("fills neighbourhood and moves to the number when the store has those fields", async () => {
+ setOption("cepis_test_br_fields", "yes");
+ const { page, close } = await checkout("/checkout-classico/");
+
+ await ready(page);
+ await page.waitForSelector("#billing_neighborhood");
+ await typeCep(page, "#billing_postcode", "01001000");
+
+ await page.waitForFunction(
+ () => document.querySelector("#billing_neighborhood").value !== "",
+ undefined,
+ { timeout: 15000 },
+ );
+
+ const filled = await page.evaluate(() => ({
+ neighborhood: document.querySelector("#billing_neighborhood").value,
+ number: document.querySelector("#billing_number").value,
+ focused: document.activeElement.id,
+ }));
+
+ expect(filled.neighborhood).toBe("Sé");
+ expect(filled.number).toBe("");
+ expect(filled.focused).toBe("billing_number");
+
+ setOption("cepis_test_br_fields", "no");
+ await close();
+ }, 90000);
+
+ test("replaces the state WooCommerce pre-selected, and works without the Brazilian fields", async () => {
+ setOption("cepis_test_br_fields", "no");
+ const { page, requests, errors, close } = await checkout("/checkout-classico/");
+
+ await ready(page);
+ expect(await page.inputValue("#billing_state")).toBe("SP");
+
+ await typeCep(page, "#billing_postcode", "20040020");
+
+ await page.waitForFunction(
+ () => document.querySelector("#billing_state").value === "RJ",
+ undefined,
+ { timeout: 15000 },
+ );
+
+ const filled = await page.evaluate(() => ({
+ street: document.querySelector("#billing_address_1").value,
+ city: document.querySelector("#billing_city").value,
+ state: document.querySelector("#billing_state").value,
+ shown: (document.querySelector("#select2-billing_state-container") || {}).textContent || "",
+ address2: document.querySelector("#billing_address_2").value,
+ hasNeighborhood: !!document.querySelector("#billing_neighborhood"),
+ hasNumber: !!document.querySelector("#billing_number"),
+ }));
+
+ expect(filled.hasNeighborhood).toBe(false);
+ expect(filled.hasNumber).toBe(false);
+ expect(filled.street).toBe("Avenida Rio Branco");
+ expect(filled.city).toBe("Rio de Janeiro");
+ expect(filled.state).toBe("RJ");
+ expect(filled.shown).toContain("Rio de Janeiro");
+ expect(filled.address2).toBe("");
+
+ await page.waitForTimeout(3000);
+ const reviews = requests.filter(
+ (request) =>
+ request.url.includes("wc-ajax=update_order_review") &&
+ decodeURIComponent(request.body).includes("billing_state=RJ"),
+ );
+ expect(reviews.length).toBeGreaterThan(0);
+
+ expect(errors).toEqual([]);
+
+ await close();
+ }, 90000);
+
+ test("a CEP that does not exist shows a message and clears nothing", async () => {
+ const { page, close } = await checkout("/checkout-classico/");
+
+ await ready(page);
+ await typeCep(page, "#billing_postcode", "01001000");
+ await page.waitForFunction(
+ () => document.querySelector("#billing_address_1").value !== "",
+ undefined,
+ { timeout: 15000 },
+ );
+
+ await typeCep(page, "#billing_postcode", "99999999");
+ await page.waitForFunction(
+ () =>
+ (document.querySelector(".cepis-autofill-status") || {}).textContent ===
+ "CEP not found. Check the number or fill the address in by hand.",
+ undefined,
+ { timeout: 15000 },
+ );
+
+ const after = await page.evaluate(() => ({
+ street: document.querySelector("#billing_address_1").value,
+ city: document.querySelector("#billing_city").value,
+ state: document.querySelector("#billing_state").value,
+ invalid: document.querySelector("#billing_postcode").getAttribute("aria-invalid"),
+ }));
+
+ expect(after.street).toBe("Praça da Sé");
+ expect(after.city).toBe("São Paulo");
+ expect(after.state).toBe("SP");
+ expect(after.invalid).toBe("true");
+
+ await close();
+ }, 90000);
+});
+
+describe("proxy mode", () => {
+ beforeAll(() => {
+ setOption("woocommerce_checkout_page_id", CLASSIC_PAGE);
+ setOption("cepis_woo_mode", "proxy");
+ wpEval(
+ "global $wpdb; $wpdb->query( \"DELETE FROM {$wpdb->options} WHERE option_name LIKE '%_transient_%cepis_woo_%'\" );",
+ );
+ });
+
+ afterAll(() => {
+ setOption("cepis_woo_mode", "direct");
+ });
+
+ test("the browser talks to this site and PHP talks to the API", async () => {
+ const { page, requests, errors, close } = await checkout("/checkout-classico/");
+
+ await ready(page);
+ await typeCep(page, "#billing_postcode", "01001000");
+
+ await page.waitForFunction(
+ () => document.querySelector("#billing_address_1").value !== "",
+ undefined,
+ { timeout: 20000 },
+ );
+
+ expect(await page.inputValue("#billing_address_1")).toBe("Praça da Sé");
+ expect(await page.inputValue("#billing_city")).toBe("São Paulo");
+ expect(await page.inputValue("#billing_state")).toBe("SP");
+
+ const proxied = requests.filter((request) =>
+ /\/cepis\/v1\/lookup\/[A-Za-z0-9]+\/01001000/.test(request.url),
+ );
+ expect(proxied.length).toBeGreaterThan(0);
+ expect(requests.some((request) => request.url.includes("fakecep"))).toBe(false);
+
+ const served = api.state.requests.filter((entry) => entry.cep === "01001000");
+ expect(served.length).toBe(1);
+ expect(served[0].userAgent).toContain("cepis-woocommerce/0.1.0");
+
+ expect(errors).toEqual([]);
+ await close();
+ }, 90000);
+
+ test("the answer is cached, so a second shopper costs no request", async () => {
+ api.reset();
+ const first = await checkout("/checkout-classico/");
+ await ready(first.page);
+ await typeCep(first.page, "#billing_postcode", "20040020");
+ await first.page.waitForFunction(
+ () => document.querySelector("#billing_address_1").value !== "",
+ undefined,
+ { timeout: 20000 },
+ );
+ await first.close();
+
+ expect(api.state.requests.filter((entry) => entry.cep === "20040020").length).toBe(1);
+
+ const second = await checkout("/checkout-classico/");
+ await ready(second.page);
+ await typeCep(second.page, "#billing_postcode", "20040020");
+ await second.page.waitForFunction(
+ () => document.querySelector("#billing_address_1").value !== "",
+ undefined,
+ { timeout: 20000 },
+ );
+ expect(await second.page.inputValue("#billing_city")).toBe("Rio de Janeiro");
+ await second.close();
+
+ expect(api.state.requests.filter((entry) => entry.cep === "20040020").length).toBe(1);
+ }, 120000);
+
+ test("the REST route refuses a request without a valid token", async () => {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ const withoutToken = await page.evaluate(async (site) => {
+ const response = await fetch(`${site}/wp-json/cepis/v1/lookup/01001000`);
+ return response.status;
+ }, SITE);
+
+ const wrongToken = await page.evaluate(async (site) => {
+ const response = await fetch(`${site}/wp-json/cepis/v1/lookup/deadbeef00/01001000`);
+ return { status: response.status, body: await response.text() };
+ }, SITE);
+
+ expect(withoutToken).toBe(404);
+ expect(wrongToken.status).toBe(403);
+ expect(wrongToken.body).toContain("cepis_invalid_token");
+
+ await context.close();
+ }, 60000);
+});
+
+describe("translations", () => {
+ beforeAll(() => {
+ setOption("woocommerce_checkout_page_id", CLASSIC_PAGE);
+ setOption("cepis_test_locale", "pt_BR");
+ });
+
+ afterAll(() => {
+ setOption("cepis_test_locale", "");
+ });
+
+ test("a store in Portuguese gets the shipped Portuguese messages", async () => {
+ const { page, close } = await checkout("/checkout-classico/");
+
+ await ready(page);
+ await typeCep(page, "#billing_postcode", "99999999");
+
+ await page.waitForFunction(
+ () =>
+ (document.querySelector(".cepis-autofill-status") || {}).textContent ===
+ "CEP não encontrado. Confira o número ou preencha o endereço manualmente.",
+ undefined,
+ { timeout: 15000 },
+ );
+
+ await close();
+ }, 90000);
+});
+
+describe("housekeeping", () => {
+ test("uninstalling removes every option and cached answer", () => {
+ wpEval(
+ 'define( "WP_UNINSTALL_PLUGIN", "cepis-woocommerce/cepis-woocommerce.php" );' +
+ 'include WP_PLUGIN_DIR . "/cepis-woocommerce/uninstall.php";',
+ );
+
+ const left = wpEval(
+ "global $wpdb;" +
+ "echo (int) $wpdb->get_var( \"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'cepis_woo_%' OR option_name LIKE '%transient%cepis_woo_%'\" );",
+ );
+
+ expect(left).toBe("0");
+
+ setOption("cepis_woo_enabled", "yes");
+ setOption("cepis_woo_base_url", FAKE_URL);
+ setOption("cepis_woo_mode", "direct");
+ });
+
+ test("WP_DEBUG logged nothing while all of that happened", () => {
+ const log = drainDebugLog();
+ const ours = log
+ .split("\n")
+ .filter((line) => line.trim() !== "")
+ .filter((line) => !line.includes("/wp-content/plugins/woocommerce/"))
+ .filter((line) => !line.includes("/wp-includes/"));
+
+ expect(ours).toEqual([]);
+ });
+});