266 lines
16 KiB
JavaScript
266 lines
16 KiB
JavaScript
// Rôle : couvre les parcours persistants des liens, compteurs, checklists et galeries d'images.
|
|
import { expect, test } from "@playwright/test";
|
|
import { Buffer } from "node:buffer";
|
|
|
|
async function setup(page, type, data) {
|
|
await page.goto("/#/toolboxes");
|
|
await page.getByText("Importer une toolbox", { exact: true }).locator('input[type="file"]').setInputFiles({
|
|
name: "basic.json", mimeType: "application/json",
|
|
buffer: Buffer.from(JSON.stringify({
|
|
toolbox: { id: "basic", name: "Basic workflows", modules: [{ id: "tool", type, title: "Tool" }] },
|
|
modules: { tool: data },
|
|
})),
|
|
});
|
|
await page.getByRole("link", { name: "Ouvrir Basic workflows", exact: true }).first().click();
|
|
const tool = page.locator(`[data-module-type="${type}"]`);
|
|
await tool.getByRole("button", { name: "Afficher l'ajout de Tool", exact: true }).click();
|
|
return tool;
|
|
}
|
|
|
|
test("links persist creation and copying; deleting a category preserves its links", async ({ page, context }) => {
|
|
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
const tool = await setup(page, "links", { links: [] });
|
|
const form = tool.locator(".links-add-form");
|
|
await form.getByPlaceholder("Nom du lien", { exact: true }).fill("Guide");
|
|
await form.locator('input[type="url"]').fill("https://example.com/guide");
|
|
await form.getByPlaceholder("Catégorie", { exact: true }).fill("Reference");
|
|
await form.getByRole("button", { name: "Ajouter", exact: true }).click();
|
|
const link = tool.getByRole("link", { name: /Guide/ });
|
|
await expect(link).toHaveAttribute("href", "https://example.com/guide");
|
|
await expect(link).toHaveAttribute("target", "_blank");
|
|
await expect(link).toHaveAttribute("rel", /noreferrer/);
|
|
await expect(form.locator('input[type="url"]')).toHaveValue("");
|
|
await tool.getByRole("button", { name: "Copier Guide", exact: true }).click();
|
|
await expect.poll(() => page.evaluate(() => globalThis.navigator.clipboard.readText())).toBe("https://example.com/guide");
|
|
await page.reload();
|
|
await expect(link).toHaveAttribute("href", "https://example.com/guide");
|
|
await tool.getByRole("button", { name: "Supprimer la catégorie Reference", exact: true }).click();
|
|
await expect(tool.getByRole("heading", { name: "Reference", exact: true })).toHaveCount(0);
|
|
await page.reload();
|
|
await expect(link).toBeVisible();
|
|
await tool.getByRole("button", { name: "Supprimer Guide", exact: true }).click();
|
|
await page.reload();
|
|
await expect(link).toHaveCount(0);
|
|
});
|
|
|
|
test("counters persist signed values and reset without affecting other counters", async ({ page }) => {
|
|
const tool = await setup(page, "counters", { counters: [{ id: "other", label: "Other", value: 9 }] });
|
|
await tool.getByPlaceholder("Nom du compteur", { exact: true }).fill("Attempts");
|
|
await tool.locator(".counters-add-form").getByRole("button", { name: "Ajouter", exact: true }).click();
|
|
const item = tool.locator(".counter-item").filter({ has: page.getByRole("button", { name: "Supprimer Attempts", exact: true }) });
|
|
await tool.getByRole("button", { name: "Incrémenter Attempts", exact: true }).click();
|
|
await expect(item.locator("strong")).toHaveText("1");
|
|
await tool.getByRole("button", { name: "Décrémenter Attempts", exact: true }).click({ clickCount: 2 });
|
|
await expect(item.locator("strong")).toHaveText("-1");
|
|
await page.reload();
|
|
await expect(item.locator("strong")).toHaveText("-1");
|
|
await tool.getByRole("button", { name: "Réinitialiser Attempts", exact: true }).click();
|
|
await page.reload();
|
|
await expect(item.locator("strong")).toHaveText("0");
|
|
await tool.getByRole("button", { name: "Supprimer Attempts", exact: true }).click();
|
|
await page.reload();
|
|
await expect(tool.locator(".counter-item")).toHaveCount(1);
|
|
await expect(tool.locator(".counter-value strong")).toHaveText("9");
|
|
await expect(tool.locator(".counter-value span")).toHaveText("Other");
|
|
});
|
|
|
|
test("empty collection tools explain that they have no content", async ({ page }) => {
|
|
await page.goto("/#/toolboxes");
|
|
await page.getByText("Importer une toolbox", { exact: true }).locator('input[type="file"]').setInputFiles({
|
|
name: "empty-tools.json",
|
|
mimeType: "application/json",
|
|
buffer: Buffer.from(JSON.stringify({
|
|
toolbox: {
|
|
id: "empty-tools",
|
|
name: "Empty tools",
|
|
modules: [
|
|
{ id: "checklist", type: "checklist", title: "Checklist" },
|
|
{ id: "images", type: "images", title: "Images" },
|
|
{ id: "links", type: "links", title: "Links" },
|
|
{ id: "counters", type: "counters", title: "Counters" }
|
|
]
|
|
},
|
|
modules: {}
|
|
}))
|
|
});
|
|
await page.getByRole("link", { name: "Ouvrir Empty tools", exact: true }).first().click();
|
|
await expect(page.getByText("Aucun élément dans cette checklist.", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("Aucune image.", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("Aucun lien.", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("Aucun compteur.", { exact: true })).toBeVisible();
|
|
});
|
|
|
|
test("checklist quantities are clamped and completed categories hide without losing data", async ({ page }) => {
|
|
const tool = await setup(page, "checklist", { sections: [] });
|
|
await tool.getByPlaceholder("Catégorie", { exact: true }).fill("Craft");
|
|
await tool.getByPlaceholder("Nouvel item", { exact: true }).fill("Ore");
|
|
await tool.getByRole("spinbutton", { name: "Quantité cible", exact: true }).fill("3");
|
|
await tool.getByRole("button", { name: "Ajouter", exact: true }).click();
|
|
const quantity = tool.getByRole("spinbutton", { name: "Quantité actuelle Ore", exact: true });
|
|
await tool.getByRole("button", { name: "Ajouter une quantité", exact: true }).click();
|
|
await expect(quantity).toHaveValue("1");
|
|
await quantity.fill("99");
|
|
await expect(quantity).toHaveValue("3");
|
|
await tool.getByRole("radio", { name: "Cachée quand terminée", exact: true }).click();
|
|
await expect(quantity).toHaveCount(0);
|
|
await page.reload();
|
|
await expect(tool.getByRole("radio", { name: "Cachée quand terminée", exact: true })).toHaveAttribute("aria-checked", "true");
|
|
await tool.getByRole("radio", { name: "Visible quand terminée", exact: true }).click();
|
|
await expect(quantity).toHaveValue("3");
|
|
const hideCompleted = tool.getByRole("button", { name: "Masquer les éléments terminés", exact: true });
|
|
await hideCompleted.click();
|
|
await expect(hideCompleted).toHaveAttribute("aria-pressed", "true");
|
|
await expect(quantity).toHaveCount(0);
|
|
await page.reload();
|
|
await expect(hideCompleted).toHaveAttribute("aria-pressed", "true");
|
|
await hideCompleted.click();
|
|
await expect(quantity).toHaveValue("3");
|
|
await tool.getByRole("button", { name: "Retirer une quantité", exact: true }).click();
|
|
await page.reload();
|
|
await expect(quantity).toHaveValue("2");
|
|
await tool.getByRole("button", { name: "Supprimer Ore", exact: true }).click();
|
|
await page.reload();
|
|
await expect(quantity).toHaveCount(0);
|
|
});
|
|
|
|
test("a simple checklist can hide and restore checked rows", async ({ page }) => {
|
|
const tool = await setup(page, "checklist", {
|
|
items: [
|
|
{ id: "done", label: "Done", qtyCurrent: 1 },
|
|
{ id: "pending", label: "Pending" }
|
|
]
|
|
});
|
|
const hideCompleted = tool.getByRole("button", { name: "Masquer les éléments terminés", exact: true });
|
|
await hideCompleted.click();
|
|
await expect(tool.getByRole("checkbox", { name: "Terminer Done", exact: true })).toHaveCount(0);
|
|
await expect(tool.getByRole("checkbox", { name: "Terminer Pending", exact: true })).toBeVisible();
|
|
await hideCompleted.click();
|
|
await expect(tool.getByRole("checkbox", { name: "Terminer Done", exact: true })).toBeVisible();
|
|
});
|
|
|
|
test("images open a gallery and navigate the current collection without looping", async ({ page }) => {
|
|
const makeImage = (color) => `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360"><rect width="640" height="360" fill="${color}"/></svg>`)}`;
|
|
const tool = await setup(page, "images", {
|
|
images: [
|
|
{ id: "one", label: "First", dataUrl: makeImage("#8b5cf6") },
|
|
{ id: "two", label: "Second", dataUrl: makeImage("#22d3ee") },
|
|
{ id: "three", dataUrl: makeImage("#f6c453") }
|
|
]
|
|
});
|
|
|
|
await tool.getByRole("button", { name: "Galerie", exact: true }).click();
|
|
const dialog = page.getByRole("dialog", { name: "Galerie d'images", exact: true });
|
|
await expect(dialog).toBeVisible();
|
|
const thumbnails = dialog.locator(".image-viewer-gallery-item");
|
|
const gallery = dialog.locator(".image-viewer-gallery");
|
|
const ratioButton = dialog.locator(".image-viewer-gallery-ratio-button");
|
|
await expect(thumbnails).toHaveCount(3);
|
|
await expect(ratioButton).toHaveText("4:3");
|
|
await expect(ratioButton).toHaveAccessibleName(/Passer au format 1:1/);
|
|
const initialCardBox = await thumbnails.first().boundingBox();
|
|
expect(initialCardBox.width / initialCardBox.height).toBeCloseTo(4 / 3, 2);
|
|
await ratioButton.click();
|
|
await expect(ratioButton).toHaveText("1:1");
|
|
await expect(ratioButton).toHaveAccessibleName(/Passer au format 4:3/);
|
|
await expect.poll(async () => {
|
|
const box = await thumbnails.first().boundingBox();
|
|
return box.width / box.height;
|
|
}).toBeCloseTo(1, 2);
|
|
expect(await page.evaluate(() => globalThis.localStorage.getItem("sokkog:image-gallery-card-ratio"))).toBe("1:1");
|
|
const initialThumbnailWidth = (await thumbnails.first().boundingBox()).width;
|
|
await dialog.getByRole("button", { name: "Agrandir les aperçus", exact: true }).click();
|
|
await expect.poll(async () => (await thumbnails.first().boundingBox()).width).toBeCloseTo(initialThumbnailWidth + 40, 0);
|
|
expect(await page.evaluate(() => globalThis.localStorage.getItem("sokkog:image-gallery-thumbnail-width"))).toBe("300");
|
|
expect(await gallery.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
|
await dialog.getByRole("button", { name: "Fermer", exact: true }).click();
|
|
await page.reload();
|
|
await tool.getByRole("button", { name: "Galerie", exact: true }).click();
|
|
await expect.poll(async () => (await thumbnails.first().boundingBox()).width).toBeCloseTo(300, 0);
|
|
await expect(ratioButton).toHaveText("1:1");
|
|
await ratioButton.click();
|
|
await expect(ratioButton).toHaveText("4:3");
|
|
expect(await page.evaluate(() => globalThis.localStorage.getItem("sokkog:image-gallery-card-ratio"))).toBe("4:3");
|
|
await dialog.getByRole("button", { name: "Réduire les aperçus", exact: true }).click();
|
|
await expect.poll(async () => (await thumbnails.first().boundingBox()).width).toBeCloseTo(260, 0);
|
|
await dialog.getByRole("button", { name: "Afficher en grand : Second", exact: true }).click();
|
|
await expect(page.locator(".image-viewer-title")).toHaveText("Second");
|
|
await expect(page.locator(".image-viewer-position")).toHaveText("2 / 3");
|
|
|
|
await page.keyboard.press("ArrowRight");
|
|
await expect(page.locator(".image-viewer-title")).toHaveText("Image");
|
|
await expect(page.getByRole("button", { name: "Image suivante", exact: true })).toBeDisabled();
|
|
await page.keyboard.press("ArrowLeft");
|
|
await expect(page.locator(".image-viewer-title")).toHaveText("Second");
|
|
|
|
await page.getByRole("button", { name: "Revenir à la galerie", exact: true }).click();
|
|
const secondThumbnail = dialog.getByRole("button", { name: "Afficher en grand : Second", exact: true });
|
|
await expect(secondThumbnail).toBeFocused();
|
|
await page.keyboard.press("Escape");
|
|
await expect(dialog).toHaveCount(0);
|
|
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await tool.getByRole("button", { name: "Galerie", exact: true }).click();
|
|
const galleryBox = await dialog.boundingBox();
|
|
expect(galleryBox.x).toBeGreaterThanOrEqual(0);
|
|
expect(galleryBox.y).toBeGreaterThanOrEqual(0);
|
|
expect(galleryBox.x + galleryBox.width).toBeLessThanOrEqual(390);
|
|
expect(galleryBox.y + galleryBox.height).toBeLessThanOrEqual(844);
|
|
await ratioButton.click();
|
|
await expect(ratioButton).toHaveText("1:1");
|
|
const zoomIn = dialog.getByRole("button", { name: "Agrandir les aperçus", exact: true });
|
|
for (let step = 0; step < 4; step += 1) await zoomIn.click();
|
|
await expect(zoomIn).toBeDisabled();
|
|
expect(await page.evaluate(() => globalThis.localStorage.getItem("sokkog:image-gallery-thumbnail-width"))).toBe("420");
|
|
expect(await gallery.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
|
await dialog.getByRole("button", { name: "Fermer", exact: true }).click();
|
|
|
|
await tool.locator(".image-preview").nth(2).click();
|
|
await expect(page.locator(".image-viewer-position")).toHaveText("3 / 3");
|
|
await expect(page.getByRole("button", { name: "Image suivante", exact: true })).toBeDisabled();
|
|
await page.getByRole("button", { name: "Fermer", exact: true }).click();
|
|
});
|
|
|
|
test("image gallery keeps proportional cards and compact centered rows at maximum zoom", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1550, height: 900 });
|
|
const makeImage = (index) => `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360"><rect width="640" height="360" fill="hsl(${index * 45} 70% 45%)"/></svg>`)}`;
|
|
const tool = await setup(page, "images", {
|
|
images: Array.from({ length: 7 }, (_, index) => ({ id: `image-${index}`, label: `Image ${index + 1}`, dataUrl: makeImage(index) }))
|
|
});
|
|
await page.evaluate(() => globalThis.localStorage.setItem("sokkog:image-gallery-thumbnail-width", "420"));
|
|
await tool.getByRole("button", { name: "Galerie", exact: true }).click();
|
|
|
|
const dialog = page.getByRole("dialog", { name: "Galerie d'images", exact: true });
|
|
const gallery = dialog.locator(".image-viewer-gallery");
|
|
const cards = gallery.locator(".image-viewer-gallery-item");
|
|
await expect(cards).toHaveCount(7);
|
|
await expect.poll(() => cards.first().locator("img").evaluate((image) => image.naturalWidth)).toBe(640);
|
|
const boxes = await cards.evaluateAll((elements) => elements.map((element) => {
|
|
const box = element.getBoundingClientRect();
|
|
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
}));
|
|
expect(boxes[0].width / boxes[0].height).toBeCloseTo(4 / 3, 2);
|
|
expect(boxes.slice(0, 3).every((box) => Math.abs(box.y - boxes[0].y) < 1)).toBe(true);
|
|
expect(boxes.slice(3, 6).every((box) => Math.abs(box.y - boxes[3].y) < 1)).toBe(true);
|
|
expect(boxes[3].y).toBeGreaterThan(boxes[0].y + boxes[0].height);
|
|
expect(boxes[6].y).toBeGreaterThan(boxes[3].y + boxes[3].height);
|
|
|
|
const gap = boxes[1].x - boxes[0].x - boxes[0].width;
|
|
const galleryBox = await gallery.boundingBox();
|
|
expect(gap).toBeGreaterThanOrEqual(10);
|
|
expect(gap).toBeLessThanOrEqual(14);
|
|
expect(boxes[0].x - galleryBox.x).toBeGreaterThan(gap * 2);
|
|
expect(galleryBox.x + galleryBox.width - boxes[2].x - boxes[2].width).toBeGreaterThan(gap * 2);
|
|
expect(boxes[6].x + boxes[6].width / 2).toBeCloseTo(galleryBox.x + galleryBox.width / 2, 0);
|
|
|
|
const firstCard = cards.first();
|
|
const frameBox = await firstCard.locator(".image-viewer-gallery-frame").boundingBox();
|
|
const imageBox = await firstCard.locator("img").boundingBox();
|
|
const captionBox = await firstCard.locator(".image-viewer-gallery-caption").boundingBox();
|
|
expect(imageBox.x).toBeGreaterThanOrEqual(frameBox.x);
|
|
expect(imageBox.y).toBeGreaterThanOrEqual(frameBox.y);
|
|
expect(imageBox.x + imageBox.width).toBeLessThanOrEqual(frameBox.x + frameBox.width);
|
|
expect(imageBox.y + imageBox.height).toBeLessThanOrEqual(frameBox.y + frameBox.height);
|
|
expect(captionBox.y + captionBox.height).toBeLessThanOrEqual(boxes[0].y + boxes[0].height);
|
|
expect(await gallery.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
|
expect(await gallery.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true);
|
|
});
|