diff --git a/tests/data-validation.test.mjs b/tests/data-validation.test.mjs new file mode 100644 index 0000000..68e37cd --- /dev/null +++ b/tests/data-validation.test.mjs @@ -0,0 +1,78 @@ +import { readFile } from "node:fs/promises"; +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + validateDiablo4Affixes, + validateEndemicData, + validateMonsterData, + validateSiteContent, + validateTranslationKeys +} from "./helpers/data-validation.mjs"; + +test("site content json is complete", async () => { + const site = JSON.parse(await readFile("website/public/data/site.json", "utf8")); + validateSiteContent(site); +}); + +test("mhwilds data and assets are available", async () => { + const site = JSON.parse(await readFile("website/public/data/site.json", "utf8")); + const games = JSON.parse(await readFile("website/public/data/games.json", "utf8")); + const monsters = JSON.parse(await readFile("website/public/data/mhwilds/monsters.json", "utf8")); + const endemicLife = JSON.parse(await readFile("website/public/data/mhwilds/endemic_life.json", "utf8")); + const translations = JSON.parse(await readFile("website/public/data/mhwilds/i18n/fr.json", "utf8")); + const enTranslations = JSON.parse(await readFile("website/public/data/mhwilds/i18n/en.json", "utf8")); + const listingSource = await readFile("website/src/features/games/mhwilds/MhwildsListing.jsx", "utf8"); + + assert.equal(site.brand.name, "Sokko G"); + assert.equal(site.home.hero.title, "Sokko G"); + assert.equal(site.gamesPage.title, "Jeux disponibles"); + assert.match(site.toolboxes.summary, /espaces modulaires/); + assert.ok(site.toolboxes.storageHelp.items.length >= 3); + assert.equal(games.games[0].id, "mhwilds"); + assert.equal(games.games[0].eyebrow, "Guide de jeu"); + assert.match(games.games[0].images.cardCover, /games\/mhwilds\/card-cover\.png/); + assert.match(games.games[0].images.heroBg, /games\/mhwilds\/hero-bg\.png/); + assert.ok(monsters.monsters.length > 0); + assert.deepEqual(monsters.elements, ["fire", "water", "thunder", "ice", "dragon"]); + validateMonsterData(monsters.monsters); + assert.ok(Array.isArray(endemicLife.locations), "endemicLife.locations must be an array"); + assert.ok(endemicLife.locations.length > 0, "endemicLife.locations must not be empty"); + assert.ok(endemicLife.endemicLife.length > 0); + validateEndemicData(endemicLife); + validateTranslationKeys(translations, "fr"); + validateTranslationKeys(enTranslations, "en"); + assert.equal(translations.monsters, "monstres"); + assert.equal(translations.dark_hornet, "frelon clair-obscur"); + assert.equal(translations["dark hornet"], undefined); + assert.match(listingSource, /function MhwildsListing/); + const monsterCardSource = await readFile("website/src/features/games/mhwilds/cards/MonsterCard.jsx", "utf8"); + assert.match(monsterCardSource, /monster-card/); + assert.match(monsterCardSource, /monster\.hitzones/); + await readFile("website/public/static/img/mhwilds/chatacabra.png"); + await readFile("website/public/static/img/games/mhwilds/card-cover.png"); + await readFile("website/public/static/img/games/mhwilds/hero-bg.png"); +}); + +test("diablo4 data and route are available", async () => { + const games = JSON.parse(await readFile("website/public/data/games.json", "utf8")); + const affixes = JSON.parse(await readFile("website/public/data/diablo4/affixes_types.json", "utf8")); + const routeSource = await readFile("website/src/features/games/GameRoute.jsx", "utf8"); + const pageSource = await readFile("website/src/features/games/diablo4/Diablo4Page.jsx", "utf8"); + const listingSource = await readFile("website/src/features/games/diablo4/Diablo4Listing.jsx", "utf8"); + const diablo4Game = games.games.find((game) => game.id === "diablo4"); + + assert.ok(diablo4Game, "games.json must declare diablo4"); + assert.match(diablo4Game.images.cardCover, /games\/diablo4\/card-cover\.png/); + assert.match(diablo4Game.images.heroBg, /games\/diablo4\/hero-bg\.png/); + validateDiablo4Affixes(affixes); + assert.match(routeSource, /game\.id === "diablo4"/); + assert.match(pageSource, /category === "affixes"/); + assert.match(listingSource, /diablo4\.filterOptions\.affixes/); + await readFile("website/public/static/img/games/diablo4/card-cover.png"); + await readFile("website/public/static/img/games/diablo4/hero-bg.png"); + await readFile("website/public/static/img/diablo4/axe.svg"); + await readFile("website/public/static/img/diablo4/cube.svg"); + await readFile("website/public/static/img/diablo4/fire.svg"); + await readFile("website/public/static/img/diablo4/shield.svg"); + await readFile("website/public/static/img/diablo4/wing.svg"); +}); diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs new file mode 100644 index 0000000..7b70265 --- /dev/null +++ b/tests/helpers/data-validation.mjs @@ -0,0 +1,178 @@ +import assert from "node:assert/strict"; + +const SNAKE_CASE_RE = /^[a-z0-9]+(?:_[a-z0-9]+)*$/; +const PHYSICAL_HITZONE_KEYS = ["cut", "blunt", "ammo"]; +const ELEMENTAL_HITZONE_KEYS = ["fire", "water", "thunder", "ice", "dragon"]; + +function valueAt(object, path) { + return path.split(".").reduce((value, key) => value?.[key], object); +} + +function assertNonEmptyString(object, path) { + const value = valueAt(object, path); + assert.equal(typeof value, "string", `${path} must be a string`); + assert.ok(value.trim(), `${path} must not be empty`); +} + +export function assertStringArray(object, path, minLength = 1) { + const value = valueAt(object, path); + assert.ok(Array.isArray(value), `${path} must be an array`); + assert.ok(value.length >= minLength, `${path} must contain at least ${minLength} item(s)`); + value.forEach((item, index) => { + assert.equal(typeof item, "string", `${path}[${index}] must be a string`); + assert.ok(item.trim(), `${path}[${index}] must not be empty`); + }); +} + +export function validateSiteContent(site) { + [ + "brand.name", + "brand.homeAriaLabel", + "navigation.home", + "navigation.toolboxes", + "navigation.games", + "navigation.mobileGames", + "sidebar.badge", + "sidebar.note", + "topbar.dashboard", + "topbar.toolbox", + "topbar.games", + "gamesPage.eyebrow", + "gamesPage.title", + "gamesPage.description", + "gamesPage.emptyTitle", + "gamesPage.emptyText", + "home.hero.eyebrow", + "home.hero.title", + "home.hero.description", + "home.hero.primaryAction", + "home.hero.secondaryAction", + "home.stats.toolboxSingular", + "home.stats.toolboxPlural", + "home.stats.toolSingular", + "home.stats.toolPlural", + "home.origin.eyebrow", + "home.origin.title", + "home.origin.visualAlt", + "home.origin.dialogueAriaLabel", + "home.origin.caption", + "home.legal.copyright", + "home.legal.disclaimer", + "toolboxes.eyebrow", + "toolboxes.title", + "toolboxes.newButton", + "toolboxes.importAll", + "toolboxes.exportAll", + "toolboxes.importOne", + "toolboxes.summary", + "toolboxes.storageHelp.title", + "toolboxes.storageHelp.text", + "toolboxes.emptyTitle", + "toolboxes.emptyText" + ].forEach((path) => assertNonEmptyString(site, path)); + + assertStringArray(site, "toolboxes.storageHelp.items", 3); + + const lines = valueAt(site, "home.origin.lines"); + assert.ok(Array.isArray(lines), "home.origin.lines must be an array"); + assert.ok(lines.length >= 2, "home.origin.lines must contain at least 2 items"); + lines.forEach((line, index) => { + assert.ok(["user", "app"].includes(line?.speaker), `home.origin.lines[${index}].speaker must be "user" or "app"`); + assert.equal(typeof line?.text, "string", `home.origin.lines[${index}].text must be a string`); + assert.ok(line.text.trim(), `home.origin.lines[${index}].text must not be empty`); + }); +} + +function assertSnakeCase(value, path) { + assert.equal(typeof value, "string", `${path} must be a string`); + assert.match(value, SNAKE_CASE_RE, `${path} must be snake_case`); +} + +function validateConditionValues(items, path) { + assert.ok(Array.isArray(items), `${path} must be an array`); + items.forEach((item, index) => { + assertSnakeCase(item?.condition, `${path}[${index}].condition`); + assert.ok(Array.isArray(item?.values), `${path}[${index}].values must be an array`); + item.values.forEach((value, valueIndex) => assertSnakeCase(value, `${path}[${index}].values[${valueIndex}]`)); + }); +} + +export function validateMonsterData(monsters) { + assert.ok(Array.isArray(monsters), "monsters must be an array"); + const ids = new Set(); + const names = new Set(); + + monsters.forEach((monster, monsterIndex) => { + const path = `monsters[${monsterIndex}]`; + assert.equal(typeof monster.id, "number", `${path}.id must be a number`); + assert.ok(Number.isInteger(monster.id), `${path}.id must be an integer`); + assert.ok(!ids.has(monster.id), `${path}.id must be unique`); + ids.add(monster.id); + + assertSnakeCase(monster.name, `${path}.name`); + assert.ok(!names.has(monster.name), `${path}.name must be unique`); + names.add(monster.name); + assertSnakeCase(monster.type, `${path}.type`); + validateConditionValues(monster.ailments, `${path}.ailments`); + validateConditionValues(monster.weaknesses, `${path}.weaknesses`); + validateConditionValues(monster.weakpoints, `${path}.weakpoints`); + + assert.equal(monster.damage, undefined, `${path}.damage must not be used`); + assert.ok(Array.isArray(monster.hitzones), `${path}.hitzones must be an array`); + assert.ok(monster.hitzones.length > 0, `${path}.hitzones must not be empty`); + monster.hitzones.forEach((hitzone, hitzoneIndex) => { + const hitzonePath = `${path}.hitzones[${hitzoneIndex}]`; + assertSnakeCase(hitzone.name, `${hitzonePath}.name`); + assert.equal(typeof hitzone.physical, "object", `${hitzonePath}.physical must be an object`); + assert.equal(typeof hitzone.elemental, "object", `${hitzonePath}.elemental must be an object`); + PHYSICAL_HITZONE_KEYS.forEach((key) => assert.equal(typeof hitzone.physical[key], "number", `${hitzonePath}.physical.${key} must be a number`)); + ELEMENTAL_HITZONE_KEYS.forEach((key) => assert.equal(typeof hitzone.elemental[key], "number", `${hitzonePath}.elemental.${key} must be a number`)); + }); + }); +} + +export function validateEndemicData(endemicPayload) { + ["endemicLife", "aquaticLife"].forEach((collection) => { + assert.ok(Array.isArray(endemicPayload[collection]), `${collection} must be an array`); + endemicPayload[collection].forEach((item, itemIndex) => { + const path = `${collection}[${itemIndex}]`; + assertSnakeCase(item.name, `${path}.name`); + if (item.description) assertSnakeCase(item.description, `${path}.description`); + validateConditionValues(item.locations, `${path}.locations`); + }); + }); +} + +export function validateTranslationKeys(translations, path) { + Object.keys(translations).forEach((key) => { + assert.ok(!/\s/.test(key), `${path}.${key} must not contain spaces`); + }); +} + +export function validateDiablo4Affixes(payload) { + assert.ok(Array.isArray(payload.categories), "diablo4.categories must be an array"); + assert.ok(payload.categories.length > 0, "diablo4.categories must not be empty"); + assert.ok(Array.isArray(payload.affixes), "diablo4.affixes must be an array"); + assert.ok(payload.affixes.length > 0, "diablo4.affixes must not be empty"); + + const ids = new Set(); + const categoryIds = payload.categories.map((category, index) => { + assertSnakeCase(category.id, `diablo4.categories[${index}].id`); + assertNonEmptyString({ category }, "category.label"); + assertSnakeCase(category.icon, `diablo4.categories[${index}].icon`); + assertSnakeCase(category.tone, `diablo4.categories[${index}].tone`); + return category.id; + }); + + payload.affixes.forEach((affix, index) => { + assertSnakeCase(affix.id, `diablo4.affixes[${index}].id`); + assert.ok(!ids.has(affix.id), `diablo4.affixes[${index}].id must be unique`); + ids.add(affix.id); + assertNonEmptyString({ affix }, "affix.label"); + assert.ok(Array.isArray(affix.categories), `diablo4.affixes[${index}].categories must be an array`); + affix.categories.forEach((category, categoryIndex) => { + assertSnakeCase(category, `diablo4.affixes[${index}].categories[${categoryIndex}]`); + assert.ok(categoryIds.includes(category), `diablo4.affixes[${index}].categories[${categoryIndex}] must exist in diablo4.categories`); + }); + }); +} diff --git a/tests/static-smoke.test.mjs b/tests/static-smoke.test.mjs index c4f036e..6326bec 100644 --- a/tests/static-smoke.test.mjs +++ b/tests/static-smoke.test.mjs @@ -2,182 +2,6 @@ import { readFile } from "node:fs/promises"; import { test } from "node:test"; import assert from "node:assert/strict"; -const SNAKE_CASE_RE = /^[a-z0-9]+(?:_[a-z0-9]+)*$/; -const PHYSICAL_HITZONE_KEYS = ["cut", "blunt", "ammo"]; -const ELEMENTAL_HITZONE_KEYS = ["fire", "water", "thunder", "ice", "dragon"]; - -function valueAt(object, path) { - return path.split(".").reduce((value, key) => value?.[key], object); -} - -function assertNonEmptyString(object, path) { - const value = valueAt(object, path); - assert.equal(typeof value, "string", `${path} must be a string`); - assert.ok(value.trim(), `${path} must not be empty`); -} - -function assertStringArray(object, path, minLength = 1) { - const value = valueAt(object, path); - assert.ok(Array.isArray(value), `${path} must be an array`); - assert.ok(value.length >= minLength, `${path} must contain at least ${minLength} item(s)`); - value.forEach((item, index) => { - assert.equal(typeof item, "string", `${path}[${index}] must be a string`); - assert.ok(item.trim(), `${path}[${index}] must not be empty`); - }); -} - -function validateSiteContent(site) { - [ - "brand.name", - "brand.homeAriaLabel", - "navigation.home", - "navigation.toolboxes", - "navigation.games", - "navigation.mobileGames", - "sidebar.badge", - "sidebar.note", - "topbar.dashboard", - "topbar.toolbox", - "topbar.games", - "gamesPage.eyebrow", - "gamesPage.title", - "gamesPage.description", - "gamesPage.emptyTitle", - "gamesPage.emptyText", - "home.hero.eyebrow", - "home.hero.title", - "home.hero.description", - "home.hero.primaryAction", - "home.hero.secondaryAction", - "home.stats.toolboxSingular", - "home.stats.toolboxPlural", - "home.stats.toolSingular", - "home.stats.toolPlural", - "home.origin.eyebrow", - "home.origin.title", - "home.origin.visualAlt", - "home.origin.dialogueAriaLabel", - "home.origin.caption", - "home.legal.copyright", - "home.legal.disclaimer", - "toolboxes.eyebrow", - "toolboxes.title", - "toolboxes.newButton", - "toolboxes.importAll", - "toolboxes.exportAll", - "toolboxes.importOne", - "toolboxes.summary", - "toolboxes.storageHelp.title", - "toolboxes.storageHelp.text", - "toolboxes.emptyTitle", - "toolboxes.emptyText" - ].forEach((path) => assertNonEmptyString(site, path)); - - assertStringArray(site, "toolboxes.storageHelp.items", 3); - - const lines = valueAt(site, "home.origin.lines"); - assert.ok(Array.isArray(lines), "home.origin.lines must be an array"); - assert.ok(lines.length >= 2, "home.origin.lines must contain at least 2 items"); - lines.forEach((line, index) => { - assert.ok(["user", "app"].includes(line?.speaker), `home.origin.lines[${index}].speaker must be "user" or "app"`); - assert.equal(typeof line?.text, "string", `home.origin.lines[${index}].text must be a string`); - assert.ok(line.text.trim(), `home.origin.lines[${index}].text must not be empty`); - }); -} - -function assertSnakeCase(value, path) { - assert.equal(typeof value, "string", `${path} must be a string`); - assert.match(value, SNAKE_CASE_RE, `${path} must be snake_case`); -} - -function validateConditionValues(items, path) { - assert.ok(Array.isArray(items), `${path} must be an array`); - items.forEach((item, index) => { - assertSnakeCase(item?.condition, `${path}[${index}].condition`); - assert.ok(Array.isArray(item?.values), `${path}[${index}].values must be an array`); - item.values.forEach((value, valueIndex) => assertSnakeCase(value, `${path}[${index}].values[${valueIndex}]`)); - }); -} - -function validateMonsterData(monsters) { - assert.ok(Array.isArray(monsters), "monsters must be an array"); - const ids = new Set(); - const names = new Set(); - - monsters.forEach((monster, monsterIndex) => { - const path = `monsters[${monsterIndex}]`; - assert.equal(typeof monster.id, "number", `${path}.id must be a number`); - assert.ok(Number.isInteger(monster.id), `${path}.id must be an integer`); - assert.ok(!ids.has(monster.id), `${path}.id must be unique`); - ids.add(monster.id); - - assertSnakeCase(monster.name, `${path}.name`); - assert.ok(!names.has(monster.name), `${path}.name must be unique`); - names.add(monster.name); - assertSnakeCase(monster.type, `${path}.type`); - validateConditionValues(monster.ailments, `${path}.ailments`); - validateConditionValues(monster.weaknesses, `${path}.weaknesses`); - validateConditionValues(monster.weakpoints, `${path}.weakpoints`); - - assert.equal(monster.damage, undefined, `${path}.damage must not be used`); - assert.ok(Array.isArray(monster.hitzones), `${path}.hitzones must be an array`); - assert.ok(monster.hitzones.length > 0, `${path}.hitzones must not be empty`); - monster.hitzones.forEach((hitzone, hitzoneIndex) => { - const hitzonePath = `${path}.hitzones[${hitzoneIndex}]`; - assertSnakeCase(hitzone.name, `${hitzonePath}.name`); - assert.equal(typeof hitzone.physical, "object", `${hitzonePath}.physical must be an object`); - assert.equal(typeof hitzone.elemental, "object", `${hitzonePath}.elemental must be an object`); - PHYSICAL_HITZONE_KEYS.forEach((key) => assert.equal(typeof hitzone.physical[key], "number", `${hitzonePath}.physical.${key} must be a number`)); - ELEMENTAL_HITZONE_KEYS.forEach((key) => assert.equal(typeof hitzone.elemental[key], "number", `${hitzonePath}.elemental.${key} must be a number`)); - }); - }); -} - -function validateEndemicData(endemicPayload) { - ["endemicLife", "aquaticLife"].forEach((collection) => { - assert.ok(Array.isArray(endemicPayload[collection]), `${collection} must be an array`); - endemicPayload[collection].forEach((item, itemIndex) => { - const path = `${collection}[${itemIndex}]`; - assertSnakeCase(item.name, `${path}.name`); - if (item.description) assertSnakeCase(item.description, `${path}.description`); - validateConditionValues(item.locations, `${path}.locations`); - }); - }); -} - -function validateTranslationKeys(translations, path) { - Object.keys(translations).forEach((key) => { - assert.ok(!/\s/.test(key), `${path}.${key} must not contain spaces`); - }); -} - -function validateDiablo4Affixes(payload) { - assert.ok(Array.isArray(payload.categories), "diablo4.categories must be an array"); - assert.ok(payload.categories.length > 0, "diablo4.categories must not be empty"); - assert.ok(Array.isArray(payload.affixes), "diablo4.affixes must be an array"); - assert.ok(payload.affixes.length > 0, "diablo4.affixes must not be empty"); - - const ids = new Set(); - const categoryIds = payload.categories.map((category, index) => { - assertSnakeCase(category.id, `diablo4.categories[${index}].id`); - assertNonEmptyString({ category }, "category.label"); - assertSnakeCase(category.icon, `diablo4.categories[${index}].icon`); - assertSnakeCase(category.tone, `diablo4.categories[${index}].tone`); - return category.id; - }); - payload.affixes.forEach((affix, index) => { - assertSnakeCase(affix.id, `diablo4.affixes[${index}].id`); - assert.ok(!ids.has(affix.id), `diablo4.affixes[${index}].id must be unique`); - ids.add(affix.id); - assertNonEmptyString({ affix }, "affix.label"); - assert.ok(Array.isArray(affix.categories), `diablo4.affixes[${index}].categories must be an array`); - affix.categories.forEach((category, categoryIndex) => { - assertSnakeCase(category, `diablo4.affixes[${index}].categories[${categoryIndex}]`); - assert.ok(categoryIds.includes(category), `diablo4.affixes[${index}].categories[${categoryIndex}] must exist in diablo4.categories`); - }); - }); -} - test("vite entrypoint loads the react application", async () => { const html = await readFile("website/index.html", "utf8"); @@ -192,6 +16,7 @@ test("react application defines the expected local toolbox primitives", async () const styleNebula = await readFile("website/src/styles/_nebula.scss", "utf8"); const styleTokens = await readFile("website/src/styles/_tokens.scss", "utf8"); const styleBase = await readFile("website/src/styles/_base.scss", "utf8"); + const styleCards = await readFile("website/src/styles/_cards.scss", "utf8"); const styleHome = await readFile("website/src/styles/_home.scss", "utf8"); const styleToolboxes = await readFile("website/src/styles/_toolboxes.scss", "utf8"); const styleIcons = await readFile("website/src/styles/_icons.scss", "utf8"); @@ -200,6 +25,8 @@ test("react application defines the expected local toolbox primitives", async () const iconComponent = await readFile("website/src/components/Icon.jsx", "utf8"); const gamesPage = await readFile("website/src/features/games/GamesPage.jsx", "utf8"); const gameRoute = await readFile("website/src/features/games/GameRoute.jsx", "utf8"); + const gameLoaders = await readFile("website/src/features/games/loaders.js", "utf8"); + const gameFiltersPanel = await readFile("website/src/features/games/GameFiltersPanel.jsx", "utf8"); const diablo4Page = await readFile("website/src/features/games/diablo4/Diablo4Page.jsx", "utf8"); const diablo4Overview = await readFile("website/src/features/games/diablo4/Diablo4Overview.jsx", "utf8"); const diablo4Listing = await readFile("website/src/features/games/diablo4/Diablo4Listing.jsx", "utf8"); @@ -218,6 +45,7 @@ test("react application defines the expected local toolbox primitives", async () const screenshotsModule = await readFile("website/src/features/toolboxes/modules/ScreenshotsModule.jsx", "utf8"); const linksModule = await readFile("website/src/features/toolboxes/modules/LinksModule.jsx", "utf8"); const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8"); + const reorderHook = await readFile("website/src/hooks/usePointerReorder.js", "utf8"); assert.match(source, /sokkog:toolboxes/); assert.match(source, /sokkog:game-toolbox-links/); @@ -244,6 +72,10 @@ test("react application defines the expected local toolbox primitives", async () assert.match(styleIcons, /ui-icon-add/); assert.match(styleMhwilds, /\.mhwilds-card/); assert.match(styleMhwilds, /\.mhwilds-flip-indicator/); + assert.match(styleMhwilds, /\.game-home-card/); + assert.match(styleMhwilds, /\.game-layout/); + assert.match(styleMhwilds, /\.game-grid/); + assert.match(styleCards, /compact-data-card/); assert.match(styleResponsive, /@media \(max-width: 760px\)/); assert.match(source, /features\/toolboxes\/modules\/index\.jsx/); assert.match(source, /features\/games\/GamesPage\.jsx/); @@ -256,10 +88,17 @@ test("react application defines the expected local toolbox primitives", async () assert.match(gameRoute, /export function GameRoute/); assert.match(gameRoute, /MhwildsPage/); assert.match(gameRoute, /Diablo4Page/); + assert.match(gameLoaders, /export async function loadMhwildsData/); + assert.match(gameLoaders, /export async function loadDiablo4Data/); + assert.match(gameLoaders, /\/data\/diablo4\/affixes_types\.json/); + assert.match(gameLoaders, /filterOptionKeys/); + assert.match(gameFiltersPanel, /export function GameFiltersPanel/); + assert.match(gameFiltersPanel, /filter-reset-button/); assert.match(diablo4Page, /export function Diablo4Page/); assert.match(diablo4Overview, /export function Diablo4Overview/); assert.match(diablo4Listing, /export function Diablo4Listing/); assert.match(diablo4Filters, /export function Diablo4Filters/); + assert.match(diablo4Filters, /GameFiltersPanel/); assert.match(diablo4AffixCard, /export function Diablo4AffixCard/); assert.match(diablo4AffixCard, /categoryMap/); assert.match(diablo4Utils, /getFilteredDiablo4Affixes/); @@ -268,6 +107,7 @@ test("react application defines the expected local toolbox primitives", async () assert.match(mhwildsListing, /export function MhwildsListing/); assert.match(mhwildsListing, /endemic_life/); assert.match(mhwildsFilters, /export function MhwildsFilters/); + assert.match(mhwildsFilters, /GameFiltersPanel/); assert.match(monsterCard, /export function MonsterCard/); assert.match(monsterCard, /ui-icon-flip/); assert.match(endemicCard, /export function EndemicCard/); @@ -297,14 +137,15 @@ test("react application defines the expected local toolbox primitives", async () assert.match(linksModule, /copyText/); assert.match(source, /DEFAULT_SITE_CONTENT/); assert.match(source, /\/data\/site\.json/); - assert.match(source, /\/data\/diablo4\/affixes_types\.json/); - assert.match(source, /filterOptionKeys/); assert.match(source, /APP_STORAGE_LIMIT_BYTES/); assert.match(source, /DEFAULT_TOOLBOX_ICON/); assert.match(source, /TOOLBOX_ICON_FILES/); assert.match(source, /function normalizeToolboxIcon/); assert.match(source, /function ToolboxIconPicker/); assert.match(source, /function ToolboxGameIcon/); + assert.match(source, /updateToolboxOrder/); + assert.match(source, /draggingToolboxId/); + assert.match(source, /toolbox-card-drag-handle/); assert.match(source, /function StorageQuota/); assert.match(source, /function writeStorageValue/); assert.match(source, /role="progressbar"/); @@ -323,10 +164,13 @@ test("react application defines the expected local toolbox primitives", async () assert.match(source, /toolbox-icon-cover/); assert.match(source, /moduleColumns/); assert.match(source, /function ToolboxView/); + assert.match(source, /usePointerReorder/); assert.match(moduleRegistry, /onDragStart/); assert.match(moduleRegistry, /onPointerDown/); - assert.match(moduleRegistry, /setPointerCapture/); - assert.match(moduleRegistry, /elementFromPoint/); + assert.match(moduleRegistry, /usePointerReorder/); + assert.match(reorderHook, /export function usePointerReorder/); + assert.match(reorderHook, /setPointerCapture/); + assert.match(reorderHook, /elementFromPoint/); assert.match(moduleRegistry, /tool-add-card/); assert.match(moduleRegistry, /tool-quick-add-button/); assert.match(moduleRegistry, /tool-add-quick-toggle/); @@ -344,74 +188,7 @@ test("react application defines the expected local toolbox primitives", async () assert.match(source, /function openImageInNewTab/); assert.match(source, / { - const site = JSON.parse(await readFile("website/public/data/site.json", "utf8")); - validateSiteContent(site); -}); - -test("mhwilds data and assets are available", async () => { - const site = JSON.parse(await readFile("website/public/data/site.json", "utf8")); - const games = JSON.parse(await readFile("website/public/data/games.json", "utf8")); - const monsters = JSON.parse(await readFile("website/public/data/mhwilds/monsters.json", "utf8")); - const endemicLife = JSON.parse(await readFile("website/public/data/mhwilds/endemic_life.json", "utf8")); - const translations = JSON.parse(await readFile("website/public/data/mhwilds/i18n/fr.json", "utf8")); - const enTranslations = JSON.parse(await readFile("website/public/data/mhwilds/i18n/en.json", "utf8")); - const listingSource = await readFile("website/src/features/games/mhwilds/MhwildsListing.jsx", "utf8"); - - assert.equal(site.brand.name, "Sokko G"); - assert.equal(site.home.hero.title, "Sokko G"); - assert.equal(site.gamesPage.title, "Jeux disponibles"); - assert.match(site.toolboxes.summary, /espaces modulaires/); - assert.ok(site.toolboxes.storageHelp.items.length >= 3); - assert.equal(games.games[0].id, "mhwilds"); - assert.equal(games.games[0].eyebrow, "Guide de jeu"); - assert.match(games.games[0].images.cardCover, /games\/mhwilds\/card-cover\.png/); - assert.match(games.games[0].images.heroBg, /games\/mhwilds\/hero-bg\.png/); - assert.ok(monsters.monsters.length > 0); - assert.deepEqual(monsters.elements, ["fire", "water", "thunder", "ice", "dragon"]); - validateMonsterData(monsters.monsters); - assert.ok(Array.isArray(endemicLife.locations), "endemicLife.locations must be an array"); - assert.ok(endemicLife.locations.length > 0, "endemicLife.locations must not be empty"); - assert.ok(endemicLife.endemicLife.length > 0); - validateEndemicData(endemicLife); - validateTranslationKeys(translations, "fr"); - validateTranslationKeys(enTranslations, "en"); - assert.equal(translations.monsters, "monstres"); - assert.equal(translations.dark_hornet, "frelon clair-obscur"); - assert.equal(translations["dark hornet"], undefined); - assert.match(listingSource, /function MhwildsListing/); - const monsterCardSource = await readFile("website/src/features/games/mhwilds/cards/MonsterCard.jsx", "utf8"); - assert.match(monsterCardSource, /monster-card/); - assert.match(monsterCardSource, /monster\.hitzones/); - await readFile("website/public/static/img/mhwilds/chatacabra.png"); - await readFile("website/public/static/img/games/mhwilds/card-cover.png"); - await readFile("website/public/static/img/games/mhwilds/hero-bg.png"); -}); - -test("diablo4 data and route are available", async () => { - const games = JSON.parse(await readFile("website/public/data/games.json", "utf8")); - const affixes = JSON.parse(await readFile("website/public/data/diablo4/affixes_types.json", "utf8")); - const routeSource = await readFile("website/src/features/games/GameRoute.jsx", "utf8"); - const pageSource = await readFile("website/src/features/games/diablo4/Diablo4Page.jsx", "utf8"); - const listingSource = await readFile("website/src/features/games/diablo4/Diablo4Listing.jsx", "utf8"); - const diablo4Game = games.games.find((game) => game.id === "diablo4"); - - assert.ok(diablo4Game, "games.json must declare diablo4"); - assert.match(diablo4Game.images.cardCover, /games\/diablo4\/card-cover\.png/); - assert.match(diablo4Game.images.heroBg, /games\/diablo4\/hero-bg\.png/); - validateDiablo4Affixes(affixes); - assert.match(routeSource, /game\.id === "diablo4"/); - assert.match(pageSource, /category === "affixes"/); - assert.match(listingSource, /diablo4\.filterOptions\.affixes/); - await readFile("website/public/static/img/games/diablo4/card-cover.png"); - await readFile("website/public/static/img/games/diablo4/hero-bg.png"); - await readFile("website/public/static/img/diablo4/axe.svg"); - await readFile("website/public/static/img/diablo4/cube.svg"); - await readFile("website/public/static/img/diablo4/fire.svg"); - await readFile("website/public/static/img/diablo4/shield.svg"); - await readFile("website/public/static/img/diablo4/wing.svg"); + assert.doesNotMatch(`${mhwildsListing}\n${diablo4Listing}\n${diablo4Overview}`, /mhwilds-(home-grid|home-card|heading|title-row|layout|filters|results|grid)/); }); test("server and vite support local env configuration", async () => { diff --git a/website/src/features/games/GameFiltersPanel.jsx b/website/src/features/games/GameFiltersPanel.jsx new file mode 100644 index 0000000..b117b4a --- /dev/null +++ b/website/src/features/games/GameFiltersPanel.jsx @@ -0,0 +1,72 @@ +import { Icon } from "../../components/Icon.jsx"; + +export function GameFiltersPanel({ + title, + nameLabel = "Nom", + resetLabel = "Réinitialiser", + matchLabel = "Correspondance", + logicLabels = { and: "Et", or: "Ou" }, + filters, + selectedKey, + options, + getOptionId = (option) => option, + getOptionLabel = (option) => option, + getOptionClassName = () => "", + renderOptionMedia, + onChange +}) { + const selected = new Set(filters[selectedKey] || []); + const logicLabel = filters.logic === "or" ? logicLabels.or : logicLabels.and; + + function update(updater) { + onChange(updater(filters)); + } + + return ( +
+
+
+

Filtres

+

{title}

+
+ +
+ +
+ {matchLabel} + +
+
+ {options.map((option) => { + const optionId = getOptionId(option); + return ( + + ); + })} +
+
+ ); +} diff --git a/website/src/features/games/diablo4/Diablo4AffixCard.jsx b/website/src/features/games/diablo4/Diablo4AffixCard.jsx index 1b47d54..08c68de 100644 --- a/website/src/features/games/diablo4/Diablo4AffixCard.jsx +++ b/website/src/features/games/diablo4/Diablo4AffixCard.jsx @@ -2,7 +2,7 @@ import { getCategoryIconStyle, getCategoryLabel } from "./utils.js"; export function Diablo4AffixCard({ affix, categoryMap }) { return ( -
+

{affix.label}

diff --git a/website/src/features/games/diablo4/Diablo4Filters.jsx b/website/src/features/games/diablo4/Diablo4Filters.jsx index bfa1978..dcf2406 100644 --- a/website/src/features/games/diablo4/Diablo4Filters.jsx +++ b/website/src/features/games/diablo4/Diablo4Filters.jsx @@ -1,58 +1,18 @@ -import { Icon } from "../../../components/Icon.jsx"; +import { GameFiltersPanel } from "../GameFiltersPanel.jsx"; import { getCategoryIconStyle, getCategoryId, getCategoryLabel } from "./utils.js"; export function Diablo4Filters({ options, filters, setFilters }) { - const selected = new Set(filters.categories || []); - const logicLabel = filters.logic === "or" ? "Ou" : "Et"; - return ( -
-
-
-

Filtres

-

Catégories

-
- -
- -
- Correspondance - -
-
- {options.map((option) => ( - - ))} -
-
+ `diablo4-filter-chip tone-${option.tone || "default"}`} + renderOptionMedia={(option) =>