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 | |
| download | woocommerce-29190494c82855f0f0ce7fb16f831c8f971022c5.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'test')
| -rw-r--r-- | test/checkout.test.js | 598 | ||||
| -rw-r--r-- | test/compose.yml | 61 | ||||
| -rw-r--r-- | test/fake-api.js | 104 | ||||
| -rwxr-xr-x | test/setup.sh | 158 | ||||
| -rw-r--r-- | test/wp.js | 88 |
5 files changed, 1009 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([]); + }); +}); diff --git a/test/compose.yml b/test/compose.yml new file mode 100644 index 0000000..ff0cbdd --- /dev/null +++ b/test/compose.yml @@ -0,0 +1,61 @@ +name: cepis-woocommerce-test + +services: + db: + image: mysql:8 + environment: + MYSQL_ROOT_PASSWORD: wordpress + MYSQL_DATABASE: wordpress + MYSQL_USER: wordpress + MYSQL_PASSWORD: wordpress + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uwordpress", "-pwordpress"] + interval: 3s + timeout: 5s + retries: 30 + + wordpress: + image: wordpress:latest + depends_on: + db: + condition: service_healthy + ports: + - "8090:80" + extra_hosts: + - "fakecep:host-gateway" + environment: + WORDPRESS_DB_HOST: db:3306 + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: wordpress + WORDPRESS_DB_NAME: wordpress + WORDPRESS_DEBUG: "1" + WORDPRESS_CONFIG_EXTRA: | + define( 'WP_DEBUG', true ); + define( 'WP_DEBUG_LOG', true ); + define( 'WP_DEBUG_DISPLAY', false ); + define( 'SCRIPT_DEBUG', true ); + define( 'DISABLE_WP_CRON', true ); + volumes: + - wp:/var/www/html + - ..:/var/www/html/wp-content/plugins/cepis-woocommerce + + cli: + image: wordpress:cli + depends_on: + - wordpress + user: "33:33" + extra_hosts: + - "fakecep:host-gateway" + environment: + WORDPRESS_DB_HOST: db:3306 + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: wordpress + WORDPRESS_DB_NAME: wordpress + WP_CLI_CACHE_DIR: /tmp/wp-cli-cache + volumes: + - wp:/var/www/html + - ..:/var/www/html/wp-content/plugins/cepis-woocommerce + entrypoint: ["tail", "-f", "/dev/null"] + +volumes: + wp: diff --git a/test/fake-api.js b/test/fake-api.js new file mode 100644 index 0000000..4b6ac39 --- /dev/null +++ b/test/fake-api.js @@ -0,0 +1,104 @@ +/** + * A fake cep.is, so the test suite never touches the network. + * + * Same routes, bodies and status codes as the real API, plus a request log the + * tests assert on. It listens on every interface because two different clients + * have to reach it: the browser on the host, and PHP inside the WordPress + * container through the `fakecep` host alias. + * + * @module + */ + +const ADDRESSES = { + "01001000": { + cep: "01001000", + logradouro: "Praça da Sé", + complemento: "lado ímpar", + unidade: "", + bairro: "Sé", + localidade: "São Paulo", + uf: "SP", + estado: "São Paulo", + regiao: "Sudeste", + ibge: "3550308", + gia: "1004", + ddd: "11", + siafi: "7107", + geo: { lat: -23.567202, lon: -46.649659, raio_m: 1084, precisao: "logradouro" }, + }, + 20040020: { + cep: "20040020", + logradouro: "Avenida Rio Branco", + complemento: "até 0999 - lado ímpar", + unidade: "", + bairro: "Centro", + localidade: "Rio de Janeiro", + uf: "RJ", + estado: "Rio de Janeiro", + regiao: "Sudeste", + ibge: "3304557", + gia: "", + ddd: "21", + siafi: "6001", + }, +}; + +const HEADERS = { + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json; charset=utf-8", +}; + +/** + * Start the fake API. + * + * `state.requests` collects `{cep, userAgent}` for every lookup, which is how + * the proxy tests prove the request came from PHP rather than the browser. + * + * @param {number} port Port to listen on. + * @returns {{port: number, state: {requests: Array<{cep: string, userAgent: string}>}, reset: () => void, stop: () => void}} + */ +export function startFakeApi(port) { + const state = { requests: [] }; + + const server = Bun.serve({ + port, + hostname: "0.0.0.0", + fetch(request) { + const cep = new URL(request.url).pathname.slice(1); + state.requests.push({ cep, userAgent: request.headers.get("user-agent") || "" }); + + if (!/^\d{8}$/.test(cep)) { + return new Response( + JSON.stringify({ erro: "cep_invalido", mensagem: "CEP deve ter 8 dígitos" }), + { status: 400, headers: HEADERS }, + ); + } + + const address = ADDRESSES[cep]; + if (!address) { + return new Response( + JSON.stringify({ erro: "cep_nao_encontrado", mensagem: "CEP não existe na base do DNE" }), + { status: 404, headers: HEADERS }, + ); + } + + return new Response(JSON.stringify(address), { status: 200, headers: HEADERS }); + }, + }); + + return { + port: server.port, + state, + reset() { + state.requests.length = 0; + }, + stop() { + server.stop(true); + }, + }; +} + +if (import.meta.main) { + const api = startFakeApi(Number(process.argv[2] || 8899)); + console.log(`fake cep.is on :${api.port}`); +} diff --git a/test/setup.sh b/test/setup.sh new file mode 100755 index 0000000..6925386 --- /dev/null +++ b/test/setup.sh @@ -0,0 +1,158 @@ +#!/bin/sh +# +# Stand up the WordPress + WooCommerce store the browser tests drive. +# +# Idempotent: running it again on a live stack reconfigures rather than +# rebuilds. `make clean` is what throws the volumes away. + +set -eu + +cd "$(dirname "$0")" + +SITE="http://127.0.0.1:8090" + +wp() { + docker compose exec -T cli wp "$@" +} + +printf 'bringing the stack up\n' +docker compose up -d >/dev/null + +printf 'waiting for the web server\n' +i=0 +while [ "$i" -lt 90 ]; do + code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 "$SITE/" 2>/dev/null || echo 000) + [ "$code" != "000" ] && break + i=$((i + 1)) + sleep 2 +done +[ "$code" = "000" ] && { printf 'web server never answered\n' >&2; exit 1; } + +if ! wp core is-installed >/dev/null 2>&1; then + printf 'installing WordPress\n' + wp core install \ + --url="$SITE" \ + --title="CEP Test Store" \ + --admin_user=admin \ + --admin_password=admin \ + --skip-email >/dev/null +fi + +wp rewrite structure '/%postname%/' >/dev/null 2>&1 +wp rewrite flush >/dev/null 2>&1 + +if ! wp plugin is-installed woocommerce >/dev/null 2>&1; then + printf 'installing WooCommerce\n' + wp plugin install woocommerce >/dev/null +fi +wp plugin activate woocommerce >/dev/null + +printf 'configuring the store\n' +wp option update woocommerce_store_address "Praça da Sé" >/dev/null +wp option update woocommerce_store_city "São Paulo" >/dev/null +wp option update woocommerce_default_country "BR:SP" >/dev/null +wp option update woocommerce_store_postcode "01001000" >/dev/null +wp option update woocommerce_currency BRL >/dev/null +wp option update woocommerce_allowed_countries all >/dev/null +wp option update woocommerce_enable_guest_checkout yes >/dev/null +wp option update woocommerce_enable_checkout_login_reminder no >/dev/null +wp option update woocommerce_calc_taxes no >/dev/null +wp option update woocommerce_cod_settings \ + '{"enabled":"yes","title":"Pagamento na entrega","description":"","instructions":""}' \ + --format=json >/dev/null +wp eval 'if ( ! WC_Shipping_Zones::get_zone( 0 )->get_shipping_methods() ) { WC_Shipping_Zones::get_zone( 0 )->add_shipping_method( "flat_rate" ); }' >/dev/null + +wp eval ' +$id = (int) get_option( "cepis_test_product", 0 ); +if ( ! $id || ! wc_get_product( $id ) ) { + $product = new WC_Product_Simple(); + $product->set_name( "Caneca" ); + $product->set_regular_price( "25" ); + $product->set_virtual( false ); + $id = $product->save(); + update_option( "cepis_test_product", $id ); +} +' >/dev/null + +wp eval ' +$blocks = 0; +foreach ( get_pages( array( "post_status" => "publish" ) ) as $page ) { + if ( has_block( "woocommerce/checkout", $page ) ) { + $blocks = (int) $page->ID; + break; + } +} +if ( ! $blocks ) { + WP_CLI::error( "no page holds the woocommerce/checkout block" ); +} +update_option( "cepis_test_blocks_page", $blocks ); +update_option( "woocommerce_checkout_page_id", $blocks ); + +$classic = get_page_by_path( "checkout-classico" ); +if ( ! $classic ) { + $id = wp_insert_post( array( + "post_title" => "Checkout classico", + "post_name" => "checkout-classico", + "post_content" => "[woocommerce_checkout]", + "post_status" => "publish", + "post_type" => "page", + ) ); +} else { + $id = (int) $classic->ID; +} +update_option( "cepis_test_classic_page", $id ); +' >/dev/null + +printf 'installing the fixture that imitates Brazilian Market on WooCommerce\n' +docker compose exec -T cli sh -c 'mkdir -p /var/www/html/wp-content/mu-plugins && cat > /var/www/html/wp-content/mu-plugins/cepis-test-br-fields.php' <<'PHP' +<?php +/** + * Adds the house-number and neighbourhood fields that "Brazilian Market on + * WooCommerce" adds, so the plugin can be tested with and without them. + * Switched by the `cepis_test_br_fields` option. + */ + +function cepis_test_br_fields( $fields, $prefix ) { + if ( 'yes' !== get_option( 'cepis_test_br_fields' ) ) { + return $fields; + } + + $fields[ $prefix . '_number' ] = array( + 'label' => 'Número', + 'required' => false, + 'class' => array( 'form-row-first', 'address-field' ), + 'priority' => 51, + ); + $fields[ $prefix . '_neighborhood' ] = array( + 'label' => 'Bairro', + 'required' => false, + 'class' => array( 'form-row-last', 'address-field' ), + 'priority' => 52, + ); + + return $fields; +} + +add_filter( 'woocommerce_billing_fields', function ( $fields ) { + return cepis_test_br_fields( $fields, 'billing' ); +} ); + +add_filter( 'woocommerce_shipping_fields', function ( $fields ) { + return cepis_test_br_fields( $fields, 'shipping' ); +} ); + +/** + * Force the site locale from an option, so the shipped translation can be + * exercised without downloading WordPress's own language pack. + */ +add_filter( 'locale', function ( $locale ) { + $forced = get_option( 'cepis_test_locale' ); + + return $forced ? $forced : $locale; +} ); +PHP + +wp plugin activate cepis-woocommerce >/dev/null + +printf 'ready: %s\n' "$SITE" diff --git a/test/wp.js b/test/wp.js new file mode 100644 index 0000000..ab2d2a7 --- /dev/null +++ b/test/wp.js @@ -0,0 +1,88 @@ +/** + * Talking to the WordPress under test. + * + * Everything goes through WP-CLI in the `cli` container of `compose.yml`, so + * the tests configure the store exactly the way a store owner would. + * + * @module + */ + +import { spawnSync } from "node:child_process"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); + +/** Where the site answers, from the host. */ +export const SITE = "http://127.0.0.1:8090"; + +/** Hostname the fake API is reached by, from both the browser and PHP. */ +export const FAKE_HOST = "fakecep"; + +/** Port the fake API listens on. */ +export const FAKE_PORT = 8899; + +/** Base URL of the fake API, as both sides must spell it. */ +export const FAKE_URL = `http://${FAKE_HOST}:${FAKE_PORT}`; + +/** + * Run a WP-CLI command in the container. + * + * @param {string[]} args Arguments after `wp`. + * @returns {string} Trimmed stdout. + */ +export function wp(args) { + const result = spawnSync("docker", ["compose", "exec", "-T", "cli", "wp", ...args], { + cwd: HERE, + encoding: "utf8", + }); + + if (result.status !== 0) { + throw new Error(`wp ${args.join(" ")} failed:\n${result.stdout}\n${result.stderr}`); + } + + return String(result.stdout).trim(); +} + +/** + * Run PHP inside WordPress. + * + * @param {string} code PHP to evaluate. + * @returns {string} Trimmed stdout. + */ +export function wpEval(code) { + return wp(["eval", code]); +} + +/** + * Set one option. + * + * @param {string} name Option name. + * @param {string} value Option value. + */ +export function setOption(name, value) { + wp(["option", "update", name, value]); +} + +/** + * Read the WordPress debug log and clear it. + * + * @returns {string} Whatever had been logged. + */ +export function drainDebugLog() { + const result = spawnSync( + "docker", + [ + "compose", + "exec", + "-T", + "cli", + "sh", + "-c", + "cat /var/www/html/wp-content/debug.log 2>/dev/null; : > /var/www/html/wp-content/debug.log 2>/dev/null || true", + ], + { cwd: HERE, encoding: "utf8" }, + ); + + return String(result.stdout || "").trim(); +} |