From 89dff754165609e558b61f9fa1ee152f8052ca88 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Sun, 14 Jun 2026 16:03:45 +0200 Subject: [PATCH] Fix frontend e2e browser env and POS camera preview --- .github/workflows/tests.yml | 177 +++++++++++++----- .../scanner/graphics/ScannerCamera.vue | 15 +- .../scanner-camera-capture-enabled.spec.js | 13 +- 3 files changed, 133 insertions(+), 72 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8a28d9a8..2f8aa162 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,20 @@ jobs: runs-on: [self-hosted, Linux, X64, pleno, frontend] timeout-minutes: 15 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 @@ -44,6 +58,20 @@ jobs: runs-on: [self-hosted, Linux, X64, pleno, frontend] timeout-minutes: 30 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 @@ -85,6 +113,20 @@ jobs: PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-pr-${{ matrix.suite }}-${{ matrix.project }} PLAYWRIGHT_REPORTER_MODE: line-html 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 with: @@ -113,20 +155,11 @@ jobs: echo "base=$base_ref" >> "$GITHUB_OUTPUT" echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT" - - name: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: 22 - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Install Playwright browsers - run: node scripts/install-playwright-browsers.mjs chromium - - - name: Set Playwright dev server port + - name: Run Playwright PR suite in container shell: bash env: + DIFF_BASE_REF: ${{ steps.playwright-diff.outputs.base }} + DIFF_HEAD_REF: ${{ steps.playwright-diff.outputs.head }} MATRIX_SUITE: ${{ matrix.suite }} MATRIX_PROJECT: ${{ matrix.project }} RUN_ID: ${{ github.run_id }} @@ -143,25 +176,41 @@ jobs: chromium-mobile) project_offset=2 ;; *) echo "Unsupported Playwright PR project: $MATRIX_PROJECT" >&2; exit 1 ;; esac - echo "PLAYWRIGHT_DEV_PORT=$((10000 + workflow_offset + suite_offset + project_offset))" >> "$GITHUB_ENV" - - - name: Run Playwright smoke tests - if: matrix.suite == 'core' - run: | - ulimit -n 16384 || true - npx playwright test --grep @smoke --project="${{ matrix.project }}" - - - name: Run Playwright PR core tests - if: matrix.suite == 'core' - run: | - ulimit -n 16384 || true - npm run test:e2e:pr -- --core-only --project="${{ matrix.project }}" - - - name: Run Playwright changed-area tests - if: matrix.suite == 'changed' - run: | - ulimit -n 16384 || true - npm run test:e2e:pr -- --changed-only --project="${{ matrix.project }}" --base="${{ steps.playwright-diff.outputs.base }}" --head="${{ steps.playwright-diff.outputs.head }}" + playwright_dev_port=$((10000 + workflow_offset + suite_offset + project_offset)) + 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 + "${docker_cmd[@]}" run --rm --ipc=host --network host \ + --volume "$PWD:/source:ro" \ + --workdir /work \ + --env HOME=/tmp \ + --env CI="${CI:-}" \ + --env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \ + --env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \ + --env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \ + --env MATRIX_SUITE="$MATRIX_SUITE" \ + --env MATRIX_PROJECT="$MATRIX_PROJECT" \ + --env DIFF_BASE_REF="$DIFF_BASE_REF" \ + --env DIFF_HEAD_REF="$DIFF_HEAD_REF" \ + mcr.microsoft.com/playwright:v1.58.2-noble \ + bash -lc ' + set -euo pipefail + cp -a /source/. /work + git config --global --add safe.directory /work + npm ci --legacy-peer-deps + ulimit -n 16384 || true + if [[ "$MATRIX_SUITE" == "core" ]]; then + npx playwright test --grep @smoke --project="$MATRIX_PROJECT" + npm run test:e2e:pr -- --core-only --project="$MATRIX_PROJECT" + else + npm run test:e2e:pr -- --changed-only --project="$MATRIX_PROJECT" --base="$DIFF_BASE_REF" --head="$DIFF_HEAD_REF" + fi + ' - name: Upload Playwright report if: failure() || cancelled() @@ -173,7 +222,7 @@ jobs: output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}-* output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }} if-no-files-found: ignore - retention-days: 3 + retention-days: 1 e2e-full: if: > @@ -206,21 +255,24 @@ jobs: PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }} PLAYWRIGHT_REPORTER_MODE: line-html 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: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: 22 - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Install Playwright browsers - run: node scripts/install-playwright-browsers.mjs ${{ matrix.browser_install }} - - - name: Set Playwright dev server port + - name: Run full Playwright slice in container shell: bash env: MATRIX_ROLE: ${{ matrix.role }} @@ -249,12 +301,35 @@ jobs: desktop) device_offset=3 ;; *) echo "Unsupported Playwright device: $MATRIX_DEVICE" >&2; exit 1 ;; esac - echo "PLAYWRIGHT_DEV_PORT=$((10000 + workflow_offset + role_offset + browser_offset + device_offset))" >> "$GITHUB_ENV" - - - name: Run full Playwright slice - run: | - ulimit -n 16384 || true - npm run test:e2e:full:slice -- --role="${{ matrix.role }}" --project="${{ matrix.browser }}-${{ matrix.device }}" + playwright_dev_port=$((10000 + workflow_offset + role_offset + browser_offset + device_offset)) + 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 + "${docker_cmd[@]}" run --rm --ipc=host --network host \ + --volume "$PWD:/source:ro" \ + --workdir /work \ + --env HOME=/tmp \ + --env CI="${CI:-}" \ + --env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \ + --env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \ + --env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \ + --env MATRIX_ROLE="$MATRIX_ROLE" \ + --env MATRIX_BROWSER="$MATRIX_BROWSER" \ + --env MATRIX_DEVICE="$MATRIX_DEVICE" \ + mcr.microsoft.com/playwright:v1.58.2-noble \ + bash -lc ' + set -euo pipefail + cp -a /source/. /work + git config --global --add safe.directory /work + npm ci --legacy-peer-deps + ulimit -n 16384 || true + npm run test:e2e:full:slice -- --role="$MATRIX_ROLE" --project="$MATRIX_BROWSER-$MATRIX_DEVICE" + ' - name: Upload Playwright report if: failure() || cancelled() @@ -267,4 +342,4 @@ jobs: output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/test-results output/playwright/test-lists/${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}.txt if-no-files-found: ignore - retention-days: 3 + retention-days: 1 diff --git a/src/components/viewport/page/templates/scanner/graphics/ScannerCamera.vue b/src/components/viewport/page/templates/scanner/graphics/ScannerCamera.vue index aaa4c103..0f354702 100644 --- a/src/components/viewport/page/templates/scanner/graphics/ScannerCamera.vue +++ b/src/components/viewport/page/templates/scanner/graphics/ScannerCamera.vue @@ -46,7 +46,6 @@ const cameraErrorKey = ref('pos.camera_permission_denied'); let captureIntervalId: ReturnType | null = null; let firstCaptureTimeoutId: ReturnType | null = null; let isFrameCaptureInProgress = false; -let isLivePreviewPausedForFrameEncode = false; let hasRequestedVideoPreviewPlay = false; let lastAppliedTrackEnabled: boolean | null = null; let cachedRelativeFocusViewportRect: LPRFrameViewportRect | null = null; @@ -59,7 +58,7 @@ const isDocumentVisible = (): boolean => typeof document === 'undefined' || document.visibilityState !== 'hidden'; const shouldRunLivePreview = (): boolean => - !props.pausePreview && !isLivePreviewPausedForFrameEncode && isDocumentVisible(); + !props.pausePreview && isDocumentVisible(); const canCaptureFrames = (): boolean => isCameraActive.value && props.captureEnabled && !props.pausePreview && isDocumentVisible(); @@ -245,15 +244,6 @@ const syncVideoPreviewPlayback = () => { playVideoPreview(videoRef.value); }; -const pauseLivePreviewForFrameEncode = () => { - if (isLivePreviewPausedForFrameEncode || !isCameraActive.value) { - return; - } - - isLivePreviewPausedForFrameEncode = true; - syncVideoPreviewPlayback(); -}; - const applyCameraStream = (stream: MediaStream) => { isCameraActive.value = true; isCameraMounted.value = true; @@ -406,7 +396,6 @@ function stopCamera() { clearFirstCaptureTimeout(); clearCaptureInterval(); clearRelativeFocusViewportRectCache(); - isLivePreviewPausedForFrameEncode = false; hasRequestedVideoPreviewPlay = false; lastAppliedTrackEnabled = null; if (cameraStream.value) { @@ -511,7 +500,6 @@ const getFrameCaptureOptions = () => { focusViewportRect: shouldUseFocusedCrop ? getRelativeFocusViewportRect(videoViewportRect) : null, shouldBuildVisualFingerprint: shouldUseFocusedCrop ? props.shouldBuildVisualFingerprint : undefined, shouldEncode: shouldUseFocusedCrop ? props.shouldEncodeFrame : undefined, - ...(shouldUseFocusedCrop ? { onFrameDrawn: pauseLivePreviewForFrameEncode } : {}), ...(videoViewportRect ? { viewportHeight: videoViewportRect.height, @@ -541,7 +529,6 @@ const getFrame = () => { return nextTick().then(() => frameData); }) .finally(() => { - isLivePreviewPausedForFrameEncode = false; isFrameCaptureInProgress = false; syncVideoPreviewPlayback(); }); diff --git a/tests/unit/scanner-camera-capture-enabled.spec.js b/tests/unit/scanner-camera-capture-enabled.spec.js index 97df7ebb..373978cb 100644 --- a/tests/unit/scanner-camera-capture-enabled.spec.js +++ b/tests/unit/scanner-camera-capture-enabled.spec.js @@ -214,7 +214,6 @@ describe("ScannerCamera capture gating", () => { { focusCrop: true, focusViewportRect: null, - onFrameDrawn: expect.any(Function), shouldBuildVisualFingerprint: undefined, shouldEncode: undefined, visualFingerprintCanvas: expect.any(HTMLCanvasElement), @@ -228,12 +227,11 @@ describe("ScannerCamera capture gating", () => { wrapper.unmount(); }); - it("pauses the live preview only after the current frame has been drawn for encoding", async () => { + it("keeps the live preview running while the current frame is encoded", async () => { + const pause = HTMLMediaElement.prototype.pause; let resolveFrame; - mocks.captureVideoFrameBlobForLPR.mockImplementationOnce((_video, _canvas, options) => { + mocks.captureVideoFrameBlobForLPR.mockImplementationOnce(() => { expect(mocks.videoTrack.enabled).toBe(true); - options.onFrameDrawn(); - expect(mocks.videoTrack.enabled).toBe(false); return new Promise((resolve) => { resolveFrame = resolve; @@ -250,7 +248,8 @@ describe("ScannerCamera capture gating", () => { await flushPromises(); expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1); - expect(mocks.videoTrack.enabled).toBe(false); + expect(mocks.videoTrack.enabled).toBe(true); + expect(pause).not.toHaveBeenCalled(); resolveFrame({ blob: new Blob(["encoded-frame"], { type: "image/jpeg" }), @@ -263,6 +262,7 @@ describe("ScannerCamera capture gating", () => { await flushPromises(); expect(mocks.videoTrack.enabled).toBe(true); + expect(pause).not.toHaveBeenCalled(); expect(wrapper.emitted("update:frame")?.[0]?.[0]).toMatchObject({ filename: "encoded-frame.jpg", fingerprint: "encoded-frame", @@ -545,7 +545,6 @@ describe("ScannerCamera capture gating", () => { x: 5, y: 10, }, - onFrameDrawn: expect.any(Function), shouldBuildVisualFingerprint: undefined, shouldEncode: undefined, viewportHeight: 844,