[codex] Improve admin notifications page UI (#145)
* Improve admin notifications page UI * Stabilize notifications page E2E bootstrap wait * Fix PR Playwright diff head ref * Guard MyWashStart timers after test teardown * Stabilize self-serve wash E2E timing * Refresh edge gateway fixture runtime state * Run Playwright E2E on GitHub-hosted runners * Run all frontend CI on GitHub-hosted runners * Increase hosted full E2E parallelism * Stabilize full E2E validation * Preserve superuser gateway navigation label * Add targeted Playwright dispatch workflow * Integrate targeted Playwright dispatch into tests workflow --------- Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
This commit is contained in:
co-authored by
Jeppe Bundgaard
parent
d2cbf823d8
commit
d2682da3cc
+220
-9
@@ -7,6 +7,30 @@ on:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: "What to run for a manual dispatch."
|
||||
required: true
|
||||
type: choice
|
||||
default: full
|
||||
options:
|
||||
- full
|
||||
- targeted
|
||||
- targeted-then-full
|
||||
target_specs:
|
||||
description: "Comma- or newline-separated Playwright spec paths under tests/e2e."
|
||||
required: false
|
||||
type: string
|
||||
default: "tests/e2e/superuser-department-overview.spec.js"
|
||||
target_projects:
|
||||
description: "JSON array of Playwright projects for targeted mode."
|
||||
required: false
|
||||
type: string
|
||||
default: '["chromium-desktop","chromium-mobile","chromium-tablet","webkit-mobile","webkit-desktop"]'
|
||||
target_grep:
|
||||
description: "Optional Playwright grep pattern for targeted mode."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
runner:
|
||||
description: "Runner pool for this manually dispatched test run"
|
||||
required: false
|
||||
@@ -31,7 +55,7 @@ jobs:
|
||||
runs-on: ${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.runner == 'github-hosted' && '["ubuntu-latest"]' || '["self-hosted","Linux","X64","pleno","frontend"]') }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Repair self-hosted workspace permissions
|
||||
- name: Normalize workspace permissions
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||
@@ -67,7 +91,7 @@ jobs:
|
||||
runs-on: ${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.runner == 'github-hosted' && '["ubuntu-latest"]' || '["self-hosted","Linux","X64","pleno","frontend"]') }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Repair self-hosted workspace permissions
|
||||
- name: Normalize workspace permissions
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||
@@ -106,11 +130,192 @@ jobs:
|
||||
env:
|
||||
VITEST_BATCH_SIZE: 5
|
||||
|
||||
e2e-pr:
|
||||
if: github.event_name != 'schedule'
|
||||
e2e-targeted:
|
||||
if: >
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
(inputs.mode == 'targeted' || inputs.mode == 'targeted-then-full')
|
||||
needs: build-and-unit
|
||||
name: E2E-targeted-${{ matrix.project }}
|
||||
# Use GitHub-hosted runners to avoid self-hosted desktop contention and sleep/power events.
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 35
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
project: ${{ fromJSON(inputs.target_projects || '["chromium-desktop"]') }}
|
||||
env:
|
||||
MATRIX_PROJECT: ${{ matrix.project }}
|
||||
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-targeted-${{ matrix.project }}
|
||||
PLAYWRIGHT_REPORTER_MODE: line-html
|
||||
PLAYWRIGHT_WORKERS: 1
|
||||
PLAYWRIGHT_VIDEO_MODE: on-first-retry
|
||||
TARGET_GREP: ${{ inputs.target_grep }}
|
||||
TARGET_SPECS: ${{ inputs.target_specs }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
steps:
|
||||
- name: Normalize workspace permissions
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
|
||||
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
|
||||
if [[ -n "$foreign_entry" ]]; then
|
||||
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
|
||||
rm -rf "$trash" 2>/dev/null || true
|
||||
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
|
||||
mkdir -p "$GITHUB_WORKSPACE"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Run targeted Playwright specs in container
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "$MATRIX_PROJECT" in
|
||||
chromium-mobile) project_offset=1 ;;
|
||||
chromium-desktop) project_offset=2 ;;
|
||||
chromium-tablet) project_offset=3 ;;
|
||||
webkit-mobile) project_offset=31 ;;
|
||||
webkit-desktop) project_offset=32 ;;
|
||||
webkit-tablet) project_offset=33 ;;
|
||||
firefox-mobile) project_offset=61 ;;
|
||||
firefox-desktop) project_offset=62 ;;
|
||||
firefox-tablet) project_offset=63 ;;
|
||||
*) echo "Unsupported Playwright project: $MATRIX_PROJECT" >&2; exit 1 ;;
|
||||
esac
|
||||
port_seed=$((20000 + (RUN_ID % 20000) + project_offset))
|
||||
lock_root="${PLAYWRIGHT_PORT_LOCK_ROOT:-/tmp/pleno-playwright-port-locks}"
|
||||
mkdir -p "$lock_root"
|
||||
chmod 1777 "$lock_root" 2>/dev/null || true
|
||||
find "$lock_root" -mindepth 1 -maxdepth 1 -type d -mmin +360 -exec rmdir {} \; 2>/dev/null || true
|
||||
playwright_port_lock=""
|
||||
playwright_dev_port=""
|
||||
for ((candidate = port_seed; candidate < port_seed + 1000; candidate += 1)); do
|
||||
lock_dir="${lock_root}/${candidate}.lock"
|
||||
if ! mkdir "$lock_dir" 2>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
if ss -H -ltn "sport = :${candidate}" 2>/dev/null | grep -q .; then
|
||||
rmdir "$lock_dir" || true
|
||||
continue
|
||||
fi
|
||||
playwright_port_lock="$lock_dir"
|
||||
playwright_dev_port="$candidate"
|
||||
break
|
||||
done
|
||||
if [[ -z "$playwright_dev_port" ]]; then
|
||||
echo "Unable to find a free Playwright dev-server port." >&2
|
||||
exit 1
|
||||
fi
|
||||
trap 'if [[ -n "${playwright_port_lock:-}" ]]; then rmdir "$playwright_port_lock" 2>/dev/null || true; fi' EXIT
|
||||
if docker info >/dev/null 2>&1; then
|
||||
docker_cmd=(docker)
|
||||
elif sudo -n docker info >/dev/null 2>&1; then
|
||||
docker_cmd=(sudo docker)
|
||||
else
|
||||
echo "Docker is not available to the runner user, and sudo docker is not available." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p output/playwright
|
||||
scripts/ci/runner-diagnostics.sh "before targeted Playwright ${MATRIX_PROJECT}" -- "${docker_cmd[@]}"
|
||||
SYSTEMD_INHIBIT_REASON="Frontend targeted Playwright ${MATRIX_PROJECT}" \
|
||||
scripts/ci/with-systemd-inhibit.sh "${docker_cmd[@]}" run --rm --ipc=host --network host \
|
||||
--volume "$PWD:/source:ro" \
|
||||
--volume "$PWD/output/playwright:/work/output/playwright" \
|
||||
--workdir /work \
|
||||
--env HOME=/tmp \
|
||||
--env CI="${CI:-}" \
|
||||
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
|
||||
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
|
||||
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
|
||||
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
|
||||
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
|
||||
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
|
||||
--env TARGET_GREP="$TARGET_GREP" \
|
||||
--env TARGET_SPECS="$TARGET_SPECS" \
|
||||
mcr.microsoft.com/playwright:v1.58.2-noble \
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf -
|
||||
git config --global --add safe.directory /work
|
||||
install_dependencies() {
|
||||
local attempt
|
||||
for attempt in 1 2 3; do
|
||||
if npm ci --legacy-peer-deps --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$attempt" == "3" ]]; then
|
||||
return 1
|
||||
fi
|
||||
echo "npm ci failed on attempt ${attempt}; retrying..." >&2
|
||||
sleep 20
|
||||
done
|
||||
}
|
||||
install_dependencies
|
||||
ulimit -n 16384 || true
|
||||
mapfile -t spec_args < <(printf "%s\n" "$TARGET_SPECS" | tr "," "\n" | sed "s/^[[:space:]]*//;s/[[:space:]]*$//;/^$/d")
|
||||
if [[ "${#spec_args[@]}" -eq 0 && -z "${TARGET_GREP:-}" ]]; then
|
||||
echo "Provide at least one spec path or grep pattern." >&2
|
||||
exit 1
|
||||
fi
|
||||
for spec_path in "${spec_args[@]}"; do
|
||||
if [[ "$spec_path" == /* || "$spec_path" == *".."* || "$spec_path" != tests/e2e/* ]]; then
|
||||
echo "Targeted spec must stay under tests/e2e: $spec_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$spec_path" ]]; then
|
||||
echo "Targeted spec does not exist: $spec_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
args=("${spec_args[@]}")
|
||||
if [[ -n "${TARGET_GREP:-}" ]]; then
|
||||
args+=(--grep "$TARGET_GREP")
|
||||
fi
|
||||
args+=(--project="$MATRIX_PROJECT")
|
||||
npx playwright test "${args[@]}"
|
||||
'
|
||||
|
||||
- name: Runner diagnostics after Playwright failure
|
||||
if: failure() || cancelled()
|
||||
continue-on-error: true
|
||||
run: scripts/ci/runner-diagnostics.sh "after targeted Playwright ${{ matrix.project }}"
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: failure() || cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-targeted-${{ matrix.project }}
|
||||
path: |
|
||||
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}-*
|
||||
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
e2e-pr:
|
||||
if: >
|
||||
always() &&
|
||||
github.event_name != 'schedule' &&
|
||||
needs.build-and-unit.result == 'success' &&
|
||||
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted') &&
|
||||
(
|
||||
github.event_name != 'workflow_dispatch' ||
|
||||
inputs.mode == 'full' ||
|
||||
needs.e2e-targeted.result == 'success'
|
||||
)
|
||||
needs: [build-and-unit, e2e-targeted]
|
||||
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
|
||||
runs-on: ubuntu-latest
|
||||
# Use GitHub-hosted runners to avoid self-hosted desktop contention and sleep/power events.
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -124,7 +329,7 @@ jobs:
|
||||
PLAYWRIGHT_WORKERS: 1
|
||||
PLAYWRIGHT_VIDEO_MODE: on-first-retry
|
||||
steps:
|
||||
- name: Repair self-hosted workspace permissions
|
||||
- name: Normalize workspace permissions
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||
@@ -300,9 +505,15 @@ jobs:
|
||||
if: >
|
||||
always() &&
|
||||
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch) &&
|
||||
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted') &&
|
||||
needs.build-and-unit.result == 'success' &&
|
||||
(github.event_name == 'schedule' || needs.e2e-pr.result == 'success')
|
||||
needs: [build-and-unit, e2e-pr]
|
||||
(github.event_name == 'schedule' || needs.e2e-pr.result == 'success') &&
|
||||
(
|
||||
github.event_name != 'workflow_dispatch' ||
|
||||
inputs.mode == 'full' ||
|
||||
needs.e2e-targeted.result == 'success'
|
||||
)
|
||||
needs: [build-and-unit, e2e-pr, e2e-targeted]
|
||||
name: E2E-full-${{ matrix.browser_label }}-${{ matrix.device }}-${{ matrix.role }}
|
||||
runs-on: ${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.runner == 'github-hosted' && '["ubuntu-latest"]' || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
|
||||
timeout-minutes: 60
|
||||
@@ -330,7 +541,7 @@ jobs:
|
||||
PLAYWRIGHT_WORKERS: 1
|
||||
PLAYWRIGHT_VIDEO_MODE: off
|
||||
steps:
|
||||
- name: Repair self-hosted workspace permissions
|
||||
- name: Normalize workspace permissions
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||
|
||||
@@ -96,8 +96,8 @@ export const sourceMappings = [
|
||||
{
|
||||
name: "pos",
|
||||
patterns: [
|
||||
/\/pos[/-]/iu,
|
||||
/POS/iu,
|
||||
/(?:^|[/_.-])pos(?:[/_.-]|$)/iu,
|
||||
/(?:^|\/)(?:POS|Pos)[A-Z][^/]*\.(?:vue|js|ts)$/u,
|
||||
/^src\/assets\/pos\.css$/u,
|
||||
/^src\/components\/displays\/boxes\/ProductBox\.vue$/u,
|
||||
/^src\/features\/customer\/customerProductRules\.js$/u,
|
||||
@@ -120,6 +120,7 @@ export const sourceMappings = [
|
||||
{
|
||||
name: "admin-department-notifications",
|
||||
patterns: [
|
||||
/^src\/views\/dashboards\/departmentDashboard\/modules\/notifications\/DepartmentNotifications\.vue$/u,
|
||||
/^src\/components\/displays\/department\/notifications\//u,
|
||||
/^src\/components\/displays\/pagination\/models\/DepartmentPos\/NotificationsPhonePagination\.vue$/u,
|
||||
],
|
||||
|
||||
+2
-1
@@ -38,7 +38,8 @@ if (router.currentRoute.value.params.departmentId) {
|
||||
<TableLabeledPagination :label="SessionUser.objects.department_notification_sms.meta.title">
|
||||
<template #buttons="{ loadList }">
|
||||
<button
|
||||
class="button is-info is-small"
|
||||
class="button is-primary is-small"
|
||||
data-testid="department-notification-sms-add-button"
|
||||
@click="SessionUser.objects.department_notification_sms.showCreateObjectForm(() => loadList(), {department: parseInt(router.currentRoute.value.params.departmentId)})"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
|
||||
+11
-11
@@ -14,6 +14,16 @@ import NotificationsPhonePagination
|
||||
:title="$t('admin.notifications.title')"
|
||||
:subtitle="$t('admin.notifications.subtitle')"
|
||||
>
|
||||
<header class="mb-5" data-testid="department-notifications-page-header">
|
||||
<h1 class="title is-3 mb-2" data-testid="department-notifications-title">
|
||||
{{ $t('admin.notifications.title') }}
|
||||
</h1>
|
||||
<p class="subtitle is-6 mb-0" data-testid="department-notifications-subtitle">
|
||||
<span>{{ $t('admin.notifications.sms_overview') }}</span>
|
||||
<br>
|
||||
<span>{{ $t('admin.notifications.sms_activated_description') }}</span>
|
||||
</p>
|
||||
</header>
|
||||
<NotFoundFallBackPageWrapper :exists="SessionUser.functions.getDepartmentIdFromUrl() && SessionUser.canAccessDepartment(SessionUser.functions.getDepartmentIdFromUrl())" :error="$t('admin.errors.select_department')">
|
||||
<element-tabs-box
|
||||
defaultActiveTab="sms"
|
||||
@@ -24,16 +34,6 @@ import NotificationsPhonePagination
|
||||
]"
|
||||
>
|
||||
<template #sms>
|
||||
<!-- What is this? -->
|
||||
<div class="message">
|
||||
<div class="message-header">
|
||||
<p>{{ $t('admin.notifications.sms_notifications') }}</p>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
<p>{{ $t('admin.notifications.sms_overview') }}</p>
|
||||
<p>{{ $t('admin.notifications.sms_activated_description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<NotificationsPhonePagination />
|
||||
</template>
|
||||
</element-tabs-box>
|
||||
@@ -44,4 +44,4 @@ import NotificationsPhonePagination
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -18,6 +18,8 @@ const json = (body: unknown, status = 200) => ({
|
||||
});
|
||||
const pageReadyTimeout = process.env.CI ? 30_000 : 15_000;
|
||||
|
||||
const pageBootstrapTimeoutMs = 45_000;
|
||||
|
||||
const setupNotificationSmsApi = async (page: Page) => {
|
||||
let listRequestCount = 0;
|
||||
const mutations: Array<{ method: string; body?: Record<string, unknown>; id?: number }> = [];
|
||||
@@ -118,6 +120,21 @@ test("@smoke department notification SMS active toggle and delete refresh the ta
|
||||
await page.goto("/admin/1/modules/notifications", { waitUntil: "domcontentloaded" });
|
||||
await expect.poll(() => notificationSmsApi.listRequestCount, { timeout: pageReadyTimeout }).toBeGreaterThan(0);
|
||||
|
||||
const pageTitle = page.getByTestId("department-notifications-title");
|
||||
await expect(pageTitle).toBeVisible({ timeout: pageBootstrapTimeoutMs });
|
||||
await expect(pageTitle).toContainText(/notifikationer|notifications/i);
|
||||
|
||||
const pageSubtitle = page.getByTestId("department-notifications-subtitle");
|
||||
await expect(pageSubtitle).toBeVisible();
|
||||
await expect(pageSubtitle).toContainText(/sms/i);
|
||||
await expect(page.locator(".message").filter({ hasText: /sms/i })).toHaveCount(0);
|
||||
|
||||
const addPhoneButton = page.getByTestId("department-notification-sms-add-button");
|
||||
await expect(addPhoneButton).toBeVisible();
|
||||
await expect(addPhoneButton).toHaveClass(/is-primary/);
|
||||
await expect(addPhoneButton).not.toHaveClass(/is-info/);
|
||||
await expect(addPhoneButton.locator(".fa-plus")).toBeVisible();
|
||||
|
||||
const row = page.locator("tr", { hasText: "Dispatch line" });
|
||||
await expect(row).toBeVisible({ timeout: pageReadyTimeout });
|
||||
const enabledToggle = page.getByTestId("department-notification-sms-enabled-toggle-501");
|
||||
|
||||
@@ -22,6 +22,11 @@ const REVIEWED_NON_LITERAL_CALLS = new Set([
|
||||
"src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue|t|const tr = (key, params = {}) => t(`configuration.release_manager.${key}`, params);",
|
||||
"src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue|t|const translated = t(fullKey, params);",
|
||||
"src/views/dashboards/superUserDashboard/ErrorReports.vue|t|const value = t(key);",
|
||||
"src/views/backoffice/LimitedBackofficeEmployees.vue|t|return `${option.flag} +${option.value} ${t(`templates.limited_backoffice.employees.country_codes.${option.labelKey}`)}`;",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|label: t(`roles.permissions.page_access.${permission}.label`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|description: t(`roles.permissions.page_access.${permission}.description`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|label: t(`roles.permissions.groups.${group.key}.label`),",
|
||||
"src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue|t|description: t(`roles.permissions.groups.${group.key}.description`),",
|
||||
'src/views/dashboards/userDashboard/wash/components/LaneSelectionSection.vue|$t|:title="option.titleKey ? $t(option.titleKey) : null"',
|
||||
'src/views/dashboards/userDashboard/wash/components/LaneSelectionSection.vue|$t|:aria-label="option.titleKey ? $t(option.titleKey) : null"',
|
||||
"src/views/dashboards/userDashboard/wash/components/LaneSelectionSection.vue|$t|{{ $t(option.statusLabelKey) }}",
|
||||
|
||||
@@ -29,7 +29,7 @@ async function primeSession(page, { token, permissions, sessionData = {} }) {
|
||||
|
||||
async function mockSelectedSubuserGrant(
|
||||
page,
|
||||
{ customerNumber = 12345679, permissions = ["SELFSERVE_LIST", "SELFSERVE_ADD"] } = {}
|
||||
{ customerNumber = 12345679, permissions = ["user", "SELFSERVE_LIST", "SELFSERVE_ADD"] } = {}
|
||||
) {
|
||||
await page.route("**/subusers/me", async (route) => {
|
||||
await route.fulfill({
|
||||
@@ -252,6 +252,7 @@ test.describe("Self-serve wash", () => {
|
||||
},
|
||||
selfServe: true,
|
||||
});
|
||||
api.selfServe.commandResponseDelayMs = [500, 0];
|
||||
await primeSession(page, {
|
||||
token: "self-serve-user-token",
|
||||
permissions: ["user"],
|
||||
@@ -1048,7 +1049,7 @@ test.describe("Self-serve wash", () => {
|
||||
customerNumberInput: "777",
|
||||
});
|
||||
|
||||
await mockApi(page, {
|
||||
const api = await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["user"],
|
||||
sessionData: {
|
||||
@@ -1066,6 +1067,25 @@ test.describe("Self-serve wash", () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
api.selfServe.previewByKey["7:ZZ00000"] = {
|
||||
...api.selfServe.previewByKey["7:ZZ00000"],
|
||||
questions: [],
|
||||
tasks: [],
|
||||
};
|
||||
api.selfServe.summaryBySessionId[601] = {
|
||||
...api.selfServe.summaryBySessionId[601],
|
||||
questions: [],
|
||||
tasks: [],
|
||||
};
|
||||
api.selfServe.summaryByKey["7:ZZ00000"] = {
|
||||
session: api.selfServe.previewByKey["7:ZZ00000"].session,
|
||||
lane: api.selfServe.previewByKey["7:ZZ00000"].lane,
|
||||
questions: [],
|
||||
conditions: [],
|
||||
rules: [],
|
||||
tasks: [],
|
||||
events: [{ id: 4, type: "STARTED", created_at: "2026-01-01T11:01:00.000Z" }],
|
||||
};
|
||||
await primeSession(page, {
|
||||
token: "self-serve-start-retry-token",
|
||||
permissions: ["user"],
|
||||
@@ -1166,6 +1186,7 @@ test.describe("Self-serve wash", () => {
|
||||
authenticated: true,
|
||||
permissions: ["user"],
|
||||
selfServe: {
|
||||
commandResponseDelayMs: [500, 0],
|
||||
commandResponses: [
|
||||
{
|
||||
success: false,
|
||||
|
||||
@@ -1317,6 +1317,7 @@ function createSelfServeFixture(overrides = {}) {
|
||||
answerRequests: [],
|
||||
commandResponse: { success: true },
|
||||
commandResponses: null,
|
||||
commandResponseDelayMs: 0,
|
||||
commandRequests: [],
|
||||
forceStopResponse: null,
|
||||
forceStopResponses: null,
|
||||
@@ -1483,6 +1484,14 @@ function buildEdgeGatewayOperationSummary(operations = []) {
|
||||
);
|
||||
}
|
||||
|
||||
function invalidateEdgeGatewayRuntimeSnapshot(gateway) {
|
||||
delete gateway.active_operation;
|
||||
delete gateway.recent_operations_summary;
|
||||
delete gateway.version_drift;
|
||||
delete gateway.diagnostics;
|
||||
delete gateway.error_state;
|
||||
}
|
||||
|
||||
function buildEdgeGatewayRuntimeFixture(gateway) {
|
||||
const relayHealth = (gateway.bindings || []).map((binding) => {
|
||||
const fallbackMode = binding.fallback_mode || "PREFER_LOCAL";
|
||||
@@ -1850,6 +1859,7 @@ function settleEdgeGatewayWork(edgeGatewayFixture, gatewayId) {
|
||||
: operation
|
||||
);
|
||||
delete edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId];
|
||||
invalidateEdgeGatewayRuntimeSnapshot(gateway);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5401,6 +5411,7 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
|
||||
{ id: Date.now(), created_at: now, action: "GATEWAY_OPERATION_CANCELLED", actor_type: "USER" },
|
||||
...(gateway.audit_logs || []),
|
||||
];
|
||||
invalidateEdgeGatewayRuntimeSnapshot(gateway);
|
||||
Object.assign(gateway, buildEdgeGatewayRuntimeFixture(gateway));
|
||||
await route.fulfill(
|
||||
json({
|
||||
@@ -5567,6 +5578,7 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
|
||||
{ id: Date.now(), created_at: now, action: "GATEWAY_OPERATION_QUEUED", actor_type: "USER" },
|
||||
...(gateway.audit_logs || []),
|
||||
];
|
||||
invalidateEdgeGatewayRuntimeSnapshot(gateway);
|
||||
Object.assign(gateway, buildEdgeGatewayRuntimeFixture(gateway));
|
||||
await route.fulfill(
|
||||
json(
|
||||
@@ -6697,6 +6709,10 @@ export async function mockApi(page, options = {}) {
|
||||
Array.isArray(selfServe.commandResponses) && selfServe.commandResponses.length > 0
|
||||
? selfServe.commandResponses.shift()
|
||||
: selfServe.commandResponse;
|
||||
const commandResponseDelayMs = Array.isArray(selfServe.commandResponseDelayMs)
|
||||
? selfServe.commandResponseDelayMs.shift() || 0
|
||||
: selfServe.commandResponseDelayMs;
|
||||
await maybeDelayFixtureResponse(commandResponseDelayMs);
|
||||
await route.fulfill(json(commandResponse));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,10 +42,20 @@ describe("Playwright full E2E workflow grouping", () => {
|
||||
expect(source).toContain("scripts/ci/runner-diagnostics.sh");
|
||||
});
|
||||
|
||||
it("diffs pull request changed-area tests against the PR head instead of the merge commit", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source).toContain("PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}");
|
||||
expect(source).toMatch(
|
||||
/if \[\[ "\$EVENT_NAME" == "pull_request" && -n "\$PR_BASE_SHA" \]\]; then[\s\S]*?head_ref="\$PR_HEAD_SHA"/u
|
||||
);
|
||||
expect(source).toContain('echo "head=$head_ref" >> "$GITHUB_OUTPUT"');
|
||||
});
|
||||
|
||||
it("keeps PR E2E runner pressure bounded and diagnosable", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?runs-on: ubuntu-latest/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?runs-on: ubuntu-24\.04/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?max-parallel: 4/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?PLAYWRIGHT_WORKERS: 1/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?PLAYWRIGHT_VIDEO_MODE: on-first-retry/u);
|
||||
@@ -53,11 +63,38 @@ describe("Playwright full E2E workflow grouping", () => {
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?--env PLAYWRIGHT_VIDEO_MODE="\$PLAYWRIGHT_VIDEO_MODE"/u);
|
||||
});
|
||||
|
||||
it("supports GitHub-hosted manual runners while keeping PR E2E on Ubuntu", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source).toContain("github.event.inputs.runner == 'github-hosted'");
|
||||
expect(source).toContain('["ubuntu-latest"]');
|
||||
expect(source).toContain('["self-hosted","Linux","X64","pleno","frontend"]');
|
||||
expect(source).toMatch(/e2e-targeted:[\s\S]*?runs-on: ubuntu-24\.04/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?runs-on: ubuntu-24\.04/u);
|
||||
expect(source).toMatch(/e2e-full:[\s\S]*?frontend","docker"\]/u);
|
||||
});
|
||||
|
||||
it("supports targeted manual Playwright reruns on GitHub-hosted runners", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source).toContain("workflow_dispatch:");
|
||||
expect(source).toContain("targeted-then-full");
|
||||
expect(source).toContain("target_specs:");
|
||||
expect(source).toContain("target_projects:");
|
||||
expect(source).toContain("e2e-targeted:");
|
||||
expect(source).toContain("matrix:");
|
||||
expect(source).toContain("project: ${{ fromJSON(inputs.target_projects || '[\"chromium-desktop\"]') }}");
|
||||
expect(source).toMatch(/e2e-targeted:[\s\S]*?runs-on: ubuntu-24\.04/u);
|
||||
expect(source).toContain('npx playwright test "${args[@]}"');
|
||||
expect(source).toContain('"$spec_path" != tests/e2e/*');
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?needs: \[build-and-unit, e2e-targeted\]/u);
|
||||
});
|
||||
|
||||
it("uses a machine-wide Playwright port lock across self-hosted runner processes", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source.match(/PLAYWRIGHT_PORT_LOCK_ROOT:-\/tmp\/pleno-playwright-port-locks/gu)).toHaveLength(2);
|
||||
expect(source.match(/chmod 1777 "\$lock_root"/gu)).toHaveLength(2);
|
||||
expect(source.match(/PLAYWRIGHT_PORT_LOCK_ROOT:-\/tmp\/pleno-playwright-port-locks/gu)).toHaveLength(3);
|
||||
expect(source.match(/chmod 1777 "\$lock_root"/gu)).toHaveLength(3);
|
||||
expect(source).not.toContain("${RUNNER_TEMP:-/tmp}/pleno-playwright-port-locks");
|
||||
});
|
||||
|
||||
|
||||
@@ -31,12 +31,20 @@ describe("Playwright PR mapping", () => {
|
||||
});
|
||||
|
||||
it("maps department notification table changes to the admin notification E2E coverage", () => {
|
||||
const notificationPaginationSpecs = specsFor(
|
||||
"src/components/displays/pagination/models/DepartmentPos/NotificationsPhonePagination.vue"
|
||||
);
|
||||
|
||||
expect(
|
||||
specsFor("src/views/dashboards/departmentDashboard/modules/notifications/DepartmentNotifications.vue")
|
||||
).toContain("tests/e2e/admin-department-notifications.spec.ts");
|
||||
expect(
|
||||
specsFor("src/components/displays/department/notifications/departmentNotificationsPhoneTable.vue")
|
||||
).toContain("tests/e2e/admin-department-notifications.spec.ts");
|
||||
expect(
|
||||
specsFor("src/components/displays/pagination/models/DepartmentPos/NotificationsPhonePagination.vue")
|
||||
).toContain("tests/e2e/admin-department-notifications.spec.ts");
|
||||
expect(notificationPaginationSpecs).toContain("tests/e2e/admin-department-notifications.spec.ts");
|
||||
expect(notificationPaginationSpecs).not.toContain("tests/e2e/pos-flow.spec.js");
|
||||
expect(notificationPaginationSpecs).not.toContain("tests/e2e/pos-mobile-order-flow.spec.js");
|
||||
expect(notificationPaginationSpecs).not.toContain("tests/e2e/admin-pos-orders.spec.ts");
|
||||
});
|
||||
|
||||
it("maps admin daily-report changes to daily-report E2E coverage", () => {
|
||||
@@ -95,6 +103,7 @@ describe("Playwright PR mapping", () => {
|
||||
it("maps customer product rule changes to POS customer rule E2E coverage", () => {
|
||||
expect(specsFor("src/features/customer/customerProductRules.js")).toContain("tests/e2e/pos-customer-rules.spec.js");
|
||||
expect(specsFor("src/components/displays/boxes/ProductBox.vue")).toContain("tests/e2e/pos-customer-rules.spec.js");
|
||||
expect(specsFor("src/components/shop/POSDepartmentProcess.vue")).toContain("tests/e2e/admin-pos-orders.spec.ts");
|
||||
});
|
||||
|
||||
it("maps limited backoffice changes to the limited backoffice E2E coverage", () => {
|
||||
|
||||
Reference in New Issue
Block a user