Compare commits

...

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] d059777cc3 Address panel readiness review feedback
Co-authored-by: timmo001 <28114703+timmo001@users.noreply.github.com>
2026-08-04 08:46:56 +00:00
Aidan Timson 6e8a8fda6a Consolidate panel readiness tests 2026-08-04 09:00:19 +01:00
Aidan Timson ea2e959d06 Wait for Integrations panel readiness 2026-08-04 09:00:03 +01:00
6 changed files with 134 additions and 81 deletions
+1
View File
@@ -106,6 +106,7 @@ class HaPanelConfig extends HassRouterPage {
integrations: {
tag: "ha-config-integrations",
load: () => import("./integrations/ha-config-integrations"),
waitForReady: true,
},
labels: {
tag: "ha-config-labels",
@@ -1,4 +1,5 @@
import { mdiFilterVariant, mdiPlus } from "@mdi/js";
import { consume } from "@lit/context";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
@@ -54,6 +55,10 @@ import type { ImprovDiscoveredDevice } from "../../../external_app/external_mess
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import type { HassTabsSubpage } from "../../../layouts/hass-tabs-subpage";
import {
childPanelReadyContext,
type RegisterChildPanelReady,
} from "../../../layouts/panel-ready";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
@@ -168,6 +173,17 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
@query("hass-tabs-subpage") private _tabsSubpage?: HassTabsSubpage;
private _resolveInitialRender?: () => void;
private _initialRenderComplete = new Promise<void>((resolve) => {
this._resolveInitialRender = resolve;
});
private _childReadyRegistered = false;
@consume({ context: childPanelReadyContext, subscribe: true })
private _registerChildPanelReady?: RegisterChildPanelReady;
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener(
@@ -388,6 +404,10 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
protected updated(changed: PropertyValues<this>) {
super.updated(changed);
if (!this._childReadyRegistered && this._registerChildPanelReady) {
this._registerChildPanelReady(this._initialRenderComplete);
this._childReadyRegistered = true;
}
if (changed.has("route")) {
this._handleRouteChanged();
}
@@ -415,6 +435,9 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
}
if (this.configEntries && this.configEntriesInProgress) {
this._resolveInitialRender?.();
this._resolveInitialRender = undefined;
const activeElement = deepActiveElement();
if (
@@ -11,6 +11,7 @@ import {
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
import { domainToName } from "../../../data/integration";
import "../../../layouts/hass-loading-screen";
import { ChildPanelReady } from "../../../layouts/panel-ready";
import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
@@ -70,6 +71,11 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
private _loadTranslationsPromise?: Promise<LocalizeFunc>;
public constructor() {
super();
new ChildPanelReady(this);
}
public hassSubscribe() {
return [
subscribeConfigEntries(
+82 -81
View File
@@ -4,7 +4,7 @@
* Run with:
* yarn test:e2e:app
*/
import { test, expect, type Page } from "@playwright/test";
import { test, expect } from "@playwright/test";
import {
appSidebar,
appSidebarConfig,
@@ -161,65 +161,100 @@ test.describe("Quick search", () => {
defineRouteSmokeTests(appRouteSmokeGroups);
const assertInitialReadiness = async (
page: Page,
options: {
test("keeps the launch screen until initial panel content renders", async ({
page,
}) => {
const cases: {
name: string;
path: string;
loadingSelector: string;
resolver:
"rejectMediaBrowse" | "resolveCalendarRegistry" | "resolveMediaBrowse";
readySelector: string;
}
) => {
await goToPanel(page, options.path);
const launchScreen = page.locator("#ha-launch-screen");
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(page.locator(options.loadingSelector)).toBeAttached({
timeout: QUICK_TIMEOUT,
});
await page.evaluate((resolver) => window[resolver]?.(), options.resolver);
await expect(page.locator(options.readySelector)).toBeAttached({
timeout: PANEL_TIMEOUT,
});
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
};
test.describe("Initial readiness", () => {
test("keeps the launch screen until calendar content renders", async ({
page,
}) => {
await assertInitialReadiness(page, {
resolvers: (
| "rejectMediaBrowse"
| "resolveCalendarRegistry"
| "resolveConfigEntries"
| "resolveConfigEntriesInProgress"
| "resolveGeneratedDashboard"
| "resolveLovelaceConfig"
| "resolveMediaBrowse"
)[];
}[] = [
{
name: "calendar",
path: "/?scenario=delayed-calendar#/calendar",
loadingSelector: "ha-panel-calendar ha-spinner",
resolver: "resolveCalendarRegistry",
readySelector: "ha-full-calendar",
});
});
test("keeps the launch screen until media content renders", async ({
page,
}) => {
await assertInitialReadiness(page, {
resolvers: ["resolveCalendarRegistry"],
},
{
name: "media browser",
path: "/?scenario=delayed-media-browse#/media-browser/browser",
loadingSelector: "ha-media-player-browse > ha-spinner",
resolver: "resolveMediaBrowse",
readySelector: "ha-media-player-browse .no-items",
});
});
test("keeps the launch screen until a media error renders", async ({
page,
}) => {
await assertInitialReadiness(page, {
resolvers: ["resolveMediaBrowse"],
},
{
name: "integrations",
path: "/?scenario=delayed-integrations#/config/integrations",
loadingSelector: "ha-config-integrations-dashboard hass-loading-screen",
readySelector: "ha-config-integrations-dashboard hass-tabs-subpage",
resolvers: ["resolveConfigEntries", "resolveConfigEntriesInProgress"],
},
{
name: "media browser error",
path: "/?scenario=delayed-media-browse-error#/media-browser/browser",
loadingSelector: "ha-media-player-browse > ha-spinner",
resolver: "rejectMediaBrowse",
readySelector: "ha-media-player-browse ha-alert",
resolvers: ["rejectMediaBrowse"],
},
{
name: "generated dashboard",
path: "/?scenario=delayed-generated-dashboard#/climate",
loadingSelector: "#ha-launch-screen",
readySelector: "hui-view",
resolvers: ["resolveGeneratedDashboard"],
},
{
name: "Lovelace dashboard",
path: "/?scenario=delayed-lovelace#/lovelace",
loadingSelector: "#ha-launch-screen",
readySelector: "hui-card",
resolvers: ["resolveLovelaceConfig"],
},
];
for (const readinessCase of cases) {
// eslint-disable-next-line no-await-in-loop
await test.step(readinessCase.name, async () => {
await goToPanel(page, readinessCase.path);
const launchScreen = page.locator("#ha-launch-screen");
const loadingScreen = page.locator(readinessCase.loadingSelector);
const readyContent = page.locator(readinessCase.readySelector).first();
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(loadingScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(readyContent).not.toBeAttached();
await readinessCase.resolvers.reduce(
async (previousResolver, resolver, index) => {
await previousResolver;
await page.evaluate((resolverName) => {
window[resolverName]?.();
}, resolver);
if (index < readinessCase.resolvers.length - 1) {
await expect(launchScreen).toBeAttached();
await expect(loadingScreen).toBeAttached();
await expect(readyContent).not.toBeAttached();
}
},
Promise.resolve()
);
await expect(readyContent).toBeAttached({ timeout: PANEL_TIMEOUT });
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
});
});
}
});
// ---------------------------------------------------------------------------
@@ -227,40 +262,6 @@ test.describe("Initial readiness", () => {
// ---------------------------------------------------------------------------
test.describe("Lovelace dashboard", () => {
test("keeps the launch screen until generated content renders", async ({
page,
}) => {
await goToPanel(page, "/?scenario=delayed-generated-dashboard#/climate");
const launchScreen = page.locator("#ha-launch-screen");
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(page.locator("hui-view")).not.toBeAttached();
await page.evaluate(() => window.resolveGeneratedDashboard?.());
await expect(page.locator("hui-view")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
});
test("keeps the launch screen until initial content renders", async ({
page,
}) => {
await goToPanel(page, "/?scenario=delayed-lovelace#/lovelace");
const launchScreen = page.locator("#ha-launch-screen");
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(page.locator("hui-card")).not.toBeAttached();
await page.evaluate(() => window.resolveLovelaceConfig?.());
await expect(page.locator("hui-card").first()).toBeAttached({
timeout: PANEL_TIMEOUT,
});
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
});
test("renders cards", async ({ page }) => {
await goToPanel(page, "/lovelace");
// At least one card should appear
+2
View File
@@ -94,6 +94,8 @@ declare global {
__mockHass: MockHomeAssistant;
rejectMediaBrowse?: () => void;
resolveCalendarRegistry?: () => void;
resolveConfigEntries?: () => void;
resolveConfigEntriesInProgress?: () => void;
resolveGeneratedDashboard?: () => void;
resolveLovelaceConfig?: () => void;
resolveMediaBrowse?: () => void;
+20
View File
@@ -185,6 +185,25 @@ const delayedCalendarScenario: Scenario = (hass) => {
hass.mockWS("config/entity_registry/list", () => registryPromise);
};
const delayedIntegrationsScenario: Scenario = (hass) => {
addLaunchScreen();
hass.mockWS(
"config_entries/subscribe",
(_msg, _currentHass, onChange?: (updates: unknown[]) => void) => {
window.resolveConfigEntries = () => onChange?.([]);
return () => undefined;
}
);
hass.mockWS(
"config_entries/flow/subscribe",
(_msg, _currentHass, onChange?: (updates: unknown[]) => void) => {
window.resolveConfigEntriesInProgress = () => onChange?.([]);
return () => undefined;
}
);
};
const delayedMediaBrowseScenario: Scenario = (hass) => {
addLaunchScreen();
@@ -230,6 +249,7 @@ export const scenarios: Record<string, Scenario> = {
"custom-theme": customThemeScenario,
"delayed-calendar": delayedCalendarScenario,
"delayed-generated-dashboard": delayedGeneratedDashboardScenario,
"delayed-integrations": delayedIntegrationsScenario,
"delayed-media-browse": delayedMediaBrowseScenario,
"delayed-media-browse-error": delayedMediaBrowseErrorScenario,
"light-more-info": lightMoreInfoScenario,