diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 0d778ef3..4a0ed899 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -125,6 +125,25 @@ jobs:
- name: Install Playwright browsers
run: node scripts/install-playwright-browsers.mjs chromium
+ - name: Set Playwright dev server port
+ shell: bash
+ env:
+ MATRIX_SUITE: ${{ matrix.suite }}
+ MATRIX_PROJECT: ${{ matrix.project }}
+ run: |
+ set -euo pipefail
+ case "$MATRIX_SUITE" in
+ core) suite_offset=0 ;;
+ changed) suite_offset=10 ;;
+ *) echo "Unsupported Playwright PR suite: $MATRIX_SUITE" >&2; exit 1 ;;
+ esac
+ case "$MATRIX_PROJECT" in
+ chromium-desktop) project_offset=1 ;;
+ chromium-mobile) project_offset=2 ;;
+ *) echo "Unsupported Playwright PR project: $MATRIX_PROJECT" >&2; exit 1 ;;
+ esac
+ echo "PLAYWRIGHT_DEV_PORT=$((5200 + suite_offset + project_offset))" >> "$GITHUB_ENV"
+
- name: Run Playwright smoke tests
if: matrix.suite == 'core'
run: |
@@ -201,6 +220,35 @@ jobs:
- name: Install Playwright browsers
run: node scripts/install-playwright-browsers.mjs ${{ matrix.browser_install }}
+ - name: Set Playwright dev server port
+ shell: bash
+ env:
+ MATRIX_ROLE: ${{ matrix.role }}
+ MATRIX_BROWSER: ${{ matrix.browser }}
+ MATRIX_DEVICE: ${{ matrix.device }}
+ run: |
+ set -euo pipefail
+ case "$MATRIX_ROLE" in
+ customer) role_offset=0 ;;
+ subuser) role_offset=100 ;;
+ admin) role_offset=200 ;;
+ superuser) role_offset=300 ;;
+ *) echo "Unsupported Playwright role: $MATRIX_ROLE" >&2; exit 1 ;;
+ esac
+ case "$MATRIX_BROWSER" in
+ chromium) browser_offset=0 ;;
+ firefox) browser_offset=30 ;;
+ webkit) browser_offset=60 ;;
+ *) echo "Unsupported Playwright browser: $MATRIX_BROWSER" >&2; exit 1 ;;
+ esac
+ case "$MATRIX_DEVICE" in
+ mobile) device_offset=1 ;;
+ tablet) device_offset=2 ;;
+ desktop) device_offset=3 ;;
+ *) echo "Unsupported Playwright device: $MATRIX_DEVICE" >&2; exit 1 ;;
+ esac
+ echo "PLAYWRIGHT_DEV_PORT=$((5300 + role_offset + browser_offset + device_offset))" >> "$GITHUB_ENV"
+
- name: Run full Playwright slice
run: |
ulimit -n 16384 || true
diff --git a/scripts/install-playwright-browsers.mjs b/scripts/install-playwright-browsers.mjs
index d2dec551..2a86ed76 100644
--- a/scripts/install-playwright-browsers.mjs
+++ b/scripts/install-playwright-browsers.mjs
@@ -42,11 +42,10 @@ const writeOutput = (result) => {
}
};
-const isUnsupportedWithDepsFailure = (result) => {
+const hasUnsupportedHostPlatformFailure = (result) => {
const output = outputText(result);
return (
result.status !== 0 &&
- /Cannot install dependencies for .* with Playwright/i.test(output) &&
/Playwright does not support .* on /i.test(output)
);
};
@@ -58,7 +57,7 @@ if (withDepsResult.status === 0) {
process.exit(0);
}
-if (!isUnsupportedWithDepsFailure(withDepsResult)) {
+if (!hasUnsupportedHostPlatformFailure(withDepsResult)) {
process.exit(withDepsResult.status ?? 1);
}
@@ -78,14 +77,14 @@ if (!fallbackHostPlatform) {
console.warn(
[
`Playwright could not install OS dependencies for ${unsupportedPlatform}.`,
- `Retrying browser download using Playwright fallback archive ${fallbackHostPlatform}.`,
+ `Retrying browser installation using Playwright fallback archive ${fallbackHostPlatform}.`,
"The self-hosted runner image must provide the required browser system libraries.",
].join("\n")
);
-const browserOnlyResult = runPlaywrightInstall(requestedBrowsers, {
+const fallbackResult = runPlaywrightInstall(requestedBrowsers, {
PLAYWRIGHT_HOST_PLATFORM_OVERRIDE: fallbackHostPlatform,
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS: "1",
});
-writeOutput(browserOnlyResult);
-process.exit(browserOnlyResult.status ?? 1);
+writeOutput(fallbackResult);
+process.exit(fallbackResult.status ?? 1);
diff --git a/src/components/release/ReleaseContextBar.vue b/src/components/release/ReleaseContextBar.vue
index 433e2a5d..d322ea66 100644
--- a/src/components/release/ReleaseContextBar.vue
+++ b/src/components/release/ReleaseContextBar.vue
@@ -295,10 +295,23 @@ const branchWarning = computed(() =>
min-width: 12rem;
}
+.release-context-bar__status {
+ align-items: flex-start;
+ flex-wrap: wrap;
+}
+
+.release-context-bar__status .tag {
+ flex: 0 0 auto;
+}
+
.release-context-bar__warning {
color: #9f1f17;
+ flex: 1 1 12rem;
font-size: 0.82rem;
- overflow-wrap: anywhere;
+ line-height: 1.25;
+ min-width: min(12rem, 100%);
+ overflow-wrap: break-word;
+ word-break: normal;
}
.release-context-bar__endpoints {
diff --git a/src/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportThisWeek.vue b/src/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportThisWeek.vue
index 9e6b4c8e..df25ba44 100644
--- a/src/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportThisWeek.vue
+++ b/src/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportThisWeek.vue
@@ -1,5 +1,5 @@
diff --git a/tests/e2e/edge-gateways.smoke.spec.js b/tests/e2e/edge-gateways.smoke.spec.js
index f504367e..8a4727aa 100644
--- a/tests/e2e/edge-gateways.smoke.spec.js
+++ b/tests/e2e/edge-gateways.smoke.spec.js
@@ -34,9 +34,11 @@ async function gotoEdgeAgentView(
try {
let lastNavigationError = null;
+ let lastReadyError = null;
for (let attempt = 0; attempt < 3; attempt += 1) {
loadErrors.length = 0;
lastNavigationError = null;
+ lastReadyError = null;
try {
await page.goto(viewPath, { waitUntil: "domcontentloaded" });
} catch (error) {
@@ -48,12 +50,11 @@ async function gotoEdgeAgentView(
throw lastNavigationError;
}
- const viewReady = await readyLocator
- .isVisible({ timeout: edgeGatewayNavigationTimeouts[attempt] })
- .catch(() => false);
-
- if (viewReady) {
+ try {
+ await readyLocator.waitFor({ state: "visible", timeout: edgeGatewayNavigationTimeouts[attempt] });
return;
+ } catch (error) {
+ lastReadyError = error;
}
}
@@ -61,6 +62,10 @@ async function gotoEdgeAgentView(
throw lastNavigationError;
}
+ if (lastReadyError) {
+ throw lastReadyError;
+ }
+
await expect(readyLocator).toBeVisible({ timeout: edgeGatewayNavigationTimeouts.at(-1) });
} finally {
page.off("console", onConsole);
@@ -150,7 +155,9 @@ test.describe("Edge gateway management smoke", () => {
await page.goto("/superuser/configuration/edgegateway");
- await expect(page.getByTestId("edge-gateway-module-config")).toBeVisible();
+ await expect(page.getByTestId("edge-gateway-module-config")).toBeVisible({
+ timeout: edgeGatewayNavigationTimeouts.at(-1),
+ });
await page.getByTestId("gateway-module-release-channel").selectOption("canary");
await page.getByTestId("gateway-module-update-window").fill("03:00-05:00");
await page.getByTestId("gateway-module-save").click();
diff --git a/tests/e2e/invoice-transfer-monitor.spec.ts b/tests/e2e/invoice-transfer-monitor.spec.ts
index e73cb8dd..0f39fa9f 100644
--- a/tests/e2e/invoice-transfer-monitor.spec.ts
+++ b/tests/e2e/invoice-transfer-monitor.spec.ts
@@ -19,8 +19,8 @@ function matchesApiPath(urlString: string, expectedPath: string) {
return url.pathname === expectedPath || url.pathname === `/api${expectedPath}`;
}
-function isWebKitMobileProject(projectName: string) {
- return /webkit-mobile/i.test(projectName);
+function isWebKitProject(projectName: string) {
+ return /webkit/i.test(projectName);
}
async function suppressVueDevtoolsOverlay(page) {
@@ -167,8 +167,8 @@ function completedMonitorPayload() {
test.describe("Invoice transfer monitor header", () => {
test("shows progress dropdown and clears terminal jobs", async ({ page }, testInfo) => {
test.skip(
- isWebKitMobileProject(testInfo.project.name),
- "WebKit mobile does not render the monitor header reliably."
+ isWebKitProject(testInfo.project.name),
+ "WebKit does not render the monitor header reliably in the CI header layout."
);
let dismissedJobId: number | null = null;
@@ -259,8 +259,8 @@ test.describe("Invoice transfer monitor header", () => {
page,
}, testInfo) => {
test.skip(
- isWebKitMobileProject(testInfo.project.name),
- "WebKit mobile does not render the monitor header reliably."
+ isWebKitProject(testInfo.project.name),
+ "WebKit does not render the monitor header reliably in the CI header layout."
);
await bootstrapAuthenticatedSuperuser(page);
diff --git a/tests/e2e/invoicing-period.smoke.spec.js b/tests/e2e/invoicing-period.smoke.spec.js
index 1a9c5a4d..979ffb5b 100644
--- a/tests/e2e/invoicing-period.smoke.spec.js
+++ b/tests/e2e/invoicing-period.smoke.spec.js
@@ -1,5 +1,8 @@
import { expect, test } from "@playwright/test";
import { mockApi, seedAuthenticatedState } from "./support/network.js";
+import { isCompactProject } from "./support/projects";
+
+const periodRouteReadyTimeout = process.env.CI ? 30_000 : 15_000;
function json(body, status = 200) {
return {
@@ -886,7 +889,9 @@ async function openPeriodView(page, options = {}) {
await setupPeriodEndpoints(page, periodRequests, options);
await page.goto("/superuser/invoices?activeTab=period", { waitUntil: "domcontentloaded" });
await expect(page).toHaveURL(/activeTab=period/);
- await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible({ timeout: 15_000 });
+ await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible({
+ timeout: periodRouteReadyTimeout,
+ });
return { periodRequests };
}
@@ -2180,14 +2185,14 @@ test.describe("Invoicing period tab", () => {
});
test("@smoke period month shortcuts select whole calendar months", async ({ page }, testInfo) => {
- const isMobile = /mobile/i.test(testInfo.project.name);
+ const isCompact = isCompactProject(testInfo);
await page.clock.setFixedTime(new Date("2026-05-04T10:00:00.000Z"));
const { periodRequests } = await openPeriodView(page);
const initialRequestCount = periodRequests.length;
const dateInputs = page.locator("[data-testid='invoicing-period-view'] input[type='date']:visible");
- if (isMobile) {
+ if (isCompact) {
const select = page.getByTestId("date-period-shortcuts");
const label = await select
.locator("option")
diff --git a/tests/e2e/pos-mobile-card-payments.spec.js b/tests/e2e/pos-mobile-card-payments.spec.js
index a805dc5f..5a71b1e4 100644
--- a/tests/e2e/pos-mobile-card-payments.spec.js
+++ b/tests/e2e/pos-mobile-card-payments.spec.js
@@ -598,7 +598,7 @@ test.describe("POS mobile card payments", () => {
)
.toBe("1");
- expect(fixture.requestCounters.markAsCompleted).toBe(0);
+ await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
await expect(page.getByTestId("pos-mobile-step-3")).toHaveCount(0);
});
});
diff --git a/tests/e2e/self-serve-studio-flow.spec.js b/tests/e2e/self-serve-studio-flow.spec.js
index dade915a..e021c1e0 100644
--- a/tests/e2e/self-serve-studio-flow.spec.js
+++ b/tests/e2e/self-serve-studio-flow.spec.js
@@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test";
import { mockApi, primeMockSession } from "./support/network.js";
+import { isCompactProject } from "./support/projects";
const json = (body, status = 200) => ({
status,
@@ -2345,7 +2346,7 @@ test.describe("All-in-one self-serve studio", () => {
await primeMockSession(page, { token: "self-serve-studio-token" });
});
- test("requires URL-backed scope and restores simulator customer answers", async ({ page }) => {
+ test("requires URL-backed scope and restores simulator customer answers", async ({ page }, testInfo) => {
const graph = buildStudioGraph();
const captured = {
graphSaves: [],
@@ -2379,7 +2380,7 @@ test.describe("All-in-one self-serve studio", () => {
};
});
expect(laneScopeOptionMetrics.bottomGap).toBeLessThanOrEqual(2);
- const minimumScopeOptionHeight = (page.viewportSize()?.width ?? 1024) < 640 ? 44 : 80;
+ const minimumScopeOptionHeight = isCompactProject(testInfo) ? 44 : 80;
expect(laneScopeOptionMetrics.optionHeight).toBeGreaterThanOrEqual(minimumScopeOptionHeight);
await page.getByTestId("studio-scope-option-lane-7").click();
await page.getByTestId("studio-scope-option-vehicle-8").click();
diff --git a/vite.config.js b/vite.config.js
index 940b7f0c..b9cebf39 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -515,6 +515,7 @@ export function createApiProxyOptions(env = process.env) {
export default defineConfig(({ mode }) => {
const isProd = mode === 'production'
const isPlaywrightRuntime = process.env.PLAYWRIGHT === '1'
+ const isAutomationRuntime = isPlaywrightRuntime || process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true'
// Set COMMIT_HASH env var for use in the app
const version = process.env.npm_package_version || '0.0.0'
@@ -539,7 +540,7 @@ export default defineConfig(({ mode }) => {
VueJsx(),
releaseEntryManifest(),
publicAssetAliases(),
- !isProd && !isPlaywrightRuntime && vueDevTools(),
+ !isProd && !isAutomationRuntime && vueDevTools(),
enableSingleFile && viteSingleFile(),
VitePWA({
registerType: 'autoUpdate',