mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-24 06:31:20 -08:00
test(browser): ENG-05 add real playback projects and artifact mapping
This commit is contained in:
1 parent
458bc3215d
commit
7318d33822
20 files changed
+1241
-9
No files matched your search
@@ -19,3 +19,9 @@ Use the pinned Node/Yarn toolchain from `../refactor/toolchain-setup.md`.
|
||||
Set `ARTPLAYER_TEST_CORE` to a built `.js`, `.legacy.js` or `.mjs` core file to add a candidate to the same public-contract run, then execute `node --test test/public-behavior.test.js`. An invalid path/export fails the run. This is a narrow event contract check, not isolated package installation or complete API compatibility; ENG-07 owns tarball consumption.
|
||||
|
||||
When a production module moves, update its loader mapping and its maintenance documentation, preserving the behavioral assertions. New contracts belong in contracts/; test-specific controlled state belongs in helpers/. Do not modify frozen refactor/fixtures or baseline captures just to pass changed behavior. Record historical defects and candidate fixes separately.
|
||||
|
||||
# Real browser tests
|
||||
|
||||
See [browser/README.md](browser/README.md) for `yarn test:browser`, browser installation,
|
||||
published/current combinations, media fixtures, candidate mapping and failure reports.
|
||||
These tests are separate from the fast Node suite and run in the Browser playback smoke CI job.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Browser regression entry
|
||||
|
||||
Use the pinned Node and Yarn versions, then run:
|
||||
|
||||
```sh
|
||||
yarn install --frozen-lockfile
|
||||
yarn test:browser:install
|
||||
yarn test:browser
|
||||
yarn test:browser --project=chromium
|
||||
yarn test:browser:report
|
||||
```
|
||||
|
||||
The three projects run actual Chromium, Firefox and WebKit. They use isolated browser contexts,
|
||||
no retries, a loopback server on 8084, and no existing dev server. Set `ARTPLAYER_BROWSER_PORT`
|
||||
to use another free port. The server builds core and chapter from current JS/TS entries in memory;
|
||||
it does not modify dist or docs outputs. Restart the test command after source changes.
|
||||
|
||||
`playback.spec.js` tests published/current core with current chapter, real decoded frame colors,
|
||||
media time advancing, pause, seek, source change to the existing docs video, destroy and real
|
||||
HTTP failure. These are initial smoke contracts, not full chapter or lifecycle coverage.
|
||||
`fixtures.js` captures browser version, actual scripts/media hashes, page errors, console errors,
|
||||
failed requests and final state. Tests attach decoded pixels and server-side media requests too.
|
||||
Windows WebKit media requests are not reliably visible to Playwright's page network events;
|
||||
use the loopback request log to prove that media was fetched. Its `videoWidth` can reflect layout
|
||||
size, so intrinsic-size compatibility is a separate test, not the playback-success predicate.
|
||||
|
||||
Reports, failure screenshots and traces are under `refactor/.cache/browser/`. They contain local
|
||||
test URLs and paths. JSON and HTML are regenerated each run; archive them before another run
|
||||
if evidence must be retained. A successful HTTP page fetch does not validate its editor UI.
|
||||
|
||||
## Files and routes
|
||||
|
||||
- `server.mjs`: current/published artifact provenance, docs serving, Range and controlled failures.
|
||||
- `player.html`: lightweight page, real ArtPlayer constructor and user-gesture play/pause buttons.
|
||||
- `fixtures.js`: per-test error and provenance attachment; contexts/storage are Playwright-owned.
|
||||
- `playback.spec.js`: behavior assertions shared by browser and core version.
|
||||
- `media/`: committed synthetic fixtures. CI needs no FFmpeg to use them.
|
||||
|
||||
`/test/manifest.json` identifies every mapped core/chapter artifact and synthetic media file by
|
||||
SHA-256. `/published/<package>.js` verifies the frozen npm archive/member from BASE-01.
|
||||
`/candidate/<package>.js` maps the current build. Existing docs `/compiled/<package>.js` and
|
||||
`/uncompiled/<package>/index.js` are aliases for that candidate. Other compiled/uncompiled paths
|
||||
fail explicitly; add a verified mapping before testing another package, rather than serving stale
|
||||
build output. Other docs pages, examples and samples use their existing paths without source edits.
|
||||
|
||||
`/test/pattern.mp4` and `.webm` support GET/HEAD and single byte ranges (including suffix ranges).
|
||||
Invalid ranges return 416. `/test/fail.mp4` always returns 503. Add a unique `?case=<test-id>` to
|
||||
media URLs and inspect `/test/requests.json?case=<test-id>` for actual server requests. The log is
|
||||
bounded and server-local; it is not a production endpoint or a substitute for decoded frames.
|
||||
|
||||
To test extracted candidate UMD files, set `ARTPLAYER_BROWSER_ARTIFACTS` to a JSON file:
|
||||
|
||||
```json
|
||||
{
|
||||
"artplayer": "./extracted/artplayer/dist/artplayer.js",
|
||||
"artplayer-plugin-chapter": "./extracted/chapter/dist/artplayer-plugin-chapter.js"
|
||||
}
|
||||
```
|
||||
|
||||
Paths resolve relative to that JSON. Both keys are required; missing files abort startup. No source
|
||||
fallback is allowed in this mode. ENG-07 owns tarball creation and broader consumer checks.
|
||||
For manual docs interaction, run `node test/browser/server.mjs` and open the existing editor URL
|
||||
on 8084 with chapter's libs/example parameters. EX-03 still owns the full Monaco Run/TS/storage
|
||||
workflow, all examples and external SDKs. The lightweight page does not complete that task.
|
||||
|
||||
## Synthetic media provenance
|
||||
|
||||
Generated locally from FFmpeg's `testsrc2`, without external footage or audio, using
|
||||
`ffmpeg version 2025-11-12-git-6cdd2cbe32-full_build-www.gyan.dev`:
|
||||
|
||||
```sh
|
||||
ffmpeg -f lavfi -i testsrc2=size=320x180:rate=24 -t 8 -an -c:v libx264 -profile:v baseline -pix_fmt yuv420p -movflags +faststart test/browser/media/pattern.mp4
|
||||
ffmpeg -f lavfi -i testsrc2=size=320x180:rate=24 -t 8 -an -c:v libvpx -b:v 160k test/browser/media/pattern.webm
|
||||
```
|
||||
|
||||
MP4 SHA-256: `0ad1d7ad286aec6e0fa9e27fc1e487e7277df2e603e30040fb942365763d0233`.
|
||||
WebM SHA-256: `9d3c2b3dec9581a599de2d6732992ef7846790e0d1368e6e131bd339b39e984f`.
|
||||
MP4 is the initial playback contract; WebM is available for subsequent codec-specific cases.
|
||||
Do not regenerate fixtures silently: update hashes and decoded-frame checks together.
|
||||
|
||||
The docs `assets/sample/video.mp4` remains an existing sample with its original provenance limits
|
||||
tracked by BASE-MEDIA-01. The new synthetic fixtures do not resolve rights for existing media.
|
||||
Browser binaries/codec support vary by OS; these projects do not replace Safari/iOS/Android
|
||||
device checks. See the [official browser guidance](https://playwright.dev/docs/browsers).
|
||||
@@ -0,0 +1,33 @@
|
||||
import process from 'node:process'
|
||||
import { test as base, expect } from '@playwright/test'
|
||||
|
||||
export const test = base.extend({
|
||||
diagnostics: [async ({ page, browser, request }, use, testInfo) => {
|
||||
const errors = []
|
||||
const consoleErrors = []
|
||||
const failedRequests = []
|
||||
page.on('pageerror', error => errors.push(error.message))
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error')
|
||||
consoleErrors.push(message.text())
|
||||
})
|
||||
page.on('requestfailed', request => failedRequests.push({ url: request.url(), resourceType: request.resourceType(), failure: request.failure() }))
|
||||
await use({ errors, consoleErrors, failedRequests })
|
||||
const manifest = await (await request.get('/test/manifest.json')).json()
|
||||
const state = page.isClosed()
|
||||
? null
|
||||
: await page.evaluate(() => {
|
||||
const video = document.querySelector('video')
|
||||
return {
|
||||
url: location.href,
|
||||
userAgent: navigator.userAgent,
|
||||
events: window.events,
|
||||
media: video ? { source: video.currentSrc, width: video.videoWidth, height: video.videoHeight, duration: video.duration, currentTime: video.currentTime, error: video.error?.code } : null,
|
||||
}
|
||||
}).catch(() => null)
|
||||
await testInfo.attach('browser-evidence', { contentType: 'application/json', body: JSON.stringify({ browser: browser.version(), platform: process.platform, manifest, state, errors, consoleErrors, failedRequests }, null, 2) })
|
||||
expect(errors, 'Unhandled browser errors').toEqual([])
|
||||
}, { auto: true }],
|
||||
})
|
||||
|
||||
export { expect }
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,100 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: real playback, pause, seek, switch and destroy`, async ({ page, request, diagnostics }, testInfo) => {
|
||||
const caseId = encodeURIComponent(testInfo.testId)
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.evaluate(url => window.createPlayer(url, true), `/test/pattern.mp4?case=${caseId}`)
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
expect(await page.locator('video').evaluate(video => video.duration)).toBeCloseTo(8, 1)
|
||||
await expect(page.locator('.art-chapter')).toHaveCount(1)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.locator('video').evaluate(video => video.currentTime)).toBeGreaterThan(0.25)
|
||||
// Read decoded frame colors, independent of WebKit Windows' layout-sized videoWidth.
|
||||
const pixels = await page.locator('video').evaluate((video) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 80
|
||||
canvas.height = 45
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(video, 0, 0, 80, 45)
|
||||
return [30, 45, 75].map(x => [...ctx.getImageData(x, 2, 1, 1).data])
|
||||
})
|
||||
expect(pixels[0][0]).toBeGreaterThan(180)
|
||||
expect(pixels[0][1]).toBeGreaterThan(180)
|
||||
expect(pixels[0][2]).toBeLessThan(80)
|
||||
expect(pixels[1][0]).toBeLessThan(80)
|
||||
expect(pixels[1][2]).toBeGreaterThan(180)
|
||||
expect(pixels[2][1]).toBeGreaterThan(180)
|
||||
expect(pixels[2][2]).toBeGreaterThan(180)
|
||||
const mediaRequests = await (await request.get(`/test/requests.json?case=${caseId}`)).json()
|
||||
expect(mediaRequests.some(item => item.path === '/test/pattern.mp4')).toBe(true)
|
||||
await testInfo.attach('decoded-frame', { body: JSON.stringify({ pixels, mediaRequests }), contentType: 'application/json' })
|
||||
await page.locator('#pause').click()
|
||||
await expect.poll(() => page.locator('video').evaluate(video => video.paused)).toBe(true)
|
||||
const pausedTime = await page.locator('video').evaluate(video => video.currentTime)
|
||||
// A bounded observation interval checks that pause stops the media clock.
|
||||
await page.waitForTimeout(200)
|
||||
expect(await page.locator('video').evaluate(video => video.currentTime)).toBeCloseTo(pausedTime, 2)
|
||||
await page.evaluate(() => {
|
||||
window.art.seek = 3
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.events.includes('video:seeked'))).toBe(true)
|
||||
expect(await page.locator('video').evaluate(video => video.currentTime)).toBeCloseTo(3, 1)
|
||||
// Reuse a real docs sample, independently of the short synthetic fixture.
|
||||
await page.evaluate(url => window.art.switchUrl(url), `/assets/sample/video.mp4?case=${caseId}`)
|
||||
await expect.poll(() => page.locator('video').evaluate(video => video.duration)).toBeGreaterThan(90)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.locator('video').evaluate(video => video.currentTime)).toBeGreaterThan(0.25)
|
||||
const events = await page.evaluate(() => window.events)
|
||||
expect(events).toContain('video:loadedmetadata')
|
||||
expect(events).toContain('ready')
|
||||
expect(events.indexOf('video:loadedmetadata')).toBeLessThan(events.indexOf('ready'))
|
||||
expect(events).toContain('video:playing')
|
||||
expect(events).not.toContain('video:error')
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
await expect(page.locator('.player')).toBeEmpty()
|
||||
expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0)
|
||||
expect(diagnostics.consoleErrors).toEqual([])
|
||||
// Chromium cancels media range requests as buffering, seeking and source ownership change.
|
||||
// Only exact cancellations of this test's two media URLs are expected; retain all in evidence.
|
||||
const unexpectedRequests = diagnostics.failedRequests.filter((item) => {
|
||||
const url = new URL(item.url)
|
||||
return !(item.resourceType === 'media' && item.failure?.errorText === 'net::ERR_ABORTED'
|
||||
&& url.origin === new URL(page.url()).origin
|
||||
&& url.searchParams.get('case') === testInfo.testId
|
||||
&& ['/test/pattern.mp4', '/assets/sample/video.mp4'].includes(url.pathname))
|
||||
})
|
||||
expect(unexpectedRequests).toEqual([])
|
||||
})
|
||||
}
|
||||
|
||||
test('controlled HTTP media failure emits a real video error', async ({ page, request }, testInfo) => {
|
||||
const caseId = encodeURIComponent(testInfo.testId)
|
||||
await page.goto('/test/player.html')
|
||||
await page.evaluate(url => window.createPlayer(url), `/test/fail.mp4?case=${caseId}`)
|
||||
await expect.poll(() => page.evaluate(() => window.events.includes('video:error'))).toBe(true)
|
||||
const requests = await (await request.get(`/test/requests.json?case=${caseId}`)).json()
|
||||
expect(requests.some(item => item.path === '/test/fail.mp4')).toBe(true)
|
||||
expect((await request.get('/test/fail.mp4')).status()).toBe(503)
|
||||
expect(await page.locator('video').evaluate(video => video.error.code)).toBeGreaterThan(0)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
await expect(page.locator('.player')).toBeEmpty()
|
||||
})
|
||||
|
||||
test('Range, media identity and docs candidate routes', async ({ request }) => {
|
||||
const manifest = await (await request.get('/test/manifest.json')).json()
|
||||
const candidate = await request.get('/candidate/artplayer.js')
|
||||
const alias = await request.get('/uncompiled/artplayer/index.js')
|
||||
expect(await alias.body()).toEqual(await candidate.body())
|
||||
expect(manifest.resources['/uncompiled/artplayer/index.js'].aliasOf).toBe('/candidate/artplayer.js')
|
||||
const full = await (await request.get('/test/pattern.mp4')).body()
|
||||
for (const [range, start, end] of [['bytes=0-31', 0, 32], ['bytes=-16', full.length - 16, full.length], ['bytes=32-', 32, full.length]]) {
|
||||
const response = await request.get('/test/pattern.mp4', { headers: { Range: range } })
|
||||
expect(response.status()).toBe(206)
|
||||
expect(await response.body()).toEqual(full.subarray(start, end))
|
||||
}
|
||||
expect((await request.get('/test/pattern.mp4', { headers: { Range: 'bytes=999999999-' } })).status()).toBe(416)
|
||||
expect((await request.get('/compiled/missing.js')).status()).toBe(404)
|
||||
expect((await request.get('/mobile.html')).status()).toBe(200)
|
||||
expect((await request.get('/?libs=./uncompiled/artplayer-plugin-chapter/index.js&example=chapter')).status()).toBe(200)
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>ArtPlayer browser contracts</title>
|
||||
<style>
|
||||
.player {
|
||||
width: 640px;
|
||||
height: 360px;
|
||||
}
|
||||
</style>
|
||||
<script src="/__CORE__/artplayer.js"></script>
|
||||
<script src="/__CHAPTER__/artplayer-plugin-chapter.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="player"></div>
|
||||
<button id="play">Play</button>
|
||||
<button id="pause">Pause</button>
|
||||
<script>
|
||||
window.createPlayer = (url, chapter = false) => {
|
||||
window.events = []
|
||||
window.art = new Artplayer({
|
||||
container: '.player',
|
||||
url,
|
||||
muted: true,
|
||||
plugins: chapter
|
||||
? [artplayerPluginChapter({ chapters: [{ start: 0, end: Infinity, title: 'Pattern' }] })]
|
||||
: [],
|
||||
})
|
||||
for (const name of [
|
||||
'video:loadedmetadata',
|
||||
'ready',
|
||||
'video:playing',
|
||||
'video:pause',
|
||||
'video:seeked',
|
||||
'video:error',
|
||||
'destroy',
|
||||
]) {
|
||||
art.on(name, () => events.push(name))
|
||||
}
|
||||
document.querySelector('#play').onclick = () => art.play()
|
||||
document.querySelector('#pause').onclick = () => art.pause()
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,141 @@
|
||||
/* eslint-disable antfu/no-top-level-await -- Build test resources before accepting requests. */
|
||||
import assert from 'node:assert/strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import fs from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { build } from 'vite'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { getEntryFile } from '../../scripts/projects.js'
|
||||
import { getGlobalName, getViteBuildConfig } from '../../scripts/utils.js'
|
||||
|
||||
const workspace = fileURLToPath(new URL('../../', import.meta.url))
|
||||
|
||||
const port = Number(process.env.ARTPLAYER_BROWSER_PORT || 8084)
|
||||
assert(Number.isInteger(port) && port > 0 && port < 65536, 'Invalid browser test port')
|
||||
const files = new Map()
|
||||
const requests = new Map()
|
||||
const manifest = { kind: 'artplayer-browser-test', resources: {} }
|
||||
function add(route, bytes, source) {
|
||||
files.set(route, bytes)
|
||||
manifest.resources[route] = { source, sha256: hash(bytes), bytes: bytes.length }
|
||||
}
|
||||
|
||||
// An explicit artifact map takes precedence over source builds and never falls back.
|
||||
const mapFile = process.env.ARTPLAYER_BROWSER_ARTIFACTS
|
||||
const overrides = mapFile ? JSON.parse(fs.readFileSync(mapFile, 'utf8')) : {}
|
||||
for (const name of ['artplayer', 'artplayer-plugin-chapter']) {
|
||||
const route = `/candidate/${name}.js`
|
||||
if (mapFile) {
|
||||
assert(Object.hasOwn(overrides, name), `Missing candidate artifact: ${name}`)
|
||||
const filename = path.resolve(path.dirname(path.resolve(mapFile)), overrides[name])
|
||||
add(route, fs.readFileSync(filename), { kind: 'artifact', file: filename })
|
||||
}
|
||||
else {
|
||||
const project = path.join(workspace, 'packages', name)
|
||||
const config = getViteBuildConfig({ entry: getEntryFile(project), name: getGlobalName(name), format: 'umd', fileName: `${name}.js`, minify: false })
|
||||
config.build.write = false
|
||||
const result = await build({ root: project, ...config })
|
||||
const chunks = (Array.isArray(result) ? result : [result]).flatMap(item => item.output).filter(item => item.type === 'chunk')
|
||||
assert.equal(chunks.length, 1, 'Browser source loader requires one self-contained chunk')
|
||||
add(route, Buffer.from(chunks[0].code), { kind: 'workspace-build', entry: path.relative(workspace, getEntryFile(project)) })
|
||||
}
|
||||
for (const alias of [`/compiled/${name}.js`, `/uncompiled/${name}/index.js`]) {
|
||||
files.set(alias, files.get(route))
|
||||
manifest.resources[alias] = { ...manifest.resources[route], aliasOf: route }
|
||||
}
|
||||
}
|
||||
const releases = JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/releases.json'), 'utf8')).releases
|
||||
for (const release of releases) {
|
||||
const member = `package/${release.manifest.main.replace(/^\.\//, '')}`
|
||||
const bytes = readMember(await ensureArchive(release), member)
|
||||
assert.equal(hash(bytes), release.files[member], 'Published browser artifact differs from frozen baseline')
|
||||
add(`/published/${release.name}.js`, bytes, { kind: 'npm-release', version: release.version, integrity: release.integrity, member })
|
||||
}
|
||||
for (const extension of ['mp4', 'webm']) {
|
||||
const name = `test/browser/media/pattern.${extension}`
|
||||
add(`/test/pattern.${extension}`, fs.readFileSync(path.join(workspace, name)), { kind: 'generated-media', file: name })
|
||||
}
|
||||
|
||||
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg' }
|
||||
function send(req, res, bytes, filename) {
|
||||
res.setHeader('Content-Type', mime[path.extname(filename)] || 'application/octet-stream')
|
||||
res.setHeader('Cache-Control', 'no-store')
|
||||
res.setHeader('Accept-Ranges', 'bytes')
|
||||
if (req.headers.range) {
|
||||
const match = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range)
|
||||
const start = match?.[1] ? Number(match[1]) : Math.max(0, bytes.length - Number(match?.[2]))
|
||||
const end = match?.[1] && match[2] ? Math.min(bytes.length - 1, Number(match[2])) : bytes.length - 1
|
||||
if (!match || (!match[1] && !match[2]) || !Number.isSafeInteger(start) || start > end || start >= bytes.length) {
|
||||
res.writeHead(416, { 'Content-Range': `bytes */${bytes.length}` }).end()
|
||||
return
|
||||
}
|
||||
res.statusCode = 206
|
||||
res.setHeader('Content-Range', `bytes ${start}-${end}/${bytes.length}`)
|
||||
bytes = bytes.subarray(start, end + 1)
|
||||
}
|
||||
res.setHeader('Content-Length', bytes.length)
|
||||
res.end(req.method === 'HEAD' ? undefined : bytes)
|
||||
}
|
||||
|
||||
add('/assets/sample/video.mp4', fs.readFileSync(path.join(workspace, 'docs/assets/sample/video.mp4')), { kind: 'docs-media', file: 'docs/assets/sample/video.mp4' })
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
try {
|
||||
assert(['GET', 'HEAD'].includes(req.method), 'Unsupported method')
|
||||
const url = new URL(req.url, `http://127.0.0.1:${port}`)
|
||||
const caseId = url.searchParams.get('case')
|
||||
if (url.pathname === '/test/requests.json') {
|
||||
send(req, res, Buffer.from(JSON.stringify(requests.get(caseId) || [])), 'requests.json')
|
||||
return
|
||||
}
|
||||
if (caseId && /\.(?:mp4|webm)$/.test(url.pathname)) {
|
||||
assert(caseId.length <= 200 && (requests.has(caseId) || requests.size < 10000), 'Test request log limit')
|
||||
const records = requests.get(caseId) || []
|
||||
if (records.length < 100)
|
||||
records.push({ path: url.pathname, range: req.headers.range || null, method: req.method })
|
||||
requests.set(caseId, records)
|
||||
}
|
||||
if (url.pathname === '/favicon.ico') {
|
||||
res.writeHead(204).end()
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/test/manifest.json') {
|
||||
send(req, res, Buffer.from(JSON.stringify(manifest)), 'manifest.json')
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/test/fail.mp4') {
|
||||
res.writeHead(503, { 'Content-Type': 'text/plain', 'Cache-Control': 'no-store' }).end('Intentional media failure')
|
||||
return
|
||||
}
|
||||
if (files.has(url.pathname)) {
|
||||
send(req, res, files.get(url.pathname), url.pathname)
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/test/player.html') {
|
||||
const core = url.searchParams.get('core') || 'candidate'
|
||||
const chapter = url.searchParams.get('chapter') || 'candidate'
|
||||
assert(['candidate', 'published'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
|
||||
const html = fs.readFileSync(path.join(workspace, 'test/browser/player.html'), 'utf8')
|
||||
.replace('__CORE__', core)
|
||||
.replace('__CHAPTER__', chapter)
|
||||
send(req, res, Buffer.from(html), 'player.html')
|
||||
return
|
||||
}
|
||||
assert(!/^\/(?:test|candidate|published|compiled|uncompiled)\//.test(url.pathname), 'Unmapped test artifact')
|
||||
const root = fs.realpathSync(path.join(workspace, 'docs'))
|
||||
let filename = path.resolve(root, `.${decodeURIComponent(url.pathname)}`)
|
||||
if (fs.statSync(filename).isDirectory())
|
||||
filename = path.join(filename, 'index.html')
|
||||
const relative = path.relative(root, fs.realpathSync(filename))
|
||||
assert(relative && !relative.startsWith('..') && !path.isAbsolute(relative), 'Invalid docs path')
|
||||
send(req, res, fs.readFileSync(filename), filename)
|
||||
}
|
||||
catch (error) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' }).end(error.message)
|
||||
}
|
||||
})
|
||||
server.listen(port, '127.0.0.1', () => process.stdout.write(`Browser test server: http://127.0.0.1:${port}\n`))
|
||||
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => server.close())
|
||||
Reference in new issue
Block a user