mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +01:00
Add delay before revoking URL on Android (#51299)
This commit is contained in:
@@ -464,7 +464,6 @@ export class HaAutomationTrace extends LitElement {
|
||||
url,
|
||||
`trace ${this._entityId} ${this._trace!.timestamp.start}.json`
|
||||
);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
private _importTrace() {
|
||||
|
||||
@@ -806,7 +806,6 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
url,
|
||||
`zwave_js_backup_${new Date().toISOString().replace(/[:.]/g, "-")}.bin`
|
||||
);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
|
||||
@@ -479,7 +479,6 @@ export class HaScriptTrace extends LitElement {
|
||||
url,
|
||||
`trace ${this._entityId} ${this._trace!.timestamp.start}.json`
|
||||
);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
private _importTrace() {
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
// 10 seconds gives the Android WebView download listener enough time
|
||||
// to open the blob before it is revoked, while still freeing memory
|
||||
// promptly. Revoking immediately would invalidate the URL before the
|
||||
// native layer can read it.
|
||||
const BLOB_REVOKE_DELAY_MS = 10_000;
|
||||
|
||||
export const fileDownload = (href: string, filename = ""): void => {
|
||||
const element = document.createElement("a");
|
||||
element.target = "_blank";
|
||||
@@ -7,4 +13,14 @@ export const fileDownload = (href: string, filename = ""): void => {
|
||||
document.body.appendChild(element);
|
||||
element.dispatchEvent(new MouseEvent("click"));
|
||||
document.body.removeChild(element);
|
||||
|
||||
if (href.startsWith("blob:")) {
|
||||
// Revoke blob URLs after a delay on Android so the WebView download
|
||||
// listener has time to fetch the blob before it becomes invalid.
|
||||
if (window.externalApp) {
|
||||
setTimeout(() => URL.revokeObjectURL(href), BLOB_REVOKE_DELAY_MS);
|
||||
} else {
|
||||
URL.revokeObjectURL(href);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
68
test/util/file_download.test.ts
Normal file
68
test/util/file_download.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fileDownload } from "../../src/util/file_download";
|
||||
|
||||
describe("fileDownload", () => {
|
||||
let appendChildSpy: ReturnType<typeof vi.spyOn>;
|
||||
let removeChildSpy: ReturnType<typeof vi.spyOn>;
|
||||
let dispatchEventSpy: ReturnType<typeof vi.spyOn>;
|
||||
let createdElement: HTMLAnchorElement;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => undefined);
|
||||
createdElement = document.createElement("a");
|
||||
vi.spyOn(createdElement, "dispatchEvent");
|
||||
vi.spyOn(document, "createElement").mockReturnValue(createdElement);
|
||||
appendChildSpy = vi
|
||||
.spyOn(document.body, "appendChild")
|
||||
.mockImplementation((node) => node);
|
||||
removeChildSpy = vi
|
||||
.spyOn(document.body, "removeChild")
|
||||
.mockImplementation((node) => node);
|
||||
dispatchEventSpy = vi.spyOn(createdElement, "dispatchEvent");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
delete (window as any).externalApp;
|
||||
});
|
||||
|
||||
it("sets href, download, and triggers a click", () => {
|
||||
fileDownload("https://example.com/file.json", "file.json");
|
||||
|
||||
expect(createdElement.href).toBe("https://example.com/file.json");
|
||||
expect(createdElement.download).toBe("file.json");
|
||||
expect(appendChildSpy).toHaveBeenCalledWith(createdElement);
|
||||
expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(MouseEvent));
|
||||
expect(removeChildSpy).toHaveBeenCalledWith(createdElement);
|
||||
});
|
||||
|
||||
it("defaults filename to empty string", () => {
|
||||
fileDownload("https://example.com/file.json");
|
||||
expect(createdElement.download).toBe("");
|
||||
});
|
||||
|
||||
it("does not revoke non-blob URLs", () => {
|
||||
fileDownload("https://example.com/file.json", "file.json");
|
||||
vi.runAllTimers();
|
||||
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("revokes blob URLs immediately outside Android", () => {
|
||||
fileDownload("blob:http://localhost/abc-123", "file.json");
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith(
|
||||
"blob:http://localhost/abc-123"
|
||||
);
|
||||
});
|
||||
|
||||
it("revokes blob URL after delay on Android", () => {
|
||||
(window as any).externalApp = {};
|
||||
fileDownload("blob:http://localhost/abc-123", "file.json");
|
||||
vi.advanceTimersByTime(9_999);
|
||||
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith(
|
||||
"blob:http://localhost/abc-123"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user