mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-05 21:45: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>
291 lines
7.5 KiB
JavaScript
291 lines
7.5 KiB
JavaScript
// Tasks to generate entry HTML
|
|
|
|
import {
|
|
applyVersionsToRegexes,
|
|
compileRegex,
|
|
getPreUserAgentRegexes,
|
|
} from "browserslist-useragent-regexp";
|
|
import fs from "fs-extra";
|
|
import gulp from "gulp";
|
|
import { minify } from "html-minifier-terser";
|
|
import template from "lodash.template";
|
|
import { dirname, extname, resolve } from "node:path";
|
|
import { htmlMinifierOptions, terserOptions } from "../bundle.cjs";
|
|
import paths from "../paths.cjs";
|
|
|
|
// macOS companion app has no way to obtain the Safari version used by WKWebView,
|
|
// and it is not in the default user agent string. So we add an additional regex
|
|
// to serve modern based on a minimum macOS version. We take the minimum Safari
|
|
// major version from browserslist and manually map that to a supported macOS
|
|
// version. Note this assumes the user has kept Safari updated.
|
|
const HA_MACOS_REGEX =
|
|
/Home Assistant\/[\d.]+ \(.+; macOS (\d+)\.(\d+)(?:\.(\d+))?\)/;
|
|
const SAFARI_TO_MACOS = {
|
|
15: [10, 15, 0],
|
|
16: [11, 0, 0],
|
|
17: [12, 0, 0],
|
|
18: [13, 0, 0],
|
|
26: [26, 0, 0],
|
|
};
|
|
|
|
const getCommonTemplateVars = () => {
|
|
const browserRegexes = getPreUserAgentRegexes({
|
|
env: "modern",
|
|
allowHigherVersions: true,
|
|
mobileToDesktop: true,
|
|
throwOnMissing: true,
|
|
});
|
|
const minSafariVersion = browserRegexes.find(
|
|
(regex) => regex.family === "safari"
|
|
)?.matchedVersions[0][0];
|
|
const minMacOSVersion = SAFARI_TO_MACOS[minSafariVersion];
|
|
if (!minMacOSVersion) {
|
|
throw Error(
|
|
`Could not find minimum MacOS version for Safari ${minSafariVersion}.`
|
|
);
|
|
}
|
|
const haMacOSRegex = applyVersionsToRegexes(
|
|
[
|
|
{
|
|
family: "ha_macos",
|
|
regex: HA_MACOS_REGEX,
|
|
matchedVersions: [minMacOSVersion],
|
|
requestVersions: [minMacOSVersion],
|
|
},
|
|
],
|
|
{ ignorePatch: true, allowHigherVersions: true }
|
|
);
|
|
return {
|
|
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
|
|
hassUrl: process.env.HASS_URL || "",
|
|
};
|
|
};
|
|
|
|
const renderTemplate = (templateFile, data = {}) => {
|
|
const compiled = template(
|
|
fs.readFileSync(templateFile, { encoding: "utf-8" })
|
|
);
|
|
return compiled({
|
|
...data,
|
|
// Resolve any child/nested templates relative to the parent and pass the same data
|
|
renderTemplate: (childTemplate) =>
|
|
renderTemplate(resolve(dirname(templateFile), childTemplate), data),
|
|
});
|
|
};
|
|
|
|
const WRAP_TAGS = { ".js": "script", ".css": "style" };
|
|
|
|
const minifyHtml = (content, ext) => {
|
|
const wrapTag = WRAP_TAGS[ext] || "";
|
|
const begTag = wrapTag && `<${wrapTag}>`;
|
|
const endTag = wrapTag && `</${wrapTag}>`;
|
|
return minify(begTag + content + endTag, {
|
|
...htmlMinifierOptions,
|
|
conservativeCollapse: false,
|
|
minifyJS: terserOptions({
|
|
latestBuild: false, // Shared scripts should be ES5
|
|
isTestBuild: true, // Don't need source maps
|
|
}),
|
|
}).then((wrapped) =>
|
|
wrapTag ? wrapped.slice(begTag.length, -endTag.length) : wrapped
|
|
);
|
|
};
|
|
|
|
// Function to generate a dev task for each project's configuration
|
|
const genPagesDevTask =
|
|
(
|
|
pageEntries,
|
|
inputRoot,
|
|
outputRoot,
|
|
inputSub = "src/html",
|
|
publicRoot = ""
|
|
) =>
|
|
async () => {
|
|
const commonVars = getCommonTemplateVars();
|
|
for (const [page, entries] of Object.entries(pageEntries)) {
|
|
const content = renderTemplate(
|
|
resolve(inputRoot, inputSub, `${page}.template`),
|
|
{
|
|
...commonVars,
|
|
latestEntryJS: entries.map(
|
|
(entry) => `${publicRoot}/frontend_latest/${entry}.js`
|
|
),
|
|
es5EntryJS: entries.map(
|
|
(entry) => `${publicRoot}/frontend_es5/${entry}.js`
|
|
),
|
|
latestCustomPanelJS: `${publicRoot}/frontend_latest/custom-panel.js`,
|
|
es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`,
|
|
}
|
|
);
|
|
fs.outputFileSync(resolve(outputRoot, page), content);
|
|
}
|
|
};
|
|
|
|
// Same as previous but for production builds
|
|
// (includes minification and hashed file names from manifest)
|
|
const genPagesProdTask =
|
|
(
|
|
pageEntries,
|
|
inputRoot,
|
|
outputRoot,
|
|
outputLatest,
|
|
outputES5,
|
|
inputSub = "src/html"
|
|
) =>
|
|
async () => {
|
|
const latestManifest = fs.readJsonSync(
|
|
resolve(outputLatest, "manifest.json")
|
|
);
|
|
const es5Manifest = outputES5
|
|
? fs.readJsonSync(resolve(outputES5, "manifest.json"))
|
|
: {};
|
|
const commonVars = getCommonTemplateVars();
|
|
const minifiedHTML = [];
|
|
for (const [page, entries] of Object.entries(pageEntries)) {
|
|
const content = renderTemplate(
|
|
resolve(inputRoot, inputSub, `${page}.template`),
|
|
{
|
|
...commonVars,
|
|
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
|
|
es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]),
|
|
latestCustomPanelJS: latestManifest["custom-panel.js"],
|
|
es5CustomPanelJS: es5Manifest["custom-panel.js"],
|
|
}
|
|
);
|
|
minifiedHTML.push(
|
|
minifyHtml(content, extname(page)).then((minified) =>
|
|
fs.outputFileSync(resolve(outputRoot, page), minified)
|
|
)
|
|
);
|
|
}
|
|
await Promise.all(minifiedHTML);
|
|
};
|
|
|
|
// Map HTML pages to their required entrypoints
|
|
const APP_PAGE_ENTRIES = {
|
|
"authorize.html": ["authorize"],
|
|
"onboarding.html": ["onboarding"],
|
|
"index.html": ["core", "app"],
|
|
};
|
|
|
|
gulp.task(
|
|
"gen-pages-app-dev",
|
|
genPagesDevTask(APP_PAGE_ENTRIES, paths.root_dir, paths.app_output_root)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-app-prod",
|
|
genPagesProdTask(
|
|
APP_PAGE_ENTRIES,
|
|
paths.root_dir,
|
|
paths.app_output_root,
|
|
paths.app_output_latest,
|
|
paths.app_output_es5
|
|
)
|
|
);
|
|
|
|
const CAST_PAGE_ENTRIES = {
|
|
"faq.html": ["launcher"],
|
|
"index.html": ["launcher"],
|
|
"media.html": ["media"],
|
|
"receiver.html": ["receiver"],
|
|
};
|
|
|
|
gulp.task(
|
|
"gen-pages-cast-dev",
|
|
genPagesDevTask(CAST_PAGE_ENTRIES, paths.cast_dir, paths.cast_output_root)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-cast-prod",
|
|
genPagesProdTask(
|
|
CAST_PAGE_ENTRIES,
|
|
paths.cast_dir,
|
|
paths.cast_output_root,
|
|
paths.cast_output_latest,
|
|
paths.cast_output_es5
|
|
)
|
|
);
|
|
|
|
const DEMO_PAGE_ENTRIES = { "index.html": ["main"] };
|
|
|
|
gulp.task(
|
|
"gen-pages-demo-dev",
|
|
genPagesDevTask(DEMO_PAGE_ENTRIES, paths.demo_dir, paths.demo_output_root)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-demo-prod",
|
|
genPagesProdTask(
|
|
DEMO_PAGE_ENTRIES,
|
|
paths.demo_dir,
|
|
paths.demo_output_root,
|
|
paths.demo_output_latest,
|
|
paths.demo_output_es5
|
|
)
|
|
);
|
|
|
|
const GALLERY_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
|
|
|
gulp.task(
|
|
"gen-pages-gallery-dev",
|
|
genPagesDevTask(
|
|
GALLERY_PAGE_ENTRIES,
|
|
paths.gallery_dir,
|
|
paths.gallery_output_root
|
|
)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-gallery-prod",
|
|
genPagesProdTask(
|
|
GALLERY_PAGE_ENTRIES,
|
|
paths.gallery_dir,
|
|
paths.gallery_output_root,
|
|
paths.gallery_output_latest
|
|
)
|
|
);
|
|
|
|
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
|
|
|
gulp.task(
|
|
"gen-pages-landing-page-dev",
|
|
genPagesDevTask(
|
|
LANDING_PAGE_PAGE_ENTRIES,
|
|
paths.landingPage_dir,
|
|
paths.landingPage_output_root
|
|
)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-landing-page-prod",
|
|
genPagesProdTask(
|
|
LANDING_PAGE_PAGE_ENTRIES,
|
|
paths.landingPage_dir,
|
|
paths.landingPage_output_root,
|
|
paths.landingPage_output_latest,
|
|
paths.landingPage_output_es5
|
|
)
|
|
);
|
|
|
|
const E2E_TEST_APP_PAGE_ENTRIES = { "index.html": ["main"] };
|
|
|
|
gulp.task(
|
|
"gen-pages-e2e-test-app-dev",
|
|
genPagesDevTask(
|
|
E2E_TEST_APP_PAGE_ENTRIES,
|
|
paths.e2eTestApp_dir,
|
|
paths.e2eTestApp_output_root
|
|
)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-e2e-test-app-prod",
|
|
genPagesProdTask(
|
|
E2E_TEST_APP_PAGE_ENTRIES,
|
|
paths.e2eTestApp_dir,
|
|
paths.e2eTestApp_output_root,
|
|
paths.e2eTestApp_output_latest
|
|
)
|
|
);
|