tests: ébauche des tests d'intégration via Playwright
This commit is contained in:
parent
a1dbd2170c
commit
a0d91a0ef7
7 changed files with 285 additions and 89 deletions
4
docs/TESTS.md
Normal file
4
docs/TESTS.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -21,20 +21,22 @@ export default defineConfig({
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 1 : undefined,
|
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 to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: "list",
|
reporter: "list",
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* 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 */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: "on-first-retry",
|
trace: "on-first-retry",
|
||||||
clientCertificates: [
|
clientCertificates: [
|
||||||
{
|
{
|
||||||
origin: "https://haikuatelier.gcch.local",
|
origin: "https://haikuatelier.gcch.local",
|
||||||
certPath: "../certs/_wildcard.gcch.local.pem",
|
certPath: "./containers/data/certs/_wildcard.gcch.local.pem",
|
||||||
keyPath: "../certs/_wildcard.gcch.local-key.pem",
|
keyPath: "./containers/data/certs/_wildcard.gcch.local-key.pem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
|
|
@ -46,42 +48,42 @@ export default defineConfig({
|
||||||
name: "desktop-chromium-1920",
|
name: "desktop-chromium-1920",
|
||||||
use: { ...devices["Desktop Chrome"], viewport: { width: 1920, height: 1080 } },
|
use: { ...devices["Desktop Chrome"], viewport: { width: 1920, height: 1080 } },
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: "desktop-chromium-1536",
|
// name: "desktop-chromium-1536",
|
||||||
use: { ...devices["Desktop Chrome"], viewport: { width: 1536, height: 864 } },
|
// use: { ...devices["Desktop Chrome"], viewport: { width: 1536, height: 864 } },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "desktop-chromium-1366",
|
// name: "desktop-chromium-1366",
|
||||||
use: { ...devices["Desktop Chrome"], viewport: { width: 1366, height: 768 } },
|
// use: { ...devices["Desktop Chrome"], viewport: { width: 1366, height: 768 } },
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
name: "desktop-firefox-1920",
|
name: "desktop-firefox-1920",
|
||||||
use: { ...devices["Desktop Firefox"], viewport: { width: 1920, height: 1080 } },
|
use: { ...devices["Desktop Firefox"], viewport: { width: 1920, height: 1080 } },
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: "desktop-firefox-1536",
|
// name: "desktop-firefox-1536",
|
||||||
use: { ...devices["Desktop Firefox"], viewport: { width: 1536, height: 864 } },
|
// use: { ...devices["Desktop Firefox"], viewport: { width: 1536, height: 864 } },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "desktop-firefox-1366",
|
// name: "desktop-firefox-1366",
|
||||||
use: { ...devices["Desktop Firefox"], viewport: { width: 1366, height: 768 } },
|
// use: { ...devices["Desktop Firefox"], viewport: { width: 1366, height: 768 } },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "tablet-chromium-portrait",
|
// name: "tablet-chromium-portrait",
|
||||||
use: { ...devices["Galaxy Tab S9"] },
|
// use: { ...devices["Galaxy Tab S9"] },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "tablet-chromium-landscape",
|
// name: "tablet-chromium-landscape",
|
||||||
use: { ...devices["Galaxy Tab S9 landscape"] },
|
// use: { ...devices["Galaxy Tab S9 landscape"] },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "mobile-chromium-portrait",
|
// name: "mobile-chromium-portrait",
|
||||||
use: { ...devices["Pixel 7"] },
|
// use: { ...devices["Pixel 7"] },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "mobile-chromium-landscape",
|
// name: "mobile-chromium-landscape",
|
||||||
use: { ...devices["Pixel 7 landscape"] },
|
// use: { ...devices["Pixel 7 landscape"] },
|
||||||
},
|
// },
|
||||||
],
|
],
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
// webServer: {
|
// webServer: {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ Array.from<TestPage>([
|
||||||
url: "https://haikuatelier.gcch.local/product/fuyou-long-earrings-silver/",
|
url: "https://haikuatelier.gcch.local/product/fuyou-long-earrings-silver/",
|
||||||
},
|
},
|
||||||
]).forEach(({ pageName, url }) => {
|
]).forEach(({ pageName, url }) => {
|
||||||
test(pageName, async ({ page }, testInfo) => {
|
test.skip(pageName, async ({ page }, testInfo) => {
|
||||||
await page.goto(url);
|
await page.goto(url);
|
||||||
|
|
||||||
const projectName = testInfo.project.name;
|
const projectName = testInfo.project.name;
|
||||||
114
tests/playwright/product.spec.ts
Normal file
114
tests/playwright/product.spec.ts
Normal 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 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<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;
|
||||||
76
tests/playwright/shop.spec.ts
Normal file
76
tests/playwright/shop.spec.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -44,5 +44,5 @@
|
||||||
"useUnknownInCatchVariables": true
|
"useUnknownInCatchVariables": true
|
||||||
},
|
},
|
||||||
"exclude": ["vendor", "web/app/plugins", "web/wp"],
|
"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"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue