Merge branch 'main' into feature/e39b-machine-access

This commit is contained in:
2026-10-14 20:17:05 +02:00
6 changed files with 77 additions and 2 deletions
@@ -1,4 +1,12 @@
[
{
"version": "0.106.1",
"date": "2026-10-14",
"new": [],
"fixed": [
"Plex player: clicking outside the Settings panel closes the panel again instead of closing the player."
]
},
{
"version": "0.106.0",
"date": "2026-10-13",
@@ -1,4 +1,12 @@
[
{
"version": "0.106.1",
"date": "2026-10-14",
"new": [],
"fixed": [
"Plex lejátszó: a Beállítások panelen kívül kattintva ismét csak a panel záródik be, nem a lejátszó."
]
},
{
"version": "0.106.0",
"date": "2026-10-13",
+47
View File
@@ -236,6 +236,53 @@ describe("closeOverlaysThenNavigate", () => {
}
});
// The Plex player's ⋯ → Settings: the menu closes (its entry goes back) and the panel opens a task
// later, before that back() has landed. A push made then is the entry the landing back() takes, so
// the books said one entry stood that did not — and the panel's own close stepped back over the
// player's entry, closing the player on a click outside the panel.
it("an overlay opened while a back() is in flight pushes its entry only once that back() lands", async () => {
const landing: (() => void)[] = [];
const back = vi.spyOn(window.history, "back").mockImplementation(() => {
landing.push(() =>
window.dispatchEvent(new PopStateEvent("popstate", { state: window.history.state })),
);
});
const land = () => act(() => landing.splice(0).forEach((l) => l()));
const push = vi.spyOn(window.history, "pushState");
let panel: { unmount: () => void } | undefined;
try {
const menu = mountLayer(() => {});
await settle();
menu.unmount();
await settle();
expect(back).toHaveBeenCalledTimes(1);
push.mockClear();
panel = mountLayer(() => {});
await tick();
expect(push).not.toHaveBeenCalled(); // it would be undone by the landing back()
land();
await settle();
expect(push).toHaveBeenCalledTimes(1);
// The books are square: closing the panel reclaims its own entry, one back() and no more.
back.mockClear();
panel.unmount();
panel = undefined;
await settle();
expect(back).toHaveBeenCalledTimes(1);
} finally {
// Square the module-global books for the next test, however far this one got.
panel?.unmount();
await settle();
land();
await settle();
back.mockRestore();
push.mockRestore();
}
});
// No such overlay exists today, but a future "discard your changes?" guard must not have the page
// switched out from under it: dropping the command is the honest outcome.
it("drops the command when an overlay does not actually close", async () => {
+7 -1
View File
@@ -261,10 +261,14 @@ let listenerInstalled = false;
const syncWaiters = new Set<() => void>();
// Push/pop history entries until their count matches the live overlay count. Runs once per tick.
// A push waits while a back() of ours is still in flight: it would land on the entry just pushed and
// take it, leaving the books one entry above the history — and the next close then stepped back over
// the entry BELOW, the player's own (the Plex player's ⋯ → Settings, then a click outside the panel,
// closed the player). The popstate that lands the last back() reconciles again.
function reconcileHistory(): void {
reconcileScheduled = false;
const want = overlayStack.length;
while (historyDepth < want) {
while (historyDepth < want && pendingProgrammatic === 0) {
historyDepth++;
window.history.pushState({ ...window.history.state, _ov: nextOverlayToken() }, "");
}
@@ -294,6 +298,8 @@ function ensureOverlayListener(): void {
window.addEventListener("popstate", () => {
if (pendingProgrammatic > 0) {
pendingProgrammatic--; // our own back(): consume and ignore
// The last one landed: an overlay that opened while it was in flight still owes its entry.
if (pendingProgrammatic === 0 && historyDepth < overlayStack.length) scheduleReconcile();
return;
}
// Genuine user Back: the browser already dropped one overlay entry, so close the topmost