Files
stemdeckapp--stemdeck/tests/js/notifications-resolve.test.mjs
T
Tha.Les 306f2ce913 Portable Windows data dir + auto-clear resolved failure notifications (#402)
* Redirect Windows portable zip cache data to data/ next to the exe

FFmpeg, Demucs models, config, and logs currently write to %LOCALAPPDATA%
regardless of where the zip is extracted, not to the data/ folder the README
already describes. A portable.txt marker, shipped in every future Windows zip,
switches local_data_dir() to the exe-relative data/ folder that packaging
already stages.

Jobs/library data is deliberately left untouched: it stays on its existing
default (~/Documents/StemDeck) and remains relocatable via the existing
Settings -> StemData location picker (#354). Defaulting it into the
exe-adjacent folder was the design in an earlier attempt at this fix, and was
reverted -- that folder is exactly what a user deletes or overwrites thinking
it's disposable.

Fixes #399

* Auto-clear failure notifications once they're resolved

Failure notifications (import/playback/export/update) persist until manually
dismissed, deliberately, from #359 -- so a crash or reload doesn't lose the
evidence needed for a bug report. This adds a second, independent trigger on
top without touching that: a notification also clears once the thing it was
about is actually resolved, while still surviving a plain reload in the
meantime.

- import: clears when a re-import supersedes the failed track, or when the
  track is trashed/purged
- playback: clears when the same track plays back successfully
- export: clears when the same track exports successfully (jobId is
  snapshotted at click time, not read live at settle time, since settling can
  take up to EXPORT_BUSY_MAX_MS and the user may have switched tracks by
  then); log export clears separately, keyed by kind since it has no jobId
- update: clears on the next successful check, which in practice only happens
  on the next app start -- checkForUpdate() has no periodic re-check today

Fixes #401

---------

Co-authored-by: Thales <>
2026-08-21 01:00:21 +01:00

91 lines
3.6 KiB
JavaScript

// Regression test for #401: a failure notification should auto-clear once
// whatever it was about is resolved (job superseded/removed, retried
// successfully), without touching the #359 behavior of surviving a plain
// reload. Covers dismissFailuresByJobId and dismissFailuresByKind directly.
//
// Run: node tests/js/notifications-resolve.test.mjs
// notifyFailure/dismissFailure* touch window/document (persist -> storeSet,
// render -> DOM); stub both before import, matching wav-header.test.mjs's
// precedent. storeGet/storeSet fall through to a caught localStorage access
// under this stub, so nothing throws.
globalThis.window = {};
globalThis.document = { getElementById: () => null, querySelectorAll: () => [] };
const {
notifyFailure,
dismissFailuresByJobId,
dismissFailuresByKind,
clearFailures,
getFailures,
} = await import("../../static/js/notifications.js");
let pass = 0, fail = 0;
const check = (name, cond, detail = "") => {
if (cond) { pass++; console.log(`PASS ${name}`); }
else { fail++; console.log(`FAIL ${name}${detail ? " -- " + detail : ""}`); }
};
const kinds = () => getFailures().map((f) => f.kind).sort();
// --------------------------------------------------------------------------
// dismissFailuresByJobId
// --------------------------------------------------------------------------
clearFailures();
notifyFailure({ kind: "import", message: "import broke", context: { jobId: "job-1" } });
notifyFailure({ kind: "playback", message: "playback broke", context: { jobId: "job-1" } });
notifyFailure({ kind: "import", message: "other track broke", context: { jobId: "job-2" } });
dismissFailuresByJobId("job-1", "playback");
check(
"by jobId+kind clears only the matching kind for that id",
JSON.stringify(kinds()) === JSON.stringify(["import", "import"]),
JSON.stringify(kinds()),
);
dismissFailuresByJobId("job-1");
check(
"by jobId (no kind) clears every kind for that id",
JSON.stringify(kinds()) === JSON.stringify(["import"]),
JSON.stringify(kinds()),
);
check("unrelated job's failure survives", getFailures().some((f) => f.context?.jobId === "job-2"));
clearFailures();
dismissFailuresByJobId(null);
check("dismissFailuresByJobId(null) is a no-op, does not throw", getFailures().length === 0);
// --------------------------------------------------------------------------
// dismissFailuresByKind
// --------------------------------------------------------------------------
clearFailures();
notifyFailure({ kind: "update", message: "check failed" });
notifyFailure({ kind: "export", message: "log export failed" }); // no jobId
notifyFailure({ kind: "export", message: "track export failed", context: { jobId: "job-3" } });
dismissFailuresByKind("export");
check(
"by kind never touches a record that carries a jobId",
getFailures().some((f) => f.kind === "export" && f.context?.jobId === "job-3"),
);
check(
"by kind clears the id-less record of that kind",
!getFailures().some((f) => f.kind === "export" && !f.context?.jobId),
);
check("unrelated kind survives", getFailures().some((f) => f.kind === "update"));
// --------------------------------------------------------------------------
// No-op guard: nothing to clear leaves the list untouched
// --------------------------------------------------------------------------
clearFailures();
notifyFailure({ kind: "import", message: "still broken", context: { jobId: "job-4" } });
dismissFailuresByJobId("job-does-not-exist");
dismissFailuresByKind("playback");
check("no-op dismisses leave unrelated failures in place", getFailures().length === 1);
console.log(`\n${pass}/${pass + fail} checks passed`);
process.exit(fail ? 1 : 0);