mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-09 07:59:32 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2d6eaa2e0 | ||
|
|
fa0b579c26 |
@@ -55,6 +55,13 @@ export class HassRouterPage extends ReactiveElement {
|
||||
|
||||
private _currentLoadProm?: Promise<void>;
|
||||
|
||||
// True while a route change is loading and the outgoing panel (or a loading
|
||||
// screen) is still shown, waiting to be replaced. While true we don't forward
|
||||
// property updates, because they are meant for the incoming panel. It stays
|
||||
// false when the new panel is shown immediately (no loading screen), so that
|
||||
// panel keeps receiving updates while its module finishes loading.
|
||||
private _replacingPanel = false;
|
||||
|
||||
private _panelReady = new PanelReady();
|
||||
|
||||
private _cache = {};
|
||||
@@ -79,9 +86,9 @@ export class HassRouterPage extends ReactiveElement {
|
||||
}
|
||||
|
||||
if (!changedProps.has("route")) {
|
||||
// Do not update if we have a currentLoadProm, because that means
|
||||
// that there is still an old panel shown and we're moving to a new one.
|
||||
if (this.lastChild && !this._currentLoadProm) {
|
||||
// Skip while the outgoing panel is still shown for a pending route
|
||||
// change; the update is meant for the incoming panel, not this one.
|
||||
if (this.lastChild && !this._replacingPanel) {
|
||||
this.updatePageEl(this.lastChild, changedProps);
|
||||
}
|
||||
return;
|
||||
@@ -185,9 +192,15 @@ export class HassRouterPage extends ReactiveElement {
|
||||
// It will be automatically upgraded when loading done.
|
||||
if (!routerOptions.showLoading) {
|
||||
const loadComplete = () => {
|
||||
this._currentLoadProm = undefined;
|
||||
// Ignore a stale load that resolves after a newer navigation took over.
|
||||
if (this._currentPage === newPage) {
|
||||
this._currentLoadProm = undefined;
|
||||
}
|
||||
};
|
||||
this._currentLoadProm = loadProm.then(loadComplete, loadComplete);
|
||||
// The new panel is shown right away, so keep forwarding updates to it
|
||||
// while its module loads.
|
||||
this._replacingPanel = false;
|
||||
this._createPanel(routerOptions, newPage, routeOptions);
|
||||
return;
|
||||
}
|
||||
@@ -195,6 +208,9 @@ export class HassRouterPage extends ReactiveElement {
|
||||
// We are only going to show the loading screen after some time.
|
||||
// That way we won't have a double fast flash on fast connections.
|
||||
let created = false;
|
||||
// The outgoing panel stays shown until the new one has loaded; don't
|
||||
// forward updates to it in the meantime.
|
||||
this._replacingPanel = true;
|
||||
|
||||
this._showLoadingScreenTimeout = window.setTimeout(() => {
|
||||
if (created || this._currentPage !== newPage) {
|
||||
@@ -210,11 +226,11 @@ export class HassRouterPage extends ReactiveElement {
|
||||
|
||||
this._currentLoadProm = loadProm.then(
|
||||
() => {
|
||||
this._currentLoadProm = undefined;
|
||||
// Check if we're still trying to show the same page.
|
||||
// Ignore a stale load that resolves after a newer navigation took over.
|
||||
if (this._currentPage !== newPage) {
|
||||
return;
|
||||
}
|
||||
this._currentLoadProm = undefined;
|
||||
|
||||
created = true;
|
||||
this._createPanel(
|
||||
@@ -223,9 +239,14 @@ export class HassRouterPage extends ReactiveElement {
|
||||
// @ts-ignore TS forgot this is not a string.
|
||||
routeOptions
|
||||
);
|
||||
// The new panel is now shown; resume forwarding updates to it.
|
||||
this._replacingPanel = false;
|
||||
},
|
||||
() => {
|
||||
this._currentLoadProm = undefined;
|
||||
if (this._currentPage === newPage) {
|
||||
this._currentLoadProm = undefined;
|
||||
this._replacingPanel = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { state } from "lit/decorators";
|
||||
import { property, state } from "lit/decorators";
|
||||
import type { RouterOptions } from "../../src/layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../src/layouts/hass-router-page";
|
||||
import { panelIsReady } from "../../src/layouts/panel-ready";
|
||||
@@ -36,9 +36,99 @@ const loadedPanelImport = new Promise<void>((resolve) => {
|
||||
|
||||
class LoadedPanel extends HTMLElement {}
|
||||
|
||||
let resolvePropPanelImport: () => void;
|
||||
const propPanelImport = new Promise<void>((resolve) => {
|
||||
resolvePropPanelImport = resolve;
|
||||
});
|
||||
|
||||
class PropPanel extends HTMLElement {
|
||||
public value = 0;
|
||||
}
|
||||
|
||||
// A router that shows its panel immediately (no `showLoading`) and forwards a
|
||||
// property to it on every update, mirroring how the config and profile panels
|
||||
// forward `hass` to their child sections.
|
||||
class PropRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public value = 0;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
routes: {
|
||||
slow: { tag: "test-prop-panel", load: () => propPanelImport },
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el: PropPanel) {
|
||||
el.value = this.value;
|
||||
}
|
||||
}
|
||||
|
||||
let resolveSecondPanelImport: () => void;
|
||||
const secondPanelImport = new Promise<void>((resolve) => {
|
||||
resolveSecondPanelImport = resolve;
|
||||
});
|
||||
|
||||
class SwapFirstPanel extends HTMLElement {
|
||||
public value = 0;
|
||||
}
|
||||
|
||||
class SwapSecondPanel extends HTMLElement {
|
||||
public value = 0;
|
||||
}
|
||||
|
||||
// A router that shows a loading screen between routes, so the outgoing panel
|
||||
// stays visible while the next one loads.
|
||||
class SwapRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public value = 0;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
showLoading: true,
|
||||
routes: {
|
||||
first: { tag: "test-swap-first-panel" },
|
||||
second: { tag: "test-swap-second-panel", load: () => secondPanelImport },
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el: SwapFirstPanel | SwapSecondPanel) {
|
||||
el.value = this.value;
|
||||
}
|
||||
}
|
||||
|
||||
let resolveRaceAlphaImport: () => void;
|
||||
const raceAlphaImport = new Promise<void>((resolve) => {
|
||||
resolveRaceAlphaImport = resolve;
|
||||
});
|
||||
|
||||
let resolveRaceBetaImport: () => void;
|
||||
const raceBetaImport = new Promise<void>((resolve) => {
|
||||
resolveRaceBetaImport = resolve;
|
||||
});
|
||||
|
||||
// A router where both routes have to load their module, used to check that a
|
||||
// stale load resolving late does not stop `pageRendered` from waiting for the
|
||||
// current route.
|
||||
class RaceRouter extends HassRouterPage {
|
||||
protected routerOptions: RouterOptions = {
|
||||
showLoading: true,
|
||||
routes: {
|
||||
alpha: { tag: "test-race-alpha-panel", load: () => raceAlphaImport },
|
||||
beta: { tag: "test-race-beta-panel", load: () => raceBetaImport },
|
||||
},
|
||||
};
|
||||
|
||||
public get rendered() {
|
||||
return this.pageRendered;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("test-router", TestRouter);
|
||||
customElements.define("test-immediate-panel", ImmediatePanel);
|
||||
customElements.define("test-deferred-panel", DeferredPanel);
|
||||
customElements.define("test-prop-router", PropRouter);
|
||||
customElements.define("test-prop-panel", PropPanel);
|
||||
customElements.define("test-swap-router", SwapRouter);
|
||||
customElements.define("test-swap-first-panel", SwapFirstPanel);
|
||||
customElements.define("test-swap-second-panel", SwapSecondPanel);
|
||||
customElements.define("test-race-router", RaceRouter);
|
||||
|
||||
let router: TestRouter | undefined;
|
||||
|
||||
@@ -101,11 +191,120 @@ describe("HassRouterPage panel readiness", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("HassRouterPage update propagation during load", () => {
|
||||
it("forwards updates to a panel shown while its module is still loading", async () => {
|
||||
const element = document.createElement("test-prop-router") as PropRouter;
|
||||
element.route = { prefix: "", path: "/slow" };
|
||||
document.body.append(element);
|
||||
await element.updateComplete;
|
||||
|
||||
const panel = element.lastElementChild as PropPanel;
|
||||
expect(panel).toBeInstanceOf(PropPanel);
|
||||
expect(panel.value).toBe(0);
|
||||
|
||||
// A new value (e.g. an updated `hass` carrying freshly loaded fragment
|
||||
// translations) arrives while the panel module is still loading. The panel
|
||||
// is already shown, so the update must reach it right away rather than
|
||||
// being skipped as if an outgoing panel were still being replaced.
|
||||
element.value = 1;
|
||||
await element.updateComplete;
|
||||
expect(panel.value).toBe(1);
|
||||
|
||||
// Finishing the load must not lose the value it received in the meantime.
|
||||
resolvePropPanelImport();
|
||||
await propPanelImport;
|
||||
await element.updateComplete;
|
||||
expect(panel.value).toBe(1);
|
||||
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it("does not forward updates to the outgoing panel while a new one loads", async () => {
|
||||
const element = document.createElement("test-swap-router") as SwapRouter;
|
||||
element.route = { prefix: "", path: "/first" };
|
||||
document.body.append(element);
|
||||
// A router with a loading screen creates the panel once its load resolves.
|
||||
await vi.waitFor(() => {
|
||||
expect(element.lastElementChild).toBeInstanceOf(SwapFirstPanel);
|
||||
});
|
||||
const first = element.lastElementChild as SwapFirstPanel;
|
||||
expect(first.value).toBe(0);
|
||||
|
||||
// Navigate to a page whose module is still loading. The first panel stays
|
||||
// shown in the meantime (the loading screen only appears after a delay).
|
||||
element.route = { prefix: "", path: "/second" };
|
||||
await element.updateComplete;
|
||||
expect(element.lastElementChild).toBe(first);
|
||||
|
||||
// An update now is meant for the incoming panel, so it must not leak to the
|
||||
// outgoing one that is about to be replaced.
|
||||
element.value = 1;
|
||||
await element.updateComplete;
|
||||
expect(first.value).toBe(0);
|
||||
|
||||
// Once the new panel is shown, updates resume normally.
|
||||
resolveSecondPanelImport();
|
||||
await vi.waitFor(() => {
|
||||
expect(element.lastElementChild).toBeInstanceOf(SwapSecondPanel);
|
||||
});
|
||||
const second = element.lastElementChild as SwapSecondPanel;
|
||||
element.value = 2;
|
||||
await element.updateComplete;
|
||||
expect(second.value).toBe(2);
|
||||
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it("keeps waiting for the current panel's load when a stale load resolves", async () => {
|
||||
const element = document.createElement("test-race-router") as RaceRouter;
|
||||
element.route = { prefix: "", path: "/alpha" };
|
||||
document.body.append(element);
|
||||
await element.updateComplete;
|
||||
|
||||
// Navigate to beta while alpha's module is still loading.
|
||||
element.route = { prefix: "", path: "/beta" };
|
||||
await element.updateComplete;
|
||||
|
||||
// The stale alpha load resolves late. It must not clear the tracking for
|
||||
// the current (beta) load, or `pageRendered` would resolve too early.
|
||||
resolveRaceAlphaImport();
|
||||
customElements.define(
|
||||
"test-race-alpha-panel",
|
||||
class extends HTMLElement {}
|
||||
);
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
|
||||
let betaReady = false;
|
||||
element.rendered.then(() => {
|
||||
betaReady = true;
|
||||
});
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
expect(betaReady).toBe(false);
|
||||
|
||||
// Beta finishing loading resolves it.
|
||||
resolveRaceBetaImport();
|
||||
customElements.define("test-race-beta-panel", class extends HTMLElement {});
|
||||
await expect(element.rendered).resolves.toBeUndefined();
|
||||
|
||||
element.remove();
|
||||
});
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"test-router": TestRouter;
|
||||
"test-immediate-panel": ImmediatePanel;
|
||||
"test-deferred-panel": DeferredPanel;
|
||||
"test-loaded-panel": LoadedPanel;
|
||||
"test-prop-router": PropRouter;
|
||||
"test-prop-panel": PropPanel;
|
||||
"test-swap-router": SwapRouter;
|
||||
"test-swap-first-panel": SwapFirstPanel;
|
||||
"test-swap-second-panel": SwapSecondPanel;
|
||||
"test-race-router": RaceRouter;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user