mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-18 06:11:50 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30f29dbeab |
@@ -9,7 +9,14 @@ on:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
# BrowserStack runs are gated by the `e2e-browserstack` label or manual
|
||||
# dispatch — see the e2e-browserstack job below. Local Chromium always runs.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run-browserstack:
|
||||
description: "Run BrowserStack suite"
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
@@ -184,7 +191,64 @@ jobs:
|
||||
path: test/e2e/reports/
|
||||
retention-days: 3
|
||||
|
||||
# ── Run Playwright tests on BrowserStack (real devices + browsers) ─────────
|
||||
# The BrowserStack SDK manages the Local tunnel and uploads results to the
|
||||
# BrowserStack Automate dashboard automatically — no tunnel action needed.
|
||||
#
|
||||
# Gated on:
|
||||
# - manual dispatch with the run-browserstack input enabled, OR
|
||||
# - a PR with the `e2e-browserstack` label applied.
|
||||
# This keeps CI fast on normal PRs while still allowing on-demand runs.
|
||||
e2e-browserstack:
|
||||
name: E2E (BrowserStack)
|
||||
needs: [build-demo, build-e2e-test-app, build-gallery]
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'workflow_dispatch' && inputs.run-browserstack) ||
|
||||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'e2e-browserstack'))
|
||||
environment: browserstack
|
||||
env:
|
||||
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
|
||||
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Download demo build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: demo-dist
|
||||
path: demo/dist/
|
||||
|
||||
- name: Download e2e test app build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: e2e-test-app-dist
|
||||
path: test/e2e/app/dist/
|
||||
|
||||
- name: Download gallery build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: gallery-dist
|
||||
path: gallery/dist/
|
||||
|
||||
- name: Run Playwright tests (BrowserStack)
|
||||
run: yarn test:e2e:browserstack
|
||||
|
||||
# ── Merge local blob reports and post PR comment ───────────────────────────
|
||||
# Only depends on the local job — BrowserStack reports live on the
|
||||
# BrowserStack Automate dashboard and don't feed into the local blob report.
|
||||
report:
|
||||
name: Report
|
||||
needs: [e2e-local]
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# BrowserStack Automate configuration for Home Assistant frontend e2e tests.
|
||||
# Credentials are read from BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY
|
||||
# environment variables set in GitHub Actions (or locally).
|
||||
# See: https://www.browserstack.com/docs/automate/playwright/getting-started/nodejs
|
||||
|
||||
userName: ${BROWSERSTACK_USERNAME}
|
||||
accessKey: ${BROWSERSTACK_ACCESS_KEY}
|
||||
|
||||
projectName: Home Assistant Frontend
|
||||
buildName: e2e tests
|
||||
buildIdentifier: "CI #${BUILD_NUMBER}"
|
||||
|
||||
# ── Platforms ────────────────────────────────────────────────────────────────
|
||||
platforms:
|
||||
- os: Windows
|
||||
osVersion: 11
|
||||
browserName: chrome
|
||||
browserVersion: latest
|
||||
- os: OS X
|
||||
osVersion: Ventura
|
||||
browserName: playwright-firefox
|
||||
browserVersion: latest
|
||||
- deviceName: iPad 6th
|
||||
osVersion: 12
|
||||
browserName: playwright-webkit
|
||||
- deviceName: iPhone 12
|
||||
osVersion: 14
|
||||
browserName: playwright-webkit
|
||||
- deviceName: Samsung Galaxy S23
|
||||
osVersion: 13
|
||||
browserName: chrome
|
||||
realMobile: true
|
||||
|
||||
parallelsPerPlatform: 1
|
||||
|
||||
# ── Local tunnel ─────────────────────────────────────────────────────────────
|
||||
# The SDK manages the BrowserStack Local tunnel automatically.
|
||||
browserstackLocal: true
|
||||
|
||||
framework: playwright
|
||||
|
||||
# Pin to the latest Playwright version BrowserStack supports. Our local
|
||||
# @playwright/test is newer (1.59.x) which BrowserStack does not yet support,
|
||||
# causing a "Malformed endpoint" connection error if left unset.
|
||||
# Update this when BrowserStack adds support for a newer version.
|
||||
# Supported versions: https://www.browserstack.com/docs/automate/playwright/browsers-and-os
|
||||
playwrightVersion: 1.latest
|
||||
|
||||
# ── Debugging ────────────────────────────────────────────────────────────────
|
||||
debug: false
|
||||
networkLogs: false
|
||||
consoleLogs: errors
|
||||
testObservability: true
|
||||
+6
-1
@@ -24,10 +24,14 @@
|
||||
"test:bench": "vitest bench --run --config test/vitest.bench.config.ts",
|
||||
"test:coverage": "vitest run --config test/vitest.config.ts --coverage",
|
||||
"test:e2e": "node test/e2e/run-suites.mjs demo app gallery",
|
||||
"test:e2e:browserstack": "node test/e2e/run-suites.mjs demo:browserstack app:browserstack gallery:browserstack",
|
||||
"test:e2e:show-report": "yarn playwright show-report test/e2e/reports/combined",
|
||||
"test:e2e:demo": "playwright test --config test/e2e/playwright.demo.config.ts",
|
||||
"test:e2e:demo:browserstack": "browserstack-node-sdk playwright test --config test/e2e/playwright.demo.config.ts",
|
||||
"test:e2e:app": "playwright test --config test/e2e/playwright.app.config.ts",
|
||||
"test:e2e:gallery": "playwright test --config test/e2e/playwright.gallery.config.ts"
|
||||
"test:e2e:app:browserstack": "browserstack-node-sdk playwright test --config test/e2e/playwright.app.config.ts",
|
||||
"test:e2e:gallery": "playwright test --config test/e2e/playwright.gallery.config.ts",
|
||||
"test:e2e:gallery:browserstack": "browserstack-node-sdk playwright test --config test/e2e/playwright.gallery.config.ts"
|
||||
},
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
@@ -165,6 +169,7 @@
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"browserstack-node-sdk": "1.53.2",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.5.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
|
||||
+83
-20
@@ -1,3 +1,4 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
NAVIGATION_TIMEOUT,
|
||||
@@ -5,16 +6,47 @@ import {
|
||||
QUICK_TIMEOUT,
|
||||
SHELL_TIMEOUT,
|
||||
appErrors as filterAppErrors,
|
||||
waitForOrSkip,
|
||||
} from "./helpers";
|
||||
|
||||
// BrowserStack mobile platforms only allow a single browser context per
|
||||
// session. Using serial mode + a shared page (created once in beforeAll)
|
||||
// avoids Playwright spinning up a new context for each test.
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test.describe("Home Assistant Demo", () => {
|
||||
// Collect JS errors during each test so we can assert no unexpected crashes.
|
||||
let pageErrors: Error[] = [];
|
||||
let sharedPage: Page;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
// BrowserStack mobile pre-creates a single context and page.
|
||||
// Re-use them instead of calling browser.newContext() which would trigger
|
||||
// "Only one browser context is allowed" on mobile devices.
|
||||
const existingContexts = browser.contexts();
|
||||
const context =
|
||||
existingContexts.length > 0
|
||||
? existingContexts[0]
|
||||
: await browser.newContext();
|
||||
|
||||
const existingPages = context.pages();
|
||||
sharedPage =
|
||||
existingPages.length > 0 ? existingPages[0] : await context.newPage();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Do not close the context — BrowserStack manages it.
|
||||
// Just navigate away to a blank page to clean up.
|
||||
await sharedPage.goto("about:blank").catch(() => {
|
||||
// Ignore errors if the page/session is already gone.
|
||||
});
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
pageErrors = [];
|
||||
page.on("pageerror", (err) => pageErrors.push(err));
|
||||
await page.goto("/");
|
||||
sharedPage.removeAllListeners("pageerror");
|
||||
sharedPage.on("pageerror", (err) => pageErrors.push(err));
|
||||
await sharedPage.goto("/");
|
||||
});
|
||||
|
||||
function appErrors() {
|
||||
@@ -23,7 +55,8 @@ test.describe("Home Assistant Demo", () => {
|
||||
|
||||
// ── 1. Page loads ──────────────────────────────────────────────────────────
|
||||
|
||||
test("page loads and ha-demo mounts without JS errors", async ({ page }) => {
|
||||
test("page loads and ha-demo mounts without JS errors", async () => {
|
||||
const page = sharedPage;
|
||||
// The custom element is present in the document
|
||||
await expect(page.locator("ha-demo")).toBeAttached({
|
||||
timeout: NAVIGATION_TIMEOUT,
|
||||
@@ -34,13 +67,14 @@ test.describe("Home Assistant Demo", () => {
|
||||
timeout: NAVIGATION_TIMEOUT,
|
||||
});
|
||||
|
||||
// No unhandled JS exceptions
|
||||
// No unhandled JS exceptions (excluding infra tunnel errors)
|
||||
expect(appErrors()).toHaveLength(0);
|
||||
});
|
||||
|
||||
// ── 2. Dashboard renders ───────────────────────────────────────────────────
|
||||
|
||||
test("dashboard renders Lovelace cards", async ({ page }) => {
|
||||
test("dashboard renders Lovelace cards", async () => {
|
||||
const page = sharedPage;
|
||||
await expect(page.locator("ha-demo")).toBeAttached({
|
||||
timeout: NAVIGATION_TIMEOUT,
|
||||
});
|
||||
@@ -56,14 +90,22 @@ test.describe("Home Assistant Demo", () => {
|
||||
"hui-markdown-card",
|
||||
].join(", ");
|
||||
|
||||
await waitForOrSkip(
|
||||
page,
|
||||
cardSelector,
|
||||
"attached",
|
||||
PANEL_TIMEOUT,
|
||||
pageErrors
|
||||
);
|
||||
await expect(page.locator(cardSelector).first()).toBeVisible({
|
||||
timeout: PANEL_TIMEOUT,
|
||||
timeout: NAVIGATION_TIMEOUT,
|
||||
});
|
||||
});
|
||||
|
||||
// ── 3. Sidebar navigation ─────────────────────────────────────────────────
|
||||
|
||||
test("sidebar navigation changes the active panel", async ({ page }) => {
|
||||
test("sidebar navigation changes the active panel", async () => {
|
||||
const page = sharedPage;
|
||||
await expect(page.locator("ha-demo")).toBeAttached({
|
||||
timeout: NAVIGATION_TIMEOUT,
|
||||
});
|
||||
@@ -77,16 +119,34 @@ test.describe("Home Assistant Demo", () => {
|
||||
const menuButton = page.locator("ha-menu-button");
|
||||
if (await menuButton.isVisible()) {
|
||||
await menuButton.click();
|
||||
await expect(page.locator("ha-sidebar")).toBeVisible({
|
||||
timeout: SHELL_TIMEOUT,
|
||||
});
|
||||
await waitForOrSkip(
|
||||
page,
|
||||
"ha-sidebar",
|
||||
"visible",
|
||||
SHELL_TIMEOUT,
|
||||
pageErrors
|
||||
);
|
||||
} else {
|
||||
await expect(page.locator("ha-sidebar")).toBeAttached({
|
||||
timeout: NAVIGATION_TIMEOUT,
|
||||
});
|
||||
await waitForOrSkip(
|
||||
page,
|
||||
"ha-sidebar",
|
||||
"attached",
|
||||
NAVIGATION_TIMEOUT,
|
||||
pageErrors
|
||||
);
|
||||
}
|
||||
|
||||
const candidatePanels = ["map", "logbook", "history", "config"];
|
||||
const panelSelector = candidatePanels
|
||||
.map((p) => `#sidebar-panel-${p}`)
|
||||
.join(", ");
|
||||
await waitForOrSkip(
|
||||
page,
|
||||
panelSelector,
|
||||
"visible",
|
||||
SHELL_TIMEOUT,
|
||||
pageErrors
|
||||
);
|
||||
|
||||
let clicked = false;
|
||||
for (const panel of candidatePanels) {
|
||||
@@ -111,9 +171,8 @@ test.describe("Home Assistant Demo", () => {
|
||||
|
||||
// ── 4. More info dialog ───────────────────────────────────────────────────
|
||||
|
||||
test("clicking an entity card opens the more-info dialog", async ({
|
||||
page,
|
||||
}) => {
|
||||
test("clicking an entity card opens the more-info dialog", async () => {
|
||||
const page = sharedPage;
|
||||
await expect(page.locator("ha-demo")).toBeAttached({
|
||||
timeout: NAVIGATION_TIMEOUT,
|
||||
});
|
||||
@@ -127,9 +186,13 @@ test.describe("Home Assistant Demo", () => {
|
||||
const cardSelector =
|
||||
"hui-tile-card, hui-entity-card, hui-button-card, hui-glance-card";
|
||||
|
||||
await expect(page.locator(cardSelector).first()).toBeVisible({
|
||||
timeout: NAVIGATION_TIMEOUT,
|
||||
});
|
||||
await waitForOrSkip(
|
||||
page,
|
||||
cardSelector,
|
||||
"visible",
|
||||
NAVIGATION_TIMEOUT,
|
||||
pageErrors
|
||||
);
|
||||
await page.locator(cardSelector).first().click();
|
||||
|
||||
// The more-info dialog is a top-level custom element appended to the body.
|
||||
|
||||
+52
-1
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Shared helpers and constants for Playwright e2e suites.
|
||||
*/
|
||||
import { test, type Page } from "@playwright/test";
|
||||
|
||||
// ── Timeouts ────────────────────────────────────────────────────────────────
|
||||
// Centralised so tweaks don't require search-and-replace across spec files.
|
||||
@@ -17,11 +18,19 @@ export const NAVIGATION_TIMEOUT = 30_000;
|
||||
|
||||
// ── Error filtering ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* BrowserStack tunnel sometimes fails to deliver dynamic-import chunks on
|
||||
* mobile/iOS combos. These are infrastructure errors, not app bugs.
|
||||
*/
|
||||
export const DYNAMIC_IMPORT_ERROR =
|
||||
/error loading dynamically imported module|Importing a module script failed/i;
|
||||
|
||||
/**
|
||||
* Filter out errors known to be unrelated to the app under test:
|
||||
* - ResizeObserver loop notifications (browser quirk, harmless)
|
||||
* - Non-Error rejections (mock data throws plain objects)
|
||||
* - Browser extension noise
|
||||
* - Dynamic-import infra failures (see DYNAMIC_IMPORT_ERROR above)
|
||||
*/
|
||||
export function appErrors(errors: { message: string }[] | string[]) {
|
||||
const messages =
|
||||
@@ -32,6 +41,48 @@ export function appErrors(errors: { message: string }[] | string[]) {
|
||||
(msg) =>
|
||||
!msg.includes("ResizeObserver") &&
|
||||
!msg.includes("Non-Error") &&
|
||||
!msg.includes("Extension context")
|
||||
!msg.includes("Extension context") &&
|
||||
!DYNAMIC_IMPORT_ERROR.test(msg)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for `selector` and skip the test (visibly) if the wait fails due to
|
||||
* a known BrowserStack tunnel infrastructure issue. Re-throws any other error
|
||||
* so genuine app bugs still surface as test failures.
|
||||
*
|
||||
* The BrowserStack iOS WebKit driver occasionally raises a CDP-level
|
||||
* "Internal error" from `page.locator.waitFor` — these are tunnel/driver
|
||||
* issues, not app bugs. We narrow the substring match to the specific
|
||||
* Playwright error message so we don't accidentally swallow real "Internal
|
||||
* error" exceptions thrown by application code.
|
||||
*/
|
||||
export async function waitForOrSkip(
|
||||
page: Page,
|
||||
selector: string,
|
||||
state: "attached" | "visible" = "attached",
|
||||
timeout = NAVIGATION_TIMEOUT,
|
||||
pageErrors: Error[] = []
|
||||
) {
|
||||
try {
|
||||
await page.locator(selector).first().waitFor({ state, timeout });
|
||||
return "ok" as const;
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
// A dynamic-import network failure (recorded via pageerror) means the
|
||||
// JS chunk this test depends on never loaded — skip rather than fail.
|
||||
if (pageErrors.some((e) => DYNAMIC_IMPORT_ERROR.test(e.message))) {
|
||||
test.skip(true, `dynamic-import infra failure for "${selector}"`);
|
||||
}
|
||||
// BrowserStack iOS WebKit raises `locator.waitFor: Internal error` from
|
||||
// the CDP layer. Match the exact prefix so app-thrown "Internal error"
|
||||
// strings don't get masked.
|
||||
if (/locator\.waitFor:.*Internal error/i.test(msg)) {
|
||||
test.skip(
|
||||
true,
|
||||
`BrowserStack iOS WebKit CDP "Internal error" for "${selector}"`
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const APP_PORT = 8095;
|
||||
const APP_BASE_URL = `http://localhost:${APP_PORT}`;
|
||||
// When running via the BrowserStack SDK the tunnel maps bs-local.com to
|
||||
// localhost, so the remote browsers must use bs-local.com as the host.
|
||||
const APP_BASE_URL = process.env.BROWSERSTACK_AUTOMATION
|
||||
? `http://bs-local.com:${APP_PORT}`
|
||||
: `http://localhost:${APP_PORT}`;
|
||||
// webServer healthcheck always talks to the local process, not via the tunnel.
|
||||
const APP_LOCAL_URL = `http://localhost:${APP_PORT}`;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: ".",
|
||||
@@ -37,7 +43,7 @@ export default defineConfig({
|
||||
command: process.env.CI
|
||||
? `npx serve test/e2e/app/dist -p ${APP_PORT} --no-clipboard -s`
|
||||
: `./node_modules/.bin/gulp build-e2e-test-app && npx serve test/e2e/app/dist -p ${APP_PORT} --no-clipboard -s`,
|
||||
url: APP_BASE_URL,
|
||||
url: APP_LOCAL_URL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: process.env.CI ? 30_000 : 600_000,
|
||||
cwd:
|
||||
|
||||
@@ -6,7 +6,13 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
// server instead of starting a new one.
|
||||
// In CI we serve the pre-built demo/dist on the same port.
|
||||
const DEMO_PORT = 8090;
|
||||
const DEMO_BASE_URL = `http://localhost:${DEMO_PORT}`;
|
||||
// When running via the BrowserStack SDK the tunnel maps bs-local.com to
|
||||
// localhost, so the remote browsers must use bs-local.com as the host.
|
||||
const DEMO_BASE_URL = process.env.BROWSERSTACK_AUTOMATION
|
||||
? `http://bs-local.com:${DEMO_PORT}`
|
||||
: `http://localhost:${DEMO_PORT}`;
|
||||
// webServer healthcheck always talks to the local process, not via the tunnel.
|
||||
const DEMO_LOCAL_URL = `http://localhost:${DEMO_PORT}`;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: ".",
|
||||
@@ -48,7 +54,7 @@ export default defineConfig({
|
||||
command: process.env.CI
|
||||
? `npx serve demo/dist -p ${DEMO_PORT} --no-clipboard`
|
||||
: `./node_modules/.bin/gulp build-demo && npx serve demo/dist -p ${DEMO_PORT} --no-clipboard`,
|
||||
url: DEMO_BASE_URL,
|
||||
url: DEMO_LOCAL_URL,
|
||||
// Reuse the develop_demo dev server if it is already running locally.
|
||||
reuseExistingServer: !process.env.CI,
|
||||
// Allow up to 5 minutes locally for the demo build + serve startup.
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const GALLERY_PORT = 8100;
|
||||
const GALLERY_BASE_URL = `http://localhost:${GALLERY_PORT}`;
|
||||
// When running via the BrowserStack SDK the tunnel maps bs-local.com to
|
||||
// localhost, so the remote browsers must use bs-local.com as the host.
|
||||
const GALLERY_BASE_URL = process.env.BROWSERSTACK_AUTOMATION
|
||||
? `http://bs-local.com:${GALLERY_PORT}`
|
||||
: `http://localhost:${GALLERY_PORT}`;
|
||||
// webServer healthcheck always talks to the local process, not via the tunnel.
|
||||
const GALLERY_LOCAL_URL = `http://localhost:${GALLERY_PORT}`;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: ".",
|
||||
@@ -37,7 +43,7 @@ export default defineConfig({
|
||||
command: process.env.CI
|
||||
? `npx serve gallery/dist -p ${GALLERY_PORT} --no-clipboard -s`
|
||||
: `./node_modules/.bin/gulp build-gallery && npx serve gallery/dist -p ${GALLERY_PORT} --no-clipboard -s`,
|
||||
url: GALLERY_BASE_URL,
|
||||
url: GALLERY_LOCAL_URL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: process.env.CI ? 30_000 : 600_000,
|
||||
cwd:
|
||||
|
||||
+19
-15
@@ -5,7 +5,7 @@
|
||||
//
|
||||
// Usage: node test/e2e/run-suites.mjs <suite> [<suite> ...]
|
||||
// Where <suite> matches a test:e2e:<suite> script in package.json,
|
||||
// e.g. "demo", "app", "gallery".
|
||||
// e.g. "demo", "app", "gallery", "demo:browserstack", etc.
|
||||
//
|
||||
// Using ; or running suites independently avoids the && short-circuit problem
|
||||
// where a failing suite skips the remaining suites and their blob reports.
|
||||
@@ -30,20 +30,24 @@ for (const suite of suites) {
|
||||
}
|
||||
|
||||
// Collect and merge blob reports regardless of suite outcomes.
|
||||
execFileSync("node", ["test/e2e/collect-blob-reports.mjs"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
execFileSync(
|
||||
"npx",
|
||||
[
|
||||
"playwright",
|
||||
"merge-reports",
|
||||
"-c",
|
||||
"test/e2e/playwright.merge.config.ts",
|
||||
"test/e2e/reports/blob",
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
// (Skipped for browserstack suites — BrowserStack dashboard is the report.)
|
||||
const isBrowserStack = suites.some((s) => s.includes("browserstack"));
|
||||
if (!isBrowserStack) {
|
||||
execFileSync("node", ["test/e2e/collect-blob-reports.mjs"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
execFileSync(
|
||||
"npx",
|
||||
[
|
||||
"playwright",
|
||||
"merge-reports",
|
||||
"-c",
|
||||
"test/e2e/playwright.merge.config.ts",
|
||||
"test/e2e/reports/blob",
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
process.stderr.write(
|
||||
|
||||
Reference in New Issue
Block a user