diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 16:41:48 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 16:47:14 -0300 |
| commit | 535dbda6b48171f8115b455e54790de94d97d42a (patch) | |
| tree | 5c0410011e61a883bdf063ee612875a637fb1d22 /test/checkout.test.js | |
| download | woocommerce-29190494c82855f0f0ce7fb16f831c8f971022c5.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'test/checkout.test.js')
| -rw-r--r-- | test/checkout.test.js | 598 |
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([]); + }); +}); |