Add local automatic backups and Aniimo regions lists
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
This commit is contained in:
parent
2255de94a7
commit
0a3bb4dffd
24 changed files with 908 additions and 16 deletions
|
|
@ -112,7 +112,9 @@ test("grouping preserves order and contains only variants that pass all filters"
|
|||
test("loader passes cancellation to all requests and reports failed responses", async (context) => {
|
||||
const calls = [];
|
||||
const controller = new AbortController();
|
||||
const bodies = [aniimos, fr, en];
|
||||
const listIndex = await readJson("aniimo/lists/index.json");
|
||||
const regions = await readJson("aniimo/lists/regions.json");
|
||||
const bodies = [aniimos, fr, en, listIndex, regions];
|
||||
context.mock.method(globalThis, "fetch", async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
const body = bodies[calls.length - 1];
|
||||
|
|
@ -120,7 +122,10 @@ test("loader passes cancellation to all requests and reports failed responses",
|
|||
});
|
||||
const result = await loadAniimoData(controller.signal);
|
||||
assert.equal(result.variants.length, data.variants.length);
|
||||
assert.equal(calls.length, 3);
|
||||
assert.equal(result.lists.length, 1);
|
||||
assert.equal(result.lists[0].id, "regions");
|
||||
assert.equal(result.lists[0].itemCount, 14);
|
||||
assert.equal(calls.length, 5);
|
||||
assert.ok(calls.every((call) => call.options.signal === controller.signal));
|
||||
globalThis.fetch.mock.mockImplementation(async () => ({ ok: false }));
|
||||
await assert.rejects(loadAniimoData(controller.signal), /indisponibles/);
|
||||
|
|
|
|||
128
tests/auto-backup.test.mjs
Normal file
128
tests/auto-backup.test.mjs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
// Rôle : vérifie les règles de nommage, d’écriture et de rétention des sauvegardes locales.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { backupContentsEqual, getBackupFileName, getLocalDateKey, isFileSystemAccessSupported, saveBackup } from "../website/src/features/toolboxes/storage/autoBackup.js";
|
||||
|
||||
class FakeDirectory {
|
||||
constructor(names = []) {
|
||||
this.files = new Map(names.map((name) => [name, "old"]));
|
||||
this.directories = new Set();
|
||||
this.permission = "granted";
|
||||
this.failCleanup = false;
|
||||
this.failWrite = false;
|
||||
}
|
||||
|
||||
async queryPermission() { return this.permission; }
|
||||
|
||||
async *values() {
|
||||
for (const name of this.files.keys()) yield { kind: "file", name };
|
||||
for (const name of this.directories) yield { kind: "directory", name };
|
||||
}
|
||||
|
||||
async getFileHandle(name, { create = false } = {}) {
|
||||
if (create && !this.files.has(name)) this.files.set(name, "");
|
||||
const directory = this;
|
||||
return {
|
||||
async getFile() {
|
||||
return { text: async () => directory.files.get(name) || "" };
|
||||
},
|
||||
async createWritable() {
|
||||
if (directory.failWrite) throw new Error("write failed");
|
||||
return {
|
||||
async write(contents) { directory.files.set(name, contents); },
|
||||
async close() {},
|
||||
async abort() {}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async removeEntry(name) {
|
||||
if (this.failCleanup) throw new Error("cleanup failed");
|
||||
this.files.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
test("feature detection does not rely on the user agent", () => {
|
||||
assert.equal(isFileSystemAccessSupported({ showDirectoryPicker() {} }), true);
|
||||
assert.equal(isFileSystemAccessSupported({}), false);
|
||||
});
|
||||
|
||||
test("backup names use the browser local date", () => {
|
||||
const date = new Date(2026, 8, 24, 0, 5);
|
||||
assert.equal(getLocalDateKey(date), "2026-09-24");
|
||||
assert.equal(getBackupFileName(date), "sokkog-global-2026-09-24.json");
|
||||
});
|
||||
|
||||
test("automatic backup skips an existing day and creates the next day", async () => {
|
||||
const directory = new FakeDirectory(["sokkog-global-2026-09-23.json"]);
|
||||
let generated = 0;
|
||||
const generateJson = async () => { generated += 1; return "{}"; };
|
||||
const first = await saveBackup({ directoryHandle: directory, generateJson, date: new Date(2026, 8, 23) });
|
||||
const second = await saveBackup({ directoryHandle: directory, generateJson, date: new Date(2026, 8, 24) });
|
||||
assert.equal(first.created, false);
|
||||
assert.equal(second.created, true);
|
||||
assert.equal(generated, 1);
|
||||
assert.ok(directory.files.has("sokkog-global-2026-09-24.json"));
|
||||
});
|
||||
|
||||
test("automatic backup skips a new day when the export is unchanged", async () => {
|
||||
const previous = JSON.stringify({ version: 1, exportedAt: "2026-09-23T08:00:00.000Z", toolboxes: [], modules: {}, links: {} });
|
||||
const directory = new FakeDirectory(["sokkog-global-2026-09-23.json"]);
|
||||
directory.files.set("sokkog-global-2026-09-23.json", previous);
|
||||
const current = JSON.stringify({ version: 1, exportedAt: "2026-09-24T08:00:00.000Z", toolboxes: [], modules: {}, links: {} });
|
||||
const result = await saveBackup({ directoryHandle: directory, generateJson: async () => current, date: new Date(2026, 8, 24) });
|
||||
assert.equal(backupContentsEqual(previous, current), true);
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.created, false);
|
||||
assert.equal(result.unchanged, true);
|
||||
assert.equal(directory.files.has("sokkog-global-2026-09-24.json"), false);
|
||||
});
|
||||
|
||||
test("manual backup replaces the day and retains ten non-consecutive versions", async () => {
|
||||
const names = [
|
||||
"sokkog-global-2024-01-01.json", "sokkog-global-2024-06-01.json", "sokkog-global-2025-01-01.json",
|
||||
"sokkog-global-2025-03-01.json", "sokkog-global-2025-09-01.json", "sokkog-global-2026-01-01.json",
|
||||
"sokkog-global-2026-03-01.json", "sokkog-global-2026-06-01.json", "sokkog-global-2026-07-01.json",
|
||||
"sokkog-global-2026-08-01.json", "sokkog-global-2026-09-24.json", "notes.json"
|
||||
];
|
||||
const directory = new FakeDirectory(names);
|
||||
const result = await saveBackup({ directoryHandle: directory, force: true, date: new Date(2026, 8, 24), generateJson: async () => "new" });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(directory.files.get("sokkog-global-2026-09-24.json"), "new");
|
||||
assert.equal([...directory.files.keys()].filter((name) => /^sokkog-global-\d{4}-\d{2}-\d{2}\.json$/.test(name)).length, 10);
|
||||
assert.equal(directory.files.get("notes.json"), "old");
|
||||
});
|
||||
|
||||
test("permission failure prevents export and cleanup", async () => {
|
||||
const directory = new FakeDirectory();
|
||||
directory.permission = "prompt";
|
||||
let generated = false;
|
||||
const result = await saveBackup({ directoryHandle: directory, generateJson: async () => { generated = true; return "{}"; } });
|
||||
assert.equal(result.reason, "permission");
|
||||
assert.equal(generated, false);
|
||||
});
|
||||
|
||||
test("cleanup failure does not invalidate a successfully written backup", async () => {
|
||||
const directory = new FakeDirectory(Array.from({ length: 10 }, (_, index) => `sokkog-global-2020-01-${String(index + 1).padStart(2, "0")}.json`));
|
||||
directory.failCleanup = true;
|
||||
const result = await saveBackup({ directoryHandle: directory, generateJson: async () => "{}", date: new Date(2026, 8, 24) });
|
||||
assert.equal(result.ok, true);
|
||||
assert.match(result.cleanupError, /cleanup failed/);
|
||||
assert.equal(directory.files.get("sokkog-global-2026-09-24.json"), "{}");
|
||||
});
|
||||
|
||||
test("failed write does not leave a newly created backup", async () => {
|
||||
const directory = new FakeDirectory();
|
||||
directory.failWrite = true;
|
||||
const result = await saveBackup({ directoryHandle: directory, generateJson: async () => "{}", date: new Date(2026, 8, 24) });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(directory.files.has("sokkog-global-2026-09-24.json"), false);
|
||||
});
|
||||
|
||||
test("export failure does not create a partial backup", async () => {
|
||||
const directory = new FakeDirectory();
|
||||
const result = await saveBackup({ directoryHandle: directory, generateJson: async () => { throw new Error("export failed"); }, date: new Date(2026, 8, 24) });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(directory.files.has("sokkog-global-2026-09-24.json"), false);
|
||||
});
|
||||
|
|
@ -220,6 +220,23 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.toastPositionLabel",
|
||||
"toolboxes.toastPositionLeft",
|
||||
"toolboxes.toastPositionRight",
|
||||
"toolboxes.autoBackup.eyebrow",
|
||||
"toolboxes.autoBackup.title",
|
||||
"toolboxes.autoBackup.description",
|
||||
"toolboxes.autoBackup.compatibleLabel",
|
||||
"toolboxes.autoBackup.folderLabel",
|
||||
"toolboxes.autoBackup.enabled",
|
||||
"toolboxes.autoBackup.toggleHelp",
|
||||
"toolboxes.autoBackup.chooseFolder",
|
||||
"toolboxes.autoBackup.changeFolder",
|
||||
"toolboxes.autoBackup.reauthorize",
|
||||
"toolboxes.autoBackup.saveNow",
|
||||
"toolboxes.autoBackup.noFolder",
|
||||
"toolboxes.autoBackup.never",
|
||||
"toolboxes.autoBackup.lastBackup",
|
||||
"toolboxes.autoBackup.retention",
|
||||
"toolboxes.autoBackup.unsupported",
|
||||
"toolboxes.autoBackup.close",
|
||||
"toolboxes.summary",
|
||||
"toolboxes.storageHelp.title",
|
||||
"toolboxes.storageHelp.text",
|
||||
|
|
@ -883,6 +900,7 @@ export function validateSiteContent(site) {
|
|||
].forEach((path) => assertNonEmptyString(site, path));
|
||||
|
||||
assertStringArray(site, "toolboxes.storageHelp.items", 3);
|
||||
assertStringArray(site, "toolboxes.autoBackup.browsers", 2);
|
||||
|
||||
const librarySummaryItems = valueAt(site, "library.summaryItems");
|
||||
assert.ok(Array.isArray(librarySummaryItems), "library.summaryItems must be an array");
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ test("mhwilds and diablo pages expose expected route pieces", async () => {
|
|||
const mhwildsOverview = await readFile("website/src/features/games/mhwilds/MhwildsOverview.jsx", "utf8");
|
||||
const mhwildsListing = await readFile("website/src/features/games/mhwilds/MhwildsListing.jsx", "utf8");
|
||||
const mhwildsLists = await readFile("website/src/features/games/mhwilds/MhwildsLists.jsx", "utf8");
|
||||
const aniimoPage = await readFile("website/src/features/games/aniimo/AniimoPage.jsx", "utf8");
|
||||
const aniimoLists = await readFile("website/src/features/games/aniimo/AniimoLists.jsx", "utf8");
|
||||
const mhwildsFilters = await readFile("website/src/features/games/mhwilds/MhwildsFilters.jsx", "utf8");
|
||||
const monsterCard = await readFile("website/src/features/games/mhwilds/cards/MonsterCard.jsx", "utf8");
|
||||
const endemicCard = await readFile("website/src/features/games/mhwilds/cards/EndemicCard.jsx", "utf8");
|
||||
|
|
@ -79,6 +81,9 @@ test("mhwilds and diablo pages expose expected route pieces", async () => {
|
|||
assert.match(mhwildsLists, /export function MhwildsLists/);
|
||||
assert.match(mhwildsLists, /GameListsPage/);
|
||||
assert.match(mhwildsLists, /selectedListId/);
|
||||
assert.match(aniimoPage, /AniimoLists/);
|
||||
assert.match(aniimoPage, /games\/aniimo\/lists/);
|
||||
assert.match(aniimoLists, /GameListsPage/);
|
||||
assert.match(mhwildsFilters, /export function MhwildsFilters/);
|
||||
assert.match(mhwildsFilters, /GameFiltersPanel/);
|
||||
assert.match(monsterCard, /export function MonsterCard/);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue