mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
fix(download): warn before the tab closes mid-download
Bunny and direct downloads are pulled through fetch() so we can save them under our own filename. The browser does not treat that as a download, so closing the tab discarded everything received so far without a word. Register a reference counted beforeunload guard while those transfers are in flight, and while a project manifest is being pulled file by file. Browser owned downloads (same-origin proxy, the over-10GB fallback, asset downloads) survive a tab close on their own and stay unguarded.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi, type MockInstance } from 'vitest';
|
||||
import { beginUnloadGuard, unloadGuardCount } from '@/lib/client/unload-guard';
|
||||
|
||||
let addSpy: MockInstance<typeof window.addEventListener>;
|
||||
let removeSpy: MockInstance<typeof window.removeEventListener>;
|
||||
|
||||
/** True when something cancelled the unload, which is what makes the browser
|
||||
* show its "leave site?" dialog. */
|
||||
function unloadWasBlocked(): boolean {
|
||||
const event = new Event('beforeunload', { cancelable: true });
|
||||
window.dispatchEvent(event);
|
||||
return event.defaultPrevented;
|
||||
}
|
||||
|
||||
function listenerCalls(spy: MockInstance<typeof window.addEventListener>): number {
|
||||
return spy.mock.calls.filter(([type]) => type === 'beforeunload').length;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
addSpy = vi.spyOn(window, 'addEventListener');
|
||||
removeSpy = vi.spyOn(window, 'removeEventListener');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
// A leaked guard would block the unload for the rest of the session, so a
|
||||
// test that leaves one behind must fail here rather than in the next test.
|
||||
expect(unloadGuardCount()).toBe(0);
|
||||
});
|
||||
|
||||
describe('beginUnloadGuard', () => {
|
||||
it('cancels the unload while a download holds it', () => {
|
||||
const release = beginUnloadGuard();
|
||||
const blocked = unloadWasBlocked();
|
||||
release();
|
||||
|
||||
expect(blocked).toBe(true);
|
||||
});
|
||||
|
||||
it('lets the page go once the download is released', () => {
|
||||
beginUnloadGuard()();
|
||||
|
||||
expect(unloadWasBlocked()).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the listener until the last concurrent download releases', () => {
|
||||
const releaseA = beginUnloadGuard();
|
||||
const releaseB = beginUnloadGuard();
|
||||
expect(listenerCalls(addSpy)).toBe(1);
|
||||
|
||||
releaseA();
|
||||
const stillBlocked = unloadWasBlocked();
|
||||
releaseB();
|
||||
|
||||
expect(stillBlocked).toBe(true);
|
||||
expect(listenerCalls(removeSpy)).toBe(1);
|
||||
expect(unloadWasBlocked()).toBe(false);
|
||||
});
|
||||
|
||||
// The download hook releases from a finally block, and a caller could hold
|
||||
// the returned function longer; a double release must not drop a guard
|
||||
// another download still holds.
|
||||
it('ignores a second release', () => {
|
||||
const releaseA = beginUnloadGuard();
|
||||
const releaseB = beginUnloadGuard();
|
||||
|
||||
releaseA();
|
||||
releaseA();
|
||||
const stillBlocked = unloadWasBlocked();
|
||||
releaseB();
|
||||
|
||||
expect(stillBlocked).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user