Cover toolbox image persistence and replacement workflows
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s
This commit is contained in:
parent
e52d6e66eb
commit
7b160bac9c
4 changed files with 126 additions and 18 deletions
|
|
@ -49,3 +49,4 @@
|
||||||
{"id":"int-3823a391b02ae7a3d58956b24c10e998","kind":"field_change","created_at":"2026-09-13T18:43:29.457174095Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
|
{"id":"int-3823a391b02ae7a3d58956b24c10e998","kind":"field_change","created_at":"2026-09-13T18:43:29.457174095Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
|
||||||
{"id":"int-e3a2bea901894d43a31eb628b588648a","kind":"field_change","created_at":"2026-09-14T07:08:33.027716428Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
|
{"id":"int-e3a2bea901894d43a31eb628b588648a","kind":"field_change","created_at":"2026-09-14T07:08:33.027716428Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
|
||||||
{"id":"int-20d1e0d862e6155eadf2c36eced314db","kind":"field_change","created_at":"2026-09-14T11:56:36.962825878Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
|
{"id":"int-20d1e0d862e6155eadf2c36eced314db","kind":"field_change","created_at":"2026-09-14T11:56:36.962825878Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
|
||||||
|
{"id":"int-cc67f2645165e0eac9f57d71e8a34e57","kind":"field_change","created_at":"2026-09-14T12:11:10.207360007Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
||||||
- Pour le simulateur de chance, vérifier que les brouillons suivent le taux et l'outil actifs, restent intacts lors d'un rendu sans rapport et se valident au blur selon le champ.
|
- Pour le simulateur de chance, vérifier que les brouillons suivent le taux et l'outil actifs, restent intacts lors d'un rendu sans rapport et se valident au blur selon le champ.
|
||||||
- Pour le visualiseur d'images, vérifier la remise à zéro des marqueurs et des modes dessin/gomme au remplacement de l'image.
|
- Pour le visualiseur d'images, vérifier la remise à zéro des marqueurs et des modes dessin/gomme au remplacement de l'image.
|
||||||
- Pour l'éditeur de couverture, vérifier les décodages concurrents, les erreurs après fermeture et la libération des bitmaps/URLs ; une image en chargement ne doit pas permettre de valider la couverture précédente.
|
- Pour l'éditeur de couverture, vérifier les décodages concurrents, les erreurs après fermeture et la libération des bitmaps/URLs ; une image en chargement ne doit pas permettre de valider la couverture précédente.
|
||||||
|
- Les parcours `e2e/toolbox-cover.spec.js` vérifient le choix d'icône, l'enregistrement d'une couverture 620 × 248, sa persistance après rechargement et import/export, l'annulation et les fichiers illisibles. L'association à un jeu doit afficher sa couverture sans perdre l'image personnalisée, restaurée après dissociation ; choisir une icône doit supprimer la couverture personnalisée.
|
||||||
- Pour Mémos, vérifier l'édition courte, les couleurs, la réorganisation et la bande déroulante.
|
- Pour Mémos, vérifier l'édition courte, les couleurs, la réorganisation et la bande déroulante.
|
||||||
- Vérifier qu'une notification ou une mise à jour sans rapport ne remplace pas un brouillon Mémos avant sa validation au blur.
|
- Vérifier qu'une notification ou une mise à jour sans rapport ne remplace pas un brouillon Mémos avant sa validation au blur.
|
||||||
- Pour l'arbre d'objectifs, vérifier la sélection après suppression/import et le retour à Base quand le nœud sélectionné devient une étape sans onglet Contenu.
|
- Pour l'arbre d'objectifs, vérifier la sélection après suppression/import et le retour à Base quand le nœud sélectionné devient une étape sans onglet Contenu.
|
||||||
|
|
|
||||||
124
e2e/toolbox-cover.spec.js
Normal file
124
e2e/toolbox-cover.spec.js
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
// Rôle : vérifie la persistance et le remplacement des images de toolbox.
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { Buffer } from "node:buffer";
|
||||||
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
|
||||||
|
const imagePath = fileURLToPath(new URL("../website/public/static/img/toolbox-icons/sword.png", import.meta.url));
|
||||||
|
const fixture = JSON.parse(await readFile(new URL("./fixtures/toolbox-export.json", import.meta.url), "utf8"));
|
||||||
|
const cover = `data:image/png;base64,${(await readFile(imagePath)).toString("base64")}`;
|
||||||
|
const { games } = JSON.parse(await readFile(new URL("../website/public/data/games.json", import.meta.url), "utf8"));
|
||||||
|
const gameCover = games.find((game) => game.id === "mhwilds").images.cardCover;
|
||||||
|
|
||||||
|
function picker(page) {
|
||||||
|
return page.getByRole("button", { name: "Changer l'image de la toolbox" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importToolbox(page, name, scope = page.getByText("Importer une toolbox", { exact: true })) {
|
||||||
|
await scope.locator('input[type="file"][accept="application/json"]').setInputFiles({
|
||||||
|
name: "cover.json", mimeType: "application/json",
|
||||||
|
buffer: Buffer.from(JSON.stringify({ ...fixture, toolbox: { ...fixture.toolbox, name, customCover: cover } })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadCover(page, file = imagePath) {
|
||||||
|
await picker(page).click();
|
||||||
|
const action = page.getByRole("button", { name: /Ajouter une image de card|Remplacer l'image de card/ });
|
||||||
|
await expect(action).toBeVisible();
|
||||||
|
const chooser = page.waitForEvent("filechooser");
|
||||||
|
await action.click();
|
||||||
|
await (await chooser).setFiles(file);
|
||||||
|
return page.getByRole("dialog", { name: "Image de card", exact: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportToolbox(page, name) {
|
||||||
|
const download = page.waitForEvent("download");
|
||||||
|
await page.getByRole("button", { name: `Exporter ${name}`, exact: true }).click();
|
||||||
|
return JSON.parse(await readFile(await (await download).path(), "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test("saves a cropped cover through reload and export/import, then returns to an icon", async ({ page }) => {
|
||||||
|
await page.goto("/#/toolboxes");
|
||||||
|
await page.getByRole("button", { name: "Nouvelle toolbox" }).click();
|
||||||
|
const creation = page.getByRole("dialog", { name: "Nouvelle toolbox" });
|
||||||
|
await creation.getByLabel("Nom").fill("Cover workflow");
|
||||||
|
await creation.getByRole("button", { name: "Créer" }).click();
|
||||||
|
await expect(picker(page).locator("img")).toHaveAttribute("src", /toolbox\.png$/);
|
||||||
|
await picker(page).click();
|
||||||
|
await page.getByRole("button", { name: "Utiliser l'icône sword", exact: true }).click();
|
||||||
|
const editor = await uploadCover(page);
|
||||||
|
await expect(editor.getByRole("button", { name: "Valider" })).toBeEnabled();
|
||||||
|
await editor.getByRole("button", { name: "Valider" }).click();
|
||||||
|
await expect(editor).toBeHidden();
|
||||||
|
const savedCover = await picker(page).locator("img").getAttribute("src");
|
||||||
|
expect(savedCover).toMatch(/^data:image\/(webp|jpeg);base64,/);
|
||||||
|
await expect.poll(() => picker(page).locator("img").evaluate((img) => [img.naturalWidth, img.naturalHeight])).toEqual([620, 248]);
|
||||||
|
await page.reload();
|
||||||
|
await expect(picker(page).locator("img")).toHaveAttribute("src", savedCover);
|
||||||
|
await page.goto("/#/toolboxes");
|
||||||
|
await expect(page.getByRole("link", { name: "Ouvrir Cover workflow", exact: true }).first().locator("img")).toHaveAttribute("src", savedCover);
|
||||||
|
const exported = await exportToolbox(page, "Cover workflow");
|
||||||
|
expect(exported.toolbox.customCover).toBe(savedCover);
|
||||||
|
expect(exported.toolbox.icon).toMatch(/sword\.png$/);
|
||||||
|
exported.toolbox.name = "Imported cover";
|
||||||
|
await page.getByText("Importer une toolbox", { exact: true }).locator('input[type="file"]').setInputFiles({
|
||||||
|
name: "export.json", mimeType: "application/json", buffer: Buffer.from(JSON.stringify(exported)),
|
||||||
|
});
|
||||||
|
await page.getByRole("link", { name: "Ouvrir Imported cover", exact: true }).first().click();
|
||||||
|
await expect(picker(page).locator("img")).toHaveAttribute("src", savedCover);
|
||||||
|
await picker(page).click();
|
||||||
|
await expect(page.getByRole("button", { name: "Remplacer l'image de card" })).toHaveAttribute("aria-pressed", "true");
|
||||||
|
await expect(page.getByRole("button", { name: "Utiliser l'icône sword", exact: true })).toHaveAttribute("aria-pressed", "false");
|
||||||
|
await page.getByRole("button", { name: "Utiliser l'icône sword", exact: true }).click();
|
||||||
|
await page.reload();
|
||||||
|
await expect(picker(page).locator("img")).toHaveAttribute("src", /sword\.png$/);
|
||||||
|
await page.goto("/#/toolboxes");
|
||||||
|
expect((await exportToolbox(page, "Imported cover")).toolbox.customCover || "").toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cancelled and unreadable replacements preserve the cover and allow selecting the same file again", async ({ page }) => {
|
||||||
|
await page.goto("/#/toolboxes");
|
||||||
|
await importToolbox(page, "Preserved cover");
|
||||||
|
await page.getByRole("link", { name: "Ouvrir Preserved cover", exact: true }).first().click();
|
||||||
|
let editor = await uploadCover(page);
|
||||||
|
await expect(editor.getByRole("button", { name: "Valider" })).toBeEnabled();
|
||||||
|
await editor.getByRole("button", { name: "Annuler" }).click();
|
||||||
|
await expect(picker(page).locator("img")).toHaveAttribute("src", cover);
|
||||||
|
editor = await uploadCover(page);
|
||||||
|
await expect(editor.getByRole("button", { name: "Valider" })).toBeEnabled();
|
||||||
|
await editor.getByRole("button", { name: "Fermer", exact: true }).click();
|
||||||
|
editor = await uploadCover(page, { name: "broken.png", mimeType: "image/png", buffer: Buffer.from("not an image") });
|
||||||
|
await expect(editor.getByText("Image illisible. Choisis un autre fichier.")).toBeVisible();
|
||||||
|
await expect(editor.getByRole("button", { name: "Valider" })).toBeDisabled();
|
||||||
|
await editor.getByRole("button", { name: "Annuler" }).click();
|
||||||
|
await page.reload();
|
||||||
|
await expect(picker(page).locator("img")).toHaveAttribute("src", cover);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("game association takes precedence without discarding the custom cover", async ({ page }) => {
|
||||||
|
await page.goto("/#/games/mhwilds");
|
||||||
|
await page.getByRole("button", { name: "Associer une toolbox", exact: true }).click();
|
||||||
|
const drawer = page.locator("#toolbox-drawer .drawer-panel");
|
||||||
|
await importToolbox(page, "Linked cover", drawer);
|
||||||
|
await expect(drawer.getByRole("heading", { name: "Linked cover" })).toBeVisible();
|
||||||
|
await drawer.getByRole("button", { name: "Fermer", exact: true }).click();
|
||||||
|
await page.goto("/#/toolboxes");
|
||||||
|
const link = page.getByRole("link", { name: "Ouvrir Linked cover", exact: true }).first();
|
||||||
|
await expect(link.locator("img")).toHaveAttribute("src", gameCover);
|
||||||
|
await expect.poll(() => link.locator("img").evaluate((img) => img.naturalWidth)).toBeGreaterThan(0);
|
||||||
|
expect((await exportToolbox(page, "Linked cover")).toolbox.customCover).toBe(cover);
|
||||||
|
await link.click();
|
||||||
|
await expect(page.locator(".toolbox-game-icon img")).toHaveAttribute("src", gameCover);
|
||||||
|
await expect(picker(page)).toHaveCount(0);
|
||||||
|
await page.goto("/#/games/mhwilds");
|
||||||
|
await page.getByRole("button", { name: "Ouvrir la toolbox Linked cover", exact: true }).click();
|
||||||
|
await drawer.getByRole("button", { name: "Lier une toolbox", exact: true }).click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Lier une toolbox", exact: true });
|
||||||
|
await dialog.getByLabel("Toolbox associée").selectOption("");
|
||||||
|
await dialog.getByRole("button", { name: "Valider" }).click();
|
||||||
|
await drawer.getByRole("button", { name: "Fermer", exact: true }).click();
|
||||||
|
await page.goto("/#/toolboxes");
|
||||||
|
await expect(link.locator("img")).toHaveAttribute("src", cover);
|
||||||
|
await link.click();
|
||||||
|
await expect(picker(page).locator("img")).toHaveAttribute("src", cover);
|
||||||
|
});
|
||||||
|
|
@ -51,20 +51,6 @@ test("toolbox cards and pages retain their public UI conventions", async () => {
|
||||||
assert.match(resetUtils, /export function getLatestDailyReset/);
|
assert.match(resetUtils, /export function getLatestDailyReset/);
|
||||||
assert.match(resetUtils, /export function getLatestWeeklyReset/);
|
assert.match(resetUtils, /export function getLatestWeeklyReset/);
|
||||||
assert.match(toolboxStorage, /nextId\("calendar"\)/);
|
assert.match(toolboxStorage, /nextId\("calendar"\)/);
|
||||||
assert.match(toolboxCard, /DEFAULT_TOOLBOX_ICON/);
|
|
||||||
assert.match(toolboxCard, /toolbox\.customCover/);
|
|
||||||
assert.match(toolboxCard, /customCoverImage = !gameCoverImage/);
|
|
||||||
assert.match(toolboxCard, /triggerImage = toolbox\.customCover \|\| icon/);
|
|
||||||
assert.match(toolboxCard, /has-custom-cover/);
|
|
||||||
assert.match(toolboxCard, /toolbox-cover-menu-action nds-button/);
|
|
||||||
assert.match(toolboxCard, /!toolbox\.customCover && path === icon/);
|
|
||||||
assert.match(toolboxCard, /aria-pressed=\{Boolean\(toolbox\.customCover\)\}/);
|
|
||||||
assert.match(toolboxCard, /<span>Image personnalisée<\/span>/);
|
|
||||||
assert.match(toolboxCard, /onChange\(path, ""\)/);
|
|
||||||
assert.match(toolboxCard, /ToolboxCoverEditor/);
|
|
||||||
assert.match(toolboxCard, /accept="image\/\*"/);
|
|
||||||
assert.doesNotMatch(toolboxCard, /Modifier l'image de card/);
|
|
||||||
assert.doesNotMatch(toolboxCoverEditor, /Supprimer l'image/);
|
|
||||||
assert.match(toolboxCard, /export function ToolboxCard/);
|
assert.match(toolboxCard, /export function ToolboxCard/);
|
||||||
assert.match(toolboxCard, /export function ToolboxIconPicker/);
|
assert.match(toolboxCard, /export function ToolboxIconPicker/);
|
||||||
assert.match(toolboxCard, /export function ToolboxGameIcon/);
|
assert.match(toolboxCard, /export function ToolboxGameIcon/);
|
||||||
|
|
@ -73,7 +59,6 @@ test("toolbox cards and pages retain their public UI conventions", async () => {
|
||||||
assert.match(toolboxCard, /toolbox-card-cover-link/);
|
assert.match(toolboxCard, /toolbox-card-cover-link/);
|
||||||
assert.match(toolboxCard, /toolbox-icon-cover/);
|
assert.match(toolboxCard, /toolbox-icon-cover/);
|
||||||
assert.match(toolboxPages, /\.\/ToolboxCard\.jsx/);
|
assert.match(toolboxPages, /\.\/ToolboxCard\.jsx/);
|
||||||
assert.match(toolboxPages, /customCover = toolbox\.customCover/);
|
|
||||||
assert.match(toolboxPages, /updateToolboxOrder/);
|
assert.match(toolboxPages, /updateToolboxOrder/);
|
||||||
assert.match(toolboxPages, /useGroupedReorder/);
|
assert.match(toolboxPages, /useGroupedReorder/);
|
||||||
assert.match(toolboxPages, /useInlineEdit/);
|
assert.match(toolboxPages, /useInlineEdit/);
|
||||||
|
|
@ -98,9 +83,6 @@ test("toolbox cards and pages retain their public UI conventions", async () => {
|
||||||
assert.match(storageQuota, /role="progressbar"/);
|
assert.match(storageQuota, /role="progressbar"/);
|
||||||
assert.match(importButton, /export function ImportButton/);
|
assert.match(importButton, /export function ImportButton/);
|
||||||
assert.match(importButton, /accept="application\/json"/);
|
assert.match(importButton, /accept="application\/json"/);
|
||||||
assert.match(toolboxCoverEditor, /image\/webp/);
|
|
||||||
assert.match(toolboxCoverEditor, /OUTPUT_WIDTH = 620/);
|
|
||||||
assert.match(toolboxCoverEditor, /OUTPUT_HEIGHT = 248/);
|
|
||||||
assert.doesNotMatch(toolboxCoverEditor, /react-easy-crop|react-image-crop/);
|
assert.doesNotMatch(toolboxCoverEditor, /react-easy-crop|react-image-crop/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue