mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-04 13:05:06 +01:00
2c8d6c1a02
Adds Playwright end-to-end tests covering three targets: - the demo build - a new lightweight test app exercising several scenarios (theming, admin/non-admin sidebar, panel navigation, more-info dialog) - the component gallery Includes the gulp/rspack build infra for the test app and an "E2E Tests" GitHub Actions workflow that builds each target once, shares it via artifacts, and runs the suites on Chromium and mobile Chrome. Browser install is cached and retried to avoid intermittent download stalls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
252 lines
8.8 KiB
TypeScript
252 lines
8.8 KiB
TypeScript
/**
|
|
* E2E tests for the HA test app (port 8095).
|
|
*
|
|
* Run with:
|
|
* yarn test:e2e:app
|
|
*/
|
|
import { test, expect, type Page } from "@playwright/test";
|
|
import { PANEL_TIMEOUT, QUICK_TIMEOUT, SHELL_TIMEOUT } from "./helpers";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// The test app is built with __DEMO__=true which enables hash-based routing.
|
|
// Panel paths must use hash URLs: /#/lovelace, /#/energy, etc.
|
|
// Scenario selection uses query params: /?scenario=foo (always at root).
|
|
|
|
/** Navigate to a panel (hash routing) and wait for app to initialize. */
|
|
async function goToPanel(page: Page, path: string) {
|
|
// Paths starting with /? are root-level (scenario selection); panel paths
|
|
// need to use hash routing (/#/panelname).
|
|
const url = path.startsWith("/?") ? path : `/#${path}`;
|
|
await page.goto(url);
|
|
await page.waitForSelector("ha-test", { state: "attached" });
|
|
// Wait for the app to finish initialising (hassConnected sets panels)
|
|
await page.waitForFunction(() => Boolean((window as any).__mockHass));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// App shell
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("App shell", () => {
|
|
test("page loads and ha-test element mounts", async ({ page }) => {
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (e) => errors.push(e.message));
|
|
|
|
await goToPanel(page, "/");
|
|
|
|
await expect(page.locator("ha-test")).toBeAttached();
|
|
expect(errors).toHaveLength(0);
|
|
});
|
|
|
|
test("sidebar renders with expected panels", async ({ page }) => {
|
|
await goToPanel(page, "/lovelace");
|
|
|
|
// Regular panels use #sidebar-panel-{urlPath} inside ha-sidebar's shadow root
|
|
for (const urlPath of ["lovelace", "energy", "history"]) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await expect(
|
|
page.locator(
|
|
`ha-test >> home-assistant-main >> ha-sidebar >> #sidebar-panel-${urlPath}`
|
|
)
|
|
).toBeAttached();
|
|
}
|
|
// Config has its own special element with id="sidebar-config"
|
|
await expect(
|
|
page.locator(
|
|
`ha-test >> home-assistant-main >> ha-sidebar >> #sidebar-config`
|
|
)
|
|
).toBeAttached();
|
|
});
|
|
|
|
test("non-admin user does NOT see config panel in sidebar", async ({
|
|
page,
|
|
}) => {
|
|
// Navigate to a panel route so the sidebar actually renders, then apply
|
|
// the non-admin scenario via query param.
|
|
await goToPanel(page, "/?scenario=non-admin#/lovelace");
|
|
|
|
// Wait for the sidebar to mount before asserting on its contents.
|
|
await expect(
|
|
page.locator("ha-test >> home-assistant-main >> ha-sidebar")
|
|
).toBeAttached({ timeout: SHELL_TIMEOUT });
|
|
|
|
// Config panel is adminOnly — should not appear for non-admin.
|
|
const configLink = page.locator(
|
|
`ha-test >> home-assistant-main >> ha-sidebar >> #sidebar-config`
|
|
);
|
|
await expect(configLink).not.toBeAttached();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Panel navigation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("Panel navigation", () => {
|
|
test("navigates to lovelace dashboard", async ({ page }) => {
|
|
await goToPanel(page, "/lovelace");
|
|
await expect(
|
|
page.locator("ha-panel-lovelace, hui-root").first()
|
|
).toBeAttached({
|
|
timeout: PANEL_TIMEOUT,
|
|
});
|
|
});
|
|
|
|
test("navigates to energy panel", async ({ page }) => {
|
|
await goToPanel(page, "/energy");
|
|
await expect(
|
|
page.locator("ha-panel-energy, energy-view").first()
|
|
).toBeAttached({
|
|
timeout: PANEL_TIMEOUT,
|
|
});
|
|
});
|
|
|
|
test("navigates to history panel", async ({ page }) => {
|
|
await goToPanel(page, "/history");
|
|
await expect(
|
|
page.locator("ha-panel-history, history-panel").first()
|
|
).toBeAttached({
|
|
timeout: PANEL_TIMEOUT,
|
|
});
|
|
});
|
|
|
|
test("navigates to developer-tools panel", async ({ page }) => {
|
|
// Since 2026.2 developer-tools is part of the config panel
|
|
await goToPanel(page, "/config/developer-tools");
|
|
await expect(
|
|
page.locator("ha-panel-config, developer-tools-main").first()
|
|
).toBeAttached({ timeout: PANEL_TIMEOUT });
|
|
});
|
|
|
|
test("navigates to profile panel", async ({ page }) => {
|
|
await goToPanel(page, "/profile");
|
|
await expect(
|
|
page.locator("ha-panel-profile, ha-config-user-profile").first()
|
|
).toBeAttached({ timeout: PANEL_TIMEOUT });
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Lovelace
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("Lovelace dashboard", () => {
|
|
test("renders cards", async ({ page }) => {
|
|
await goToPanel(page, "/lovelace");
|
|
// At least one card should appear
|
|
await expect(page.locator("hui-card, hui-tile-card").first()).toBeAttached({
|
|
timeout: PANEL_TIMEOUT,
|
|
});
|
|
});
|
|
|
|
test("admin user sees edit button", async ({ page }) => {
|
|
await goToPanel(page, "/lovelace");
|
|
// The edit FAB / menu button is present for admins
|
|
await expect(
|
|
page.locator("[data-testid='edit-mode-button'], ha-menu-button")
|
|
).toBeAttached({ timeout: QUICK_TIMEOUT });
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// More-info dialog (light)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("Light more-info dialog", () => {
|
|
test("opens more-info dialog for a light entity", async ({ page }) => {
|
|
// The light-more-info scenario seeds light.test_light synchronously.
|
|
await goToPanel(page, "/?scenario=light-more-info#/lovelace");
|
|
|
|
// Fire the standard hass-more-info event from the app root. The HA shell
|
|
// listens for this and opens ha-more-info-dialog via its dialog manager.
|
|
await page.evaluate(() => {
|
|
const el = document.querySelector("ha-test");
|
|
el?.dispatchEvent(
|
|
new CustomEvent("hass-more-info", {
|
|
detail: { entityId: "light.test_light" },
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
});
|
|
|
|
const dialog = page.locator("ha-more-info-dialog");
|
|
await expect(dialog).toBeAttached({ timeout: SHELL_TIMEOUT });
|
|
|
|
// Confirm it actually rendered our entity, not a generic empty dialog.
|
|
await expect(dialog.locator("span.title")).toContainText("Test Light", {
|
|
timeout: QUICK_TIMEOUT,
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Theming
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("Theming", () => {
|
|
test("dark theme sets darkMode flag", async ({ page }) => {
|
|
await goToPanel(page, "/?scenario=dark-theme#/lovelace");
|
|
|
|
// The dark-theme scenario sets selectedTheme.dark = true, which causes
|
|
// _applyTheme() to set themes.darkMode = true on the element.
|
|
await page.waitForFunction(
|
|
() =>
|
|
(document.querySelector("ha-test") as any)?.hass?.themes?.darkMode ===
|
|
true,
|
|
{ timeout: QUICK_TIMEOUT }
|
|
);
|
|
});
|
|
|
|
test("custom theme applies CSS variables", async ({ page }) => {
|
|
await goToPanel(page, "/?scenario=custom-theme#/lovelace");
|
|
|
|
// The custom-theme scenario sets --primary-color to #e91e63. Wait until
|
|
// _applyTheme has propagated the value to <html> before reading it — the
|
|
// scenario fires before hassConnected, but the variable lands on :root in
|
|
// the same tick mockTheme is called.
|
|
await page.waitForFunction(
|
|
() =>
|
|
getComputedStyle(document.documentElement)
|
|
.getPropertyValue("--primary-color")
|
|
.trim() !== "",
|
|
{ timeout: QUICK_TIMEOUT }
|
|
);
|
|
|
|
const primaryColor = await page.evaluate(() =>
|
|
getComputedStyle(document.documentElement)
|
|
.getPropertyValue("--primary-color")
|
|
.trim()
|
|
);
|
|
// Compare normalised — some browsers serialise as rgb().
|
|
expect(primaryColor.toLowerCase()).toMatch(
|
|
/#e91e63|rgb\(233,\s*30,\s*99\)/
|
|
);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Config panel
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("Config panel", () => {
|
|
test("config panel loads without JS errors", async ({ page }) => {
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (e) => errors.push(e.message));
|
|
|
|
await goToPanel(page, "/config");
|
|
await expect(
|
|
page.locator("ha-panel-config, ha-config-dashboard").first()
|
|
).toBeAttached({ timeout: PANEL_TIMEOUT + 5_000 });
|
|
|
|
// Filter known pre-existing errors from vendor code
|
|
const realErrors = errors.filter(
|
|
(e) => !e.includes("ResizeObserver") && !e.includes("Non-Error")
|
|
);
|
|
expect(realErrors).toHaveLength(0);
|
|
});
|
|
});
|