Merge remote-tracking branch 'origin/master' into codex/backoffice-title-department-selector

# Conflicts:
#	.github/workflows/tests.yml
This commit is contained in:
Jeppe Bundgaard
2026-07-06 22:34:43 +02:00
10 changed files with 348 additions and 231 deletions
+221 -211
View File
@@ -7,57 +7,38 @@ 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
default: self-hosted
default: "self-hosted"
type: choice
options:
- self-hosted
- github-hosted
run_scope:
description: Test scope for manually dispatched runs.
required: false
type: choice
default: all
options:
- all
- focused-e2e
focused_role:
description: Role slice for focused E2E runs.
required: false
type: choice
default: customer
options:
- superuser
- admin
- customer
- subuser
focused_project:
description: Playwright project for focused E2E runs.
required: false
type: choice
default: webkit-mobile
options:
- chromium-mobile
- chromium-desktop
- chromium-tablet
- webkit-mobile
- webkit-desktop
- webkit-tablet
- firefox-mobile
- firefox-desktop
- firefox-tablet
focused_spec:
description: Optional spec file or path pattern for focused E2E runs.
required: false
type: string
default: tests/e2e/limited-backoffice.spec.ts
focused_grep:
description: Optional Playwright grep pattern for focused E2E runs.
required: false
type: string
default: limits price management to assigned departments and explicit prices
schedule:
- cron: "0 2 * * *"
@@ -70,12 +51,11 @@ concurrency:
jobs:
format-tests:
if: github.event_name != 'workflow_dispatch' || inputs.run_scope != 'focused-e2e'
# CI normally runs on the repository's self-hosted pool; manual runs can opt into GitHub-hosted runners.
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
@@ -107,12 +87,11 @@ jobs:
run: npm run format:tests:check
build-and-unit:
if: github.event_name != 'workflow_dispatch' || inputs.run_scope != 'focused-e2e'
needs: format-tests
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
@@ -151,11 +130,192 @@ jobs:
env:
VITEST_BATCH_SIZE: 5
e2e-pr:
if: github.event_name != 'schedule' && (github.event_name != 'workflow_dispatch' || inputs.run_scope != 'focused-e2e')
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
@@ -169,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
@@ -341,169 +501,19 @@ jobs:
if-no-files-found: ignore
retention-days: 1
e2e-focused:
if: github.event_name == 'workflow_dispatch' && inputs.run_scope == 'focused-e2e'
name: E2E-focused-${{ inputs.focused_project }}-${{ inputs.focused_role }}
runs-on: ${{ fromJSON(github.event.inputs.runner == 'github-hosted' && '["ubuntu-latest"]' || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
timeout-minutes: 45
env:
FOCUSED_GREP: ${{ inputs.focused_grep }}
FOCUSED_SPEC: ${{ inputs.focused_spec }}
MATRIX_PROJECT: ${{ inputs.focused_project }}
MATRIX_ROLE: ${{ inputs.focused_role }}
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-focused-${{ inputs.focused_project }}-${{ inputs.focused_role }}
PLAYWRIGHT_REPORTER_MODE: line-html
PLAYWRIGHT_VIDEO_MODE: on-first-retry
PLAYWRIGHT_WORKERS: 1
RUN_ID: ${{ github.run_id }}
steps:
- name: Repair self-hosted 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: Run focused Playwright full slice in container
shell: bash
run: |
set -euo pipefail
case "$MATRIX_ROLE" in
superuser) role_offset=0 ;;
admin) role_offset=100 ;;
customer) role_offset=200 ;;
subuser) role_offset=300 ;;
*) echo "Unsupported Playwright role: $MATRIX_ROLE" >&2; exit 1 ;;
esac
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) + role_offset + 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 focused Playwright ${MATRIX_PROJECT}/${MATRIX_ROLE}" -- "${docker_cmd[@]}"
SYSTEMD_INHIBIT_REASON="Focused frontend Playwright ${MATRIX_PROJECT}/${MATRIX_ROLE}" \
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 FOCUSED_GREP="$FOCUSED_GREP" \
--env FOCUSED_SPEC="$FOCUSED_SPEC" \
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
--env MATRIX_ROLE="$MATRIX_ROLE" \
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
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
forwarded_args=()
if [[ -n "${FOCUSED_SPEC:-}" ]]; then
forwarded_args+=("$FOCUSED_SPEC")
fi
if [[ -n "${FOCUSED_GREP:-}" ]]; then
forwarded_args+=(--grep "$FOCUSED_GREP")
fi
npm run test:e2e:full:slice -- --role="$MATRIX_ROLE" --project="$MATRIX_PROJECT" -- "${forwarded_args[@]}"
'
- name: Runner diagnostics after Playwright failure
if: failure() || cancelled()
continue-on-error: true
run: scripts/ci/runner-diagnostics.sh "after focused Playwright ${{ inputs.focused_project }}/${{ inputs.focused_role }}"
- name: Upload Playwright report
if: failure() || cancelled()
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: playwright-report-focused-${{ inputs.focused_project }}-${{ inputs.focused_role }}
path: |
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/report
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/test-results
output/playwright/test-lists/${{ inputs.focused_project }}-${{ inputs.focused_role }}.txt
if-no-files-found: ignore
retention-days: 1
e2e-full:
if: >
always() &&
(github.event_name != 'workflow_dispatch' || inputs.run_scope != 'focused-e2e') &&
(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
@@ -531,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
+3 -2
View File
@@ -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,
],
@@ -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">
@@ -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");
+5
View File
@@ -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) }}",
+23 -2
View File
@@ -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,
+16
View File
@@ -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;
}
+38 -1
View File
@@ -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,6 +63,33 @@ 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();
+12 -3
View File
@@ -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", () => {
@@ -96,6 +104,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", () => {