tests: ébauche des tests d'intégration via Playwright

This commit is contained in:
gcch 2025-12-23 16:18:28 +01:00
commit a0d91a0ef7
7 changed files with 285 additions and 89 deletions

View file

@ -0,0 +1,53 @@
import { expect, type Page, test } from "@playwright/test";
type TestPage = {
pageName: string;
url: string;
};
const genTimestamp = (): string =>
Intl.DateTimeFormat("sv-SE", {
dateStyle: "short",
}).format(Date.now());
const takeFullPageScreenshot = async (page: Page, name: string): Promise<void> => {
await page.screenshot({ fullPage: false, path: `captures/${name}`, type: "png" });
};
// TODO: Faire des tests spécifiques pour chaque page, que l'on puisse attendre que toutes les images dans la vue soient chargées, et prendre des captures à différentes positions dans la page.
Array.from<TestPage>([
{
pageName: "home",
url: "https://haikuatelier.gcch.local/",
},
{
pageName: "shop",
url: "https://haikuatelier.gcch.local/shop/",
},
{
pageName: "about",
url: "https://haikuatelier.gcch.local/about/",
},
{
pageName: "category",
url: "https://haikuatelier.gcch.local/product-category/rings/",
},
{
pageName: "product",
url: "https://haikuatelier.gcch.local/product/fuyou-long-earrings-silver/",
},
]).forEach(({ pageName, url }) => {
test.skip(pageName, async ({ page }, testInfo) => {
await page.goto(url);
const projectName = testInfo.project.name;
const timestamp: string = genTimestamp();
const viewport = page.viewportSize();
const captureName = `${pageName}/${projectName}-${viewport?.width}-${viewport?.height} ${timestamp}.png`;
await takeFullPageScreenshot(page, captureName);
await expect(page).toHaveURL(url);
});
});

View file

@ -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 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<ProductsFixture>({
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<Response> = 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;

View file

@ -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<void> => {
await scrollToGridsEnd(page);
});
test("can access all Products' pages", async ({ page, request }): Promise<void> => {
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<Array<string>> => {
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<void> => {
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<Response> = 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;
}
}
};