From a0d91a0ef7ddbbbb5a9fe181d2e0b6f9ca17a217 Mon Sep 17 00:00:00 2001 From: gcch Date: Tue, 23 Dec 2025 16:18:28 +0100 Subject: [PATCH] =?UTF-8?q?tests:=20=C3=A9bauche=20des=20tests=20d'int?= =?UTF-8?q?=C3=A9gration=20via=20Playwright?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/TESTS.md | 4 + playwright.config.ts | 72 ++++++++-------- tests/{ => playwright}/capture.spec.ts | 2 +- tests/playwright/product.spec.ts | 114 +++++++++++++++++++++++++ tests/playwright/shop.spec.ts | 76 +++++++++++++++++ tsconfig.json | 2 +- 6 files changed, 233 insertions(+), 37 deletions(-) create mode 100644 docs/TESTS.md rename tests/{ => playwright}/capture.spec.ts (96%) create mode 100644 tests/playwright/product.spec.ts create mode 100644 tests/playwright/shop.spec.ts diff --git a/docs/TESTS.md b/docs/TESTS.md new file mode 100644 index 00000000..4f26791d --- /dev/null +++ b/docs/TESTS.md @@ -0,0 +1,4 @@ +- Produits + - Aller sur tous les Produits + - La page doit correctement se charger + - Tous les attributs et leurs valeurs doivent être proposés diff --git a/playwright.config.ts b/playwright.config.ts index ae98dd4b..09b103f7 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -21,20 +21,22 @@ export default defineConfig({ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, + // Définis un délai d'exécution. + timeout: 30000, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "list", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://localhost:3000', + baseURL: "https://haikuatelier.gcch.local", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", clientCertificates: [ { origin: "https://haikuatelier.gcch.local", - certPath: "../certs/_wildcard.gcch.local.pem", - keyPath: "../certs/_wildcard.gcch.local-key.pem", + certPath: "./containers/data/certs/_wildcard.gcch.local.pem", + keyPath: "./containers/data/certs/_wildcard.gcch.local-key.pem", }, ], ignoreHTTPSErrors: true, @@ -46,42 +48,42 @@ export default defineConfig({ name: "desktop-chromium-1920", use: { ...devices["Desktop Chrome"], viewport: { width: 1920, height: 1080 } }, }, - { - name: "desktop-chromium-1536", - use: { ...devices["Desktop Chrome"], viewport: { width: 1536, height: 864 } }, - }, - { - name: "desktop-chromium-1366", - use: { ...devices["Desktop Chrome"], viewport: { width: 1366, height: 768 } }, - }, + // { + // name: "desktop-chromium-1536", + // use: { ...devices["Desktop Chrome"], viewport: { width: 1536, height: 864 } }, + // }, + // { + // name: "desktop-chromium-1366", + // use: { ...devices["Desktop Chrome"], viewport: { width: 1366, height: 768 } }, + // }, { name: "desktop-firefox-1920", use: { ...devices["Desktop Firefox"], viewport: { width: 1920, height: 1080 } }, }, - { - name: "desktop-firefox-1536", - use: { ...devices["Desktop Firefox"], viewport: { width: 1536, height: 864 } }, - }, - { - name: "desktop-firefox-1366", - use: { ...devices["Desktop Firefox"], viewport: { width: 1366, height: 768 } }, - }, - { - name: "tablet-chromium-portrait", - use: { ...devices["Galaxy Tab S9"] }, - }, - { - name: "tablet-chromium-landscape", - use: { ...devices["Galaxy Tab S9 landscape"] }, - }, - { - name: "mobile-chromium-portrait", - use: { ...devices["Pixel 7"] }, - }, - { - name: "mobile-chromium-landscape", - use: { ...devices["Pixel 7 landscape"] }, - }, + // { + // name: "desktop-firefox-1536", + // use: { ...devices["Desktop Firefox"], viewport: { width: 1536, height: 864 } }, + // }, + // { + // name: "desktop-firefox-1366", + // use: { ...devices["Desktop Firefox"], viewport: { width: 1366, height: 768 } }, + // }, + // { + // name: "tablet-chromium-portrait", + // use: { ...devices["Galaxy Tab S9"] }, + // }, + // { + // name: "tablet-chromium-landscape", + // use: { ...devices["Galaxy Tab S9 landscape"] }, + // }, + // { + // name: "mobile-chromium-portrait", + // use: { ...devices["Pixel 7"] }, + // }, + // { + // name: "mobile-chromium-landscape", + // use: { ...devices["Pixel 7 landscape"] }, + // }, ], /* Run your local dev server before starting the tests */ // webServer: { diff --git a/tests/capture.spec.ts b/tests/playwright/capture.spec.ts similarity index 96% rename from tests/capture.spec.ts rename to tests/playwright/capture.spec.ts index 0ce1cea2..81e6e89a 100644 --- a/tests/capture.spec.ts +++ b/tests/playwright/capture.spec.ts @@ -38,7 +38,7 @@ Array.from([ url: "https://haikuatelier.gcch.local/product/fuyou-long-earrings-silver/", }, ]).forEach(({ pageName, url }) => { - test(pageName, async ({ page }, testInfo) => { + test.skip(pageName, async ({ page }, testInfo) => { await page.goto(url); const projectName = testInfo.project.name; diff --git a/tests/playwright/product.spec.ts b/tests/playwright/product.spec.ts new file mode 100644 index 00000000..c44c739b --- /dev/null +++ b/tests/playwright/product.spec.ts @@ -0,0 +1,114 @@ +import { test as base, expect, Response } from "@playwright/test"; +import { + WCV3Product, + WCV3Products, +} from "../../web/app/themes/haiku-atelier-2024/src/scripts/lib/types/api/v3/products"; + +/* + * Faire un premier test simple où l'on clic sur la première carte du shop + * On doit pouvoir naviguer sur la page + * Le produit ne DOIT pas avoir de variations (pour l'instant) + * Le bouton d'ajout du panier doit être visible + * (Si le produit n'est pas en stock) On ne doit pas pouvoir ajouter le produit au panier + * Le bouton doit afficher le texte "Out of stock" + * Le bouton doit être désactivé + */ + +type ProductsFixture = { + products: WCV3Products; +}; + +export const test = base.extend({ + products: async ({ page, request }, use) => { + await page.goto("/shop"); + const nonce = await page.locator("data#nonce").textContent(); + const authString = await page.locator("data#auth-string").textContent(); + + if (nonce === null || nonce === "") { + throw new Error("Le nonce ne peut être vide."); + } + if (authString === null || authString === "") { + throw new Error("L'en-tête auth-string ne peut être vide."); + } + + const response = await request.get("/wp-json/wc/v3/products?page=1&per_page=100&status=publish", { + headers: { Nonce: nonce, Authorization: `Basic ${authString}` }, + }); + expect(response.ok(), "The API returned the list of every Product").toBeTruthy(); + + const products = await response.json() as WCV3Products; + await use(products); + }, +}); + +test("can add a Product without variation with stock to the Cart", async ({ products, page }) => { + const simpleProducts = products.filter(p => isSimpleProduct(p) && hasQuantity(p)); + console.debug("Simple Products with stock", simpleProducts.length); + expect(simpleProducts.length, "At least one Simple product with stock must exist").toBeGreaterThan(0); + + // Prend un produit au hasard. + const randomProductIndex = getRandomIntInclusive(0, simpleProducts.length - 1); + const randomProduct = simpleProducts.at(randomProductIndex); + expect(randomProduct, "The selected random Product must exist").toBeTruthy(); + if (randomProduct === undefined) { + throw new Error("The random product can't be undefined"); + } + + // Va à la page du Produit. + await page.goto(randomProduct.permalink); + + // Vérifie le bon état du bouton de l'ajout au Panier. + const addToCartButton = page.getByRole("button", { name: "Add to cart", disabled: false }); + await addToCartButton.scrollIntoViewIfNeeded(); + await expect(addToCartButton, "The add to cart button must be visible").toBeVisible(); + await expect(addToCartButton, "The add to cart button must be enabled").toBeEnabled(); + + // Vérifie qu'au clic sur le bouton, l'ajout au Panier retourne un succès. + const addToCartResponse: Promise = page.waitForResponse( + new RegExp(".*/wp-json/wc/store/cart/add-item"), + ); + await addToCartButton.click(); + const addToCartStatus = (await addToCartResponse).ok(); + expect(addToCartStatus, "The cart addition must succeed").toBeTruthy(); + + // Vérifie que le bouton ait changé de texte. + const addedToCartButton = page.getByRole("button", { name: "Added to cart!" }); + expect(addedToCartButton, "The add to cart button's text has changed").toBeDefined(); + + // Vérifie que le compteur d'articles dans le Panier soit incrémenté. + const cartLink = page.getByRole("link", { name: "cart (1)" }); + expect(cartLink, "The cart items' indicator has been incremented").toBeDefined(); +}); + +test("can't add a Product without variation without stock to the Cart", async ({ products, page }) => { + const simpleProducts = products.filter(p => isSimpleProduct(p) && !hasQuantity(p)); + console.debug("Simple Products without stock", simpleProducts.length); + expect(simpleProducts.length, "At least one Simple product without stock must exist").toBeGreaterThan(0); + + // Prend un produit au hasard. + const randomProductIndex = getRandomIntInclusive(0, simpleProducts.length - 1); + const randomProduct = simpleProducts.at(randomProductIndex); + expect(randomProduct, "The selected random Product must exist").toBeTruthy(); + if (randomProduct === undefined) { + throw new Error("The random product can't be undefined"); + } + + // Va à la page du Produit. + await page.goto(randomProduct.permalink); + + // Vérifie le bon état du bouton de l'ajout au Panier. + const outOfStockButton = page.getByRole("button", { name: "Out of stock", disabled: true }); + await outOfStockButton.scrollIntoViewIfNeeded(); + await expect(outOfStockButton, "The add to cart button must be visible").toBeVisible(); + await expect(outOfStockButton, "The add to cart button must be disabled").toBeDisabled(); +}); + +const getRandomIntInclusive = (min: number, max: number): number => { + const minCeiled = Math.ceil(min); + const maxFloored = Math.floor(max); + + return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled); +}; + +const isSimpleProduct = (product: WCV3Product) => product.type === "simple"; +const hasQuantity = (product: WCV3Product) => (product.stock_quantity ?? 0) > 0; diff --git a/tests/playwright/shop.spec.ts b/tests/playwright/shop.spec.ts new file mode 100644 index 00000000..b5522062 --- /dev/null +++ b/tests/playwright/shop.spec.ts @@ -0,0 +1,76 @@ +import { test, expect, Page, Locator, Response, APIRequestContext } from "@playwright/test"; +import { WCV3Products } from "../../web/app/themes/haiku-atelier-2024/src/scripts/lib/types/api/v3/products"; + +test.describe.configure({ mode: "parallel", timeout: 60000 }); + +test("can scroll to the end of the grid", async ({ page }): Promise => { + await scrollToGridsEnd(page); +}); + +test("can access all Products' pages", async ({ page, request }): Promise => { + await page.goto("https://haikuatelier.gcch.local/shop/"); + const links = await getAllProductsLinks(page, request); + + for (const link of links) { + // Vérifie que le lien de la page retourne OK. + const req = await request.get(link as string); + expect(req, "The Product's page is accessible").toBeOK(); + } +}); + +const getAllProductsLinks = async (page: Page, request: APIRequestContext): Promise> => { + const nonce = await page.locator("data#nonce").textContent(); + const authString = await page.locator("data#auth-string").textContent(); + + if (nonce === null || nonce === "") { + throw new Error("Le nonce ne peut être vide."); + } + if (authString === null || authString === "") { + throw new Error("L'en-tête auth-string ne peut être vide."); + } + + const response = await request.get("/wp-json/wc/v3/products?page=1&per_page=100&status=publish", { + headers: { Nonce: nonce, Authorization: `Basic ${authString}` }, + }); + const json = await response.json() as WCV3Products; + const links = json.map(p => p.permalink); + + return links; +}; + +const scrollToGridsEnd = async (page: Page): Promise => { + await page.goto("https://haikuatelier.gcch.local/shop/"); + + let hasMoreProducts = true; + let currentPageNumber = "1"; + + const productsGrid: Locator = page.locator(".grille-produits"); + await expect(productsGrid).toBeVisible(); + + const showMoreButton: Locator = page.getByRole("button", { name: "Show more" }); + await expect(productsGrid).toBeVisible(); + + while (hasMoreProducts) { + expect(await (productsGrid.getAttribute("data-page"))).toBe(currentPageNumber); + + const newProductsResponse: Promise = page.waitForResponse( + new RegExp(".*wp-json\/wc\/v3\/products.*"), + ); + await showMoreButton.click(); + await newProductsResponse; + + const newPageNumber = String(Number(currentPageNumber) + 1); + // Créé un nouveau Locator que l'on attend pour s'assurer que l'attribut soit bien mis à jour. + const gridWithNewPageNumber = page.locator(`.grille-produits[data-page="${newPageNumber}"]`); + await gridWithNewPageNumber.waitFor(); + + // Redondance pour expliciter la raison de l'assertion. + expect(await (productsGrid.getAttribute("data-page"))).toBe(newPageNumber); + currentPageNumber = newPageNumber; + + // La fin de la grille est atteint. + if (await showMoreButton.isHidden()) { + hasMoreProducts = false; + } + } +}; diff --git a/tsconfig.json b/tsconfig.json index fbb77606..f1d26c87 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -44,5 +44,5 @@ "useUnknownInCatchVariables": true }, "exclude": ["vendor", "web/app/plugins", "web/wp"], - "include": ["*.js", "lib", "web/app/themes/haiku-atelier-2024/src", "vite.config.ts"] + "include": ["*.js", "lib", "web/app/themes/haiku-atelier-2024/src", "vite.config.ts", "tests"] }