Fix frontend e2e browser env and POS camera preview

This commit is contained in:
Jeppe Bundgaard
2026-06-14 18:18:14 +02:00
parent 8a790582f4
commit 89dff75416
3 changed files with 133 additions and 72 deletions
+126 -51
View File
@@ -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
@@ -46,7 +46,6 @@ const cameraErrorKey = ref('pos.camera_permission_denied');
let captureIntervalId: ReturnType<typeof window.setInterval> | null = null;
let firstCaptureTimeoutId: ReturnType<typeof window.setTimeout> | 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();
});
@@ -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,