Fix frontend e2e browser env and POS camera preview
This commit is contained in:
+126
-51
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user