mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-22 10:42:10 +10:00
feat: add reproducible portal README media
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
ARG PLAYWRIGHT_VERSION=1.63.0
|
||||
FROM mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble
|
||||
FROM mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble AS portal-contract
|
||||
|
||||
ARG PLAYWRIGHT_VERSION
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
@@ -8,4 +8,12 @@ COPY tests/portal-contract/package.json tests/portal-contract/package-lock.json
|
||||
RUN npm ci --ignore-scripts && \
|
||||
[ "$(node -p "require('@playwright/test/package.json').version")" = "$PLAYWRIGHT_VERSION" ]
|
||||
COPY tests/portal-contract/ ./
|
||||
CMD ["npm", "test"]
|
||||
RUN chmod 755 /work/run-portal-contract.sh /work/render-readme-media.sh
|
||||
CMD ["/work/run-portal-contract.sh"]
|
||||
|
||||
FROM portal-contract AS media
|
||||
USER root
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ffmpeg && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
USER pwuser
|
||||
|
||||
@@ -3,6 +3,7 @@ services:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: tests/portal-contract/Dockerfile
|
||||
target: ${PORTAL_CONTRACT_DOCKER_TARGET:-portal-contract}
|
||||
args:
|
||||
PLAYWRIGHT_VERSION: "1.63.0"
|
||||
network_mode: host
|
||||
@@ -13,6 +14,7 @@ services:
|
||||
HOME: /tmp
|
||||
PORTAL_URL: http://192.168.4.1
|
||||
PORTAL_BROWSER_MODE: ${PORTAL_BROWSER_MODE:-auto}
|
||||
PORTAL_CAPTURE_README_MEDIA: ${PORTAL_CAPTURE_README_MEDIA:-0}
|
||||
ARTIFACT_DIR: /artifacts
|
||||
volumes:
|
||||
- ${PORTAL_ARTIFACT_DIR:?portal artifact directory is required}:/artifacts
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
artifact_dir="${ARTIFACT_DIR:?ARTIFACT_DIR is required}"
|
||||
media_dir="$artifact_dir/readme-media"
|
||||
source_video="$media_dir/raw/portal-tour.webm"
|
||||
target_gif="$media_dir/portal-tour.gif"
|
||||
|
||||
[[ -s "$source_video" ]] || {
|
||||
echo "README media video was not recorded: $source_video" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ffmpeg -hide_banner -loglevel error -y -i "$source_video" \
|
||||
-filter_complex '[0:v]setpts=1.25*PTS,fps=6,scale=720:-2:flags=lanczos,split[a][b];[a]palettegen=max_colors=128[p];[b][p]paletteuse' \
|
||||
-loop 0 "$target_gif"
|
||||
|
||||
[[ -s "$target_gif" ]] || {
|
||||
echo "README media GIF was not rendered: $target_gif" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
duration="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$target_gif")"
|
||||
awk -v duration="$duration" 'BEGIN { exit !(duration >= 4 && duration <= 30) }' || {
|
||||
echo "README media GIF duration is outside the 4–30 second review range: $duration" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
max_bytes=$((2 * 1024 * 1024))
|
||||
(( $(wc -c < "$target_gif") <= max_bytes )) || {
|
||||
echo "README media GIF exceeds its 2 MiB documentation budget: $target_gif" >&2
|
||||
exit 1
|
||||
}
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
/work/node_modules/.bin/playwright test --config /work/playwright.config.js
|
||||
|
||||
if [[ "${PORTAL_CAPTURE_README_MEDIA:-0}" == "1" ]]; then
|
||||
/work/render-readme-media.sh
|
||||
fi
|
||||
@@ -0,0 +1,68 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
function mediaPath(...parts) {
|
||||
const root = process.env.ARTIFACT_DIR || '/artifacts';
|
||||
const target = path.join(root, 'readme-media', ...parts);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
||||
return target;
|
||||
}
|
||||
|
||||
async function waitForCompletedScan(request) {
|
||||
await expect.poll(async () => {
|
||||
try {
|
||||
const response = await request.get('/api/wifi/scan-status');
|
||||
if (!response.ok()) return false;
|
||||
const result = JSON.parse(await response.text());
|
||||
return result.state === 'complete' && result.results_valid && result.count > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, { timeout: 45_000, intervals: [500, 800, 1_000] }).toBe(true);
|
||||
}
|
||||
|
||||
test.describe('WiFiManager README media', () => {
|
||||
test.skip(process.env.PORTAL_CAPTURE_README_MEDIA !== '1', 'README capture was not requested.');
|
||||
|
||||
test('records an approved ESP32 portal tour', async ({ browser, request }) => {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 720, height: 900 },
|
||||
recordVideo: {
|
||||
dir: mediaPath('raw'),
|
||||
size: { width: 720, height: 900 },
|
||||
},
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') errors.push(message.text());
|
||||
});
|
||||
|
||||
const video = page.video();
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('#wm-reset-portal-timeout')).toBeVisible();
|
||||
// These pauses exist only in the README recording. The ordinary contract
|
||||
// remains timing-focused; this tour needs readable stable states.
|
||||
await page.waitForTimeout(1200);
|
||||
await page.screenshot({ path: mediaPath('portal-overview.png'), fullPage: true });
|
||||
|
||||
await page.locator('a[href="#/wifi"]').click();
|
||||
await expect(page.locator('#wm-refresh-scan')).toBeVisible();
|
||||
await waitForCompletedScan(request);
|
||||
await expect(page.locator('#wm-scan-results .wm-scan-row').first()).toBeVisible();
|
||||
await page.locator('#wm-f-installation_label').fill('Workshop sensor');
|
||||
await page.waitForTimeout(1200);
|
||||
await page.screenshot({ path: mediaPath('portal-wifi-settings.png'), fullPage: true });
|
||||
|
||||
await page.locator('#wm-refresh-scan').click();
|
||||
await expect(page.locator('#wm-wifi-scan-overlay')).toBeVisible();
|
||||
await page.waitForTimeout(1200);
|
||||
await context.close();
|
||||
|
||||
const source = await video.path();
|
||||
fs.renameSync(source, mediaPath('raw', 'portal-tour.webm'));
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user