mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-16 09:36:16 -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
@@ -20,6 +20,37 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
|
||||
|
||||
jobs:
|
||||
browser-smoke:
|
||||
name: Browser playback smoke
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
CI: true
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
package-manager-cache: false
|
||||
- name: Install the pinned package manager and dependencies
|
||||
run: |
|
||||
npm install --global --force yarn@1.22.22
|
||||
yarn install --frozen-lockfile --non-interactive
|
||||
- name: Install test browsers
|
||||
run: yarn test:browser:install
|
||||
- name: Run all three engines
|
||||
run: yarn test:browser
|
||||
- name: Upload browser evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: browser-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: refactor/.cache/browser/
|
||||
include-hidden-files: true
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
checks:
|
||||
name: Checks and build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+7
-3
@@ -38,10 +38,10 @@
|
||||
"test:dash-control": "node --test test/dash-control.test.js",
|
||||
"dev": "npx cross-env NODE_ENV=development node ./scripts/dev.js",
|
||||
"build": "npx cross-env NODE_ENV=production node ./scripts/build.js",
|
||||
"lint": "eslint \"packages/*/{src,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" --no-fix",
|
||||
"lint": "eslint \"packages/*/{src,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" playwright.config.js --no-fix",
|
||||
"build:all": "yarn ci:build && yarn lint",
|
||||
"check:toolchain": "node scripts/check-toolchain.mjs",
|
||||
"lint:fix": "eslint \"packages/*/{src,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" --fix",
|
||||
"lint:fix": "eslint \"packages/*/{src,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" playwright.config.js --fix",
|
||||
"check:plan": "node refactor/scripts/plan.mjs --check",
|
||||
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js",
|
||||
"test:baseline": "node --test refactor/scripts/*.test.mjs",
|
||||
@@ -50,11 +50,15 @@
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/playback.test.js test/dash-control.test.js test/public-behavior.test.js test/helpers.test.js",
|
||||
"test": "yarn test:node && yarn test:baseline"
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
"test:browser:install": "playwright install chromium firefox webkit",
|
||||
"test:browser:report": "playwright show-report refactor/.cache/browser/html"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "5.4.1",
|
||||
"@playwright/test": "1.63.0",
|
||||
"@vue/compiler-sfc": "3.5.28",
|
||||
"@yarnpkg/lockfile": "1.1.0",
|
||||
"cpy": "13.2.3",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import process from 'node:process'
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
const port = Number(process.env.ARTPLAYER_BROWSER_PORT || 8084)
|
||||
const baseURL = `http://127.0.0.1:${port}`
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './test/browser',
|
||||
timeout: 20000,
|
||||
expect: { timeout: 7000 },
|
||||
fullyParallel: true,
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
retries: 0,
|
||||
workers: process.env.CI ? 2 : 3,
|
||||
outputDir: 'refactor/.cache/browser/results',
|
||||
reporter: [
|
||||
['list'],
|
||||
['html', { outputFolder: 'refactor/.cache/browser/html', open: 'never' }],
|
||||
['json', { outputFile: 'refactor/.cache/browser/report.json' }],
|
||||
],
|
||||
use: {
|
||||
baseURL,
|
||||
viewport: { width: 960, height: 720 },
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
||||
],
|
||||
webServer: {
|
||||
command: 'node test/browser/server.mjs',
|
||||
url: `${baseURL}/test/manifest.json`,
|
||||
reuseExistingServer: false,
|
||||
timeout: 60000,
|
||||
env: { ARTPLAYER_BROWSER_PORT: String(port) },
|
||||
},
|
||||
})
|
||||
@@ -44,6 +44,7 @@
|
||||
| [类型检查与迁移入口](typechecking.md) | 根/分包配置、当前/旧编译器、严格正反例和历史声明错误 |
|
||||
| [JS/TS 构建与开发](build-development.md) | 非交互选包、开发重建、资源夹具和 AMD 全局修复 |
|
||||
| [测试目录维护说明](../test/README.md) | 统一测试命令、JS/TS loader、受控媒体及新旧公共契约 |
|
||||
| [真实浏览器入口](../test/browser/README.md) | 三浏览器、解码画面与媒体状态、Range/失败服务及候选文件映射 |
|
||||
|
||||
## 维护规则
|
||||
|
||||
|
||||
@@ -0,0 +1,666 @@
|
||||
{
|
||||
"task": "ENG-05",
|
||||
"capturedAt": "2026-09-10T14:21:28.353Z",
|
||||
"node": "24.21.0",
|
||||
"platform": "win32",
|
||||
"playwright": "1.63.0",
|
||||
"mode": "workspace-build",
|
||||
"sourceHashAlgorithm": "sha256-lf",
|
||||
"sourceHashes": {
|
||||
"playwright.config.js": "df2384f5626252ec933725867e400ec22eb92bc5e6f04873059b32decf2f2d56",
|
||||
"test/browser/server.mjs": "d9025f9585f941a3b9b1f5b6c1b7b297c1fde6f50266720cbcc66f1334976f4a",
|
||||
"test/browser/player.html": "179c89abf50f7d69c9083a4c2951cb6cbcd624f61d3b417ead139506a6468eb7",
|
||||
"test/browser/fixtures.js": "313414b8958eb5b03a2da8595fa0bbf6bfdc01484373898c6cb7179545df175d",
|
||||
"test/browser/playback.spec.js": "f4d8f65f35bad9ceb68137e605881f2e799f94bc636c0fdbde07c49d3623d86c"
|
||||
},
|
||||
"manifest": {
|
||||
"kind": "artplayer-browser-test",
|
||||
"resources": {
|
||||
"/candidate/artplayer.js": {
|
||||
"source": {
|
||||
"kind": "workspace-build",
|
||||
"entry": "packages\\artplayer\\src\\index.js"
|
||||
},
|
||||
"sha256": "c6f564431e03a198c81fc026af75ead9f7c624e5c7f2f23cba5ce984ec045290",
|
||||
"bytes": 220281
|
||||
},
|
||||
"/compiled/artplayer.js": {
|
||||
"source": {
|
||||
"kind": "workspace-build",
|
||||
"entry": "packages\\artplayer\\src\\index.js"
|
||||
},
|
||||
"sha256": "c6f564431e03a198c81fc026af75ead9f7c624e5c7f2f23cba5ce984ec045290",
|
||||
"bytes": 220281,
|
||||
"aliasOf": "/candidate/artplayer.js"
|
||||
},
|
||||
"/uncompiled/artplayer/index.js": {
|
||||
"source": {
|
||||
"kind": "workspace-build",
|
||||
"entry": "packages\\artplayer\\src\\index.js"
|
||||
},
|
||||
"sha256": "c6f564431e03a198c81fc026af75ead9f7c624e5c7f2f23cba5ce984ec045290",
|
||||
"bytes": 220281,
|
||||
"aliasOf": "/candidate/artplayer.js"
|
||||
},
|
||||
"/candidate/artplayer-plugin-chapter.js": {
|
||||
"source": {
|
||||
"kind": "workspace-build",
|
||||
"entry": "packages\\artplayer-plugin-chapter\\src\\index.js"
|
||||
},
|
||||
"sha256": "833b81bf3334443531ed1822e28a38a241484056c6dbbdd50cd1ec093303ab94",
|
||||
"bytes": 8178
|
||||
},
|
||||
"/compiled/artplayer-plugin-chapter.js": {
|
||||
"source": {
|
||||
"kind": "workspace-build",
|
||||
"entry": "packages\\artplayer-plugin-chapter\\src\\index.js"
|
||||
},
|
||||
"sha256": "833b81bf3334443531ed1822e28a38a241484056c6dbbdd50cd1ec093303ab94",
|
||||
"bytes": 8178,
|
||||
"aliasOf": "/candidate/artplayer-plugin-chapter.js"
|
||||
},
|
||||
"/uncompiled/artplayer-plugin-chapter/index.js": {
|
||||
"source": {
|
||||
"kind": "workspace-build",
|
||||
"entry": "packages\\artplayer-plugin-chapter\\src\\index.js"
|
||||
},
|
||||
"sha256": "833b81bf3334443531ed1822e28a38a241484056c6dbbdd50cd1ec093303ab94",
|
||||
"bytes": 8178,
|
||||
"aliasOf": "/candidate/artplayer-plugin-chapter.js"
|
||||
},
|
||||
"/published/artplayer.js": {
|
||||
"source": {
|
||||
"kind": "npm-release",
|
||||
"version": "5.4.0",
|
||||
"integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==",
|
||||
"member": "package/dist/artplayer.js"
|
||||
},
|
||||
"sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f",
|
||||
"bytes": 134037
|
||||
},
|
||||
"/published/artplayer-plugin-chapter.js": {
|
||||
"source": {
|
||||
"kind": "npm-release",
|
||||
"version": "1.1.0",
|
||||
"integrity": "sha512-ji66Lr2K2S+OjFMuMby3yq+pefWlOC51Sbpop/DIr2N6w3F0smrF+YZbshN1Dy8/Tx1TYpT7N6WkkZWy5Jix1w==",
|
||||
"member": "package/dist/artplayer-plugin-chapter.js"
|
||||
},
|
||||
"sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0",
|
||||
"bytes": 4973
|
||||
},
|
||||
"/test/pattern.mp4": {
|
||||
"source": {
|
||||
"kind": "generated-media",
|
||||
"file": "test/browser/media/pattern.mp4"
|
||||
},
|
||||
"sha256": "0ad1d7ad286aec6e0fa9e27fc1e487e7277df2e603e30040fb942365763d0233",
|
||||
"bytes": 251538
|
||||
},
|
||||
"/test/pattern.webm": {
|
||||
"source": {
|
||||
"kind": "generated-media",
|
||||
"file": "test/browser/media/pattern.webm"
|
||||
},
|
||||
"sha256": "9d3c2b3dec9581a599de2d6732992ef7846790e0d1368e6e131bd339b39e984f",
|
||||
"bytes": 162073
|
||||
},
|
||||
"/assets/sample/video.mp4": {
|
||||
"source": {
|
||||
"kind": "docs-media",
|
||||
"file": "docs/assets/sample/video.mp4"
|
||||
},
|
||||
"sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980",
|
||||
"bytes": 5818706
|
||||
}
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"startTime": "2026-09-10T14:21:28.353Z",
|
||||
"duration": 12894.75,
|
||||
"expected": 12,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"project": "chromium",
|
||||
"title": "published: real playback, pause, seek, switch and destroy",
|
||||
"status": "passed",
|
||||
"durationMs": 1796,
|
||||
"browser": "153.0.8010.12",
|
||||
"events": [
|
||||
"video:loadedmetadata",
|
||||
"ready",
|
||||
"video:playing",
|
||||
"video:pause",
|
||||
"video:seeked",
|
||||
"video:loadedmetadata",
|
||||
"video:seeked",
|
||||
"video:playing",
|
||||
"destroy"
|
||||
],
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": [
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/test/pattern.mp4?case=b4ed495288f4b512a8ac-846a43d5c11f461bc706",
|
||||
"resourceType": "media",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/assets/sample/video.mp4?case=b4ed495288f4b512a8ac-846a43d5c11f461bc706",
|
||||
"resourceType": "media",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/assets/sample/video.mp4?case=b4ed495288f4b512a8ac-846a43d5c11f461bc706",
|
||||
"resourceType": "media",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
}
|
||||
],
|
||||
"decodedFrame": {
|
||||
"pixels": [
|
||||
[
|
||||
255,
|
||||
255,
|
||||
2,
|
||||
255
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
253,
|
||||
255
|
||||
],
|
||||
[
|
||||
1,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
]
|
||||
],
|
||||
"mediaRequests": [
|
||||
{
|
||||
"path": "/test/pattern.mp4",
|
||||
"range": "bytes=0-",
|
||||
"method": "GET"
|
||||
},
|
||||
{
|
||||
"path": "/test/pattern.mp4",
|
||||
"range": "bytes=229376-",
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"title": "candidate: real playback, pause, seek, switch and destroy",
|
||||
"status": "passed",
|
||||
"durationMs": 1727,
|
||||
"browser": "153.0.8010.12",
|
||||
"events": [
|
||||
"video:loadedmetadata",
|
||||
"ready",
|
||||
"video:playing",
|
||||
"video:pause",
|
||||
"video:seeked",
|
||||
"video:loadedmetadata",
|
||||
"video:seeked",
|
||||
"video:playing",
|
||||
"destroy"
|
||||
],
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": [
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/test/pattern.mp4?case=b4ed495288f4b512a8ac-6fc43c2ad3b16ac57feb",
|
||||
"resourceType": "media",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/assets/sample/video.mp4?case=b4ed495288f4b512a8ac-6fc43c2ad3b16ac57feb",
|
||||
"resourceType": "media",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/assets/sample/video.mp4?case=b4ed495288f4b512a8ac-6fc43c2ad3b16ac57feb",
|
||||
"resourceType": "media",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
}
|
||||
],
|
||||
"decodedFrame": {
|
||||
"pixels": [
|
||||
[
|
||||
255,
|
||||
255,
|
||||
2,
|
||||
255
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
253,
|
||||
255
|
||||
],
|
||||
[
|
||||
1,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
]
|
||||
],
|
||||
"mediaRequests": [
|
||||
{
|
||||
"path": "/test/pattern.mp4",
|
||||
"range": "bytes=0-",
|
||||
"method": "GET"
|
||||
},
|
||||
{
|
||||
"path": "/test/pattern.mp4",
|
||||
"range": "bytes=229376-",
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"title": "controlled HTTP media failure emits a real video error",
|
||||
"status": "passed",
|
||||
"durationMs": 391,
|
||||
"browser": "153.0.8010.12",
|
||||
"events": [
|
||||
"video:error",
|
||||
"destroy"
|
||||
],
|
||||
"errors": [],
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: the server responded with a status of 503 (Service Unavailable)"
|
||||
],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"title": "Range, media identity and docs candidate routes",
|
||||
"status": "passed",
|
||||
"durationMs": 308,
|
||||
"browser": "153.0.8010.12",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"title": "published: real playback, pause, seek, switch and destroy",
|
||||
"status": "passed",
|
||||
"durationMs": 3064,
|
||||
"browser": "155.0",
|
||||
"events": [
|
||||
"video:loadedmetadata",
|
||||
"ready",
|
||||
"video:playing",
|
||||
"video:pause",
|
||||
"video:seeked",
|
||||
"video:loadedmetadata",
|
||||
"video:seeked",
|
||||
"video:playing",
|
||||
"destroy"
|
||||
],
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": [],
|
||||
"decodedFrame": {
|
||||
"pixels": [
|
||||
[
|
||||
255,
|
||||
241,
|
||||
2,
|
||||
255
|
||||
],
|
||||
[
|
||||
0,
|
||||
14,
|
||||
253,
|
||||
255
|
||||
],
|
||||
[
|
||||
0,
|
||||
230,
|
||||
255,
|
||||
255
|
||||
]
|
||||
],
|
||||
"mediaRequests": [
|
||||
{
|
||||
"path": "/test/pattern.mp4",
|
||||
"range": "bytes=0-",
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"title": "candidate: real playback, pause, seek, switch and destroy",
|
||||
"status": "passed",
|
||||
"durationMs": 3118,
|
||||
"browser": "155.0",
|
||||
"events": [
|
||||
"video:loadedmetadata",
|
||||
"ready",
|
||||
"video:playing",
|
||||
"video:pause",
|
||||
"video:seeked",
|
||||
"video:loadedmetadata",
|
||||
"video:seeked",
|
||||
"video:playing",
|
||||
"destroy"
|
||||
],
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": [],
|
||||
"decodedFrame": {
|
||||
"pixels": [
|
||||
[
|
||||
255,
|
||||
241,
|
||||
2,
|
||||
255
|
||||
],
|
||||
[
|
||||
0,
|
||||
14,
|
||||
253,
|
||||
255
|
||||
],
|
||||
[
|
||||
0,
|
||||
230,
|
||||
255,
|
||||
255
|
||||
]
|
||||
],
|
||||
"mediaRequests": [
|
||||
{
|
||||
"path": "/test/pattern.mp4",
|
||||
"range": "bytes=0-",
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"title": "controlled HTTP media failure emits a real video error",
|
||||
"status": "passed",
|
||||
"durationMs": 2403,
|
||||
"browser": "155.0",
|
||||
"events": [
|
||||
"video:error",
|
||||
"destroy"
|
||||
],
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"title": "Range, media identity and docs candidate routes",
|
||||
"status": "passed",
|
||||
"durationMs": 543,
|
||||
"browser": "155.0",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"title": "published: real playback, pause, seek, switch and destroy",
|
||||
"status": "passed",
|
||||
"durationMs": 4626,
|
||||
"browser": "26.6",
|
||||
"events": [
|
||||
"video:loadedmetadata",
|
||||
"ready",
|
||||
"video:playing",
|
||||
"video:pause",
|
||||
"video:seeked",
|
||||
"video:loadedmetadata",
|
||||
"video:seeked",
|
||||
"video:playing",
|
||||
"destroy"
|
||||
],
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": [],
|
||||
"decodedFrame": {
|
||||
"pixels": [
|
||||
[
|
||||
235,
|
||||
236,
|
||||
12,
|
||||
255
|
||||
],
|
||||
[
|
||||
16,
|
||||
15,
|
||||
239,
|
||||
255
|
||||
],
|
||||
[
|
||||
13,
|
||||
237,
|
||||
237,
|
||||
255
|
||||
]
|
||||
],
|
||||
"mediaRequests": [
|
||||
{
|
||||
"path": "/test/pattern.mp4",
|
||||
"range": "bytes=0-",
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"title": "candidate: real playback, pause, seek, switch and destroy",
|
||||
"status": "passed",
|
||||
"durationMs": 4628,
|
||||
"browser": "26.6",
|
||||
"events": [
|
||||
"video:loadedmetadata",
|
||||
"ready",
|
||||
"video:playing",
|
||||
"video:pause",
|
||||
"video:seeked",
|
||||
"video:loadedmetadata",
|
||||
"video:seeked",
|
||||
"video:playing",
|
||||
"destroy"
|
||||
],
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": [],
|
||||
"decodedFrame": {
|
||||
"pixels": [
|
||||
[
|
||||
235,
|
||||
236,
|
||||
12,
|
||||
255
|
||||
],
|
||||
[
|
||||
16,
|
||||
15,
|
||||
239,
|
||||
255
|
||||
],
|
||||
[
|
||||
13,
|
||||
237,
|
||||
237,
|
||||
255
|
||||
]
|
||||
],
|
||||
"mediaRequests": [
|
||||
{
|
||||
"path": "/test/pattern.mp4",
|
||||
"range": "bytes=0-",
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"title": "controlled HTTP media failure emits a real video error",
|
||||
"status": "passed",
|
||||
"durationMs": 719,
|
||||
"browser": "26.6",
|
||||
"events": [
|
||||
"video:error",
|
||||
"destroy"
|
||||
],
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"title": "Range, media identity and docs candidate routes",
|
||||
"status": "passed",
|
||||
"durationMs": 577,
|
||||
"browser": "26.6",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
}
|
||||
],
|
||||
"artifactMode": {
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"stats": {
|
||||
"startTime": "2026-09-10T14:22:28.949Z",
|
||||
"duration": 5939.912,
|
||||
"expected": 4,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"resources": {
|
||||
"/candidate/artplayer.js": {
|
||||
"source": {
|
||||
"kind": "artifact",
|
||||
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\eng-06-build\\packages\\artplayer\\dist\\artplayer.js"
|
||||
},
|
||||
"sha256": "9ad044a601dafc24f449b690403fb1c343b40e99ffb31043272189fc3c1e7bfa",
|
||||
"bytes": 134939
|
||||
},
|
||||
"/compiled/artplayer.js": {
|
||||
"source": {
|
||||
"kind": "artifact",
|
||||
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\eng-06-build\\packages\\artplayer\\dist\\artplayer.js"
|
||||
},
|
||||
"sha256": "9ad044a601dafc24f449b690403fb1c343b40e99ffb31043272189fc3c1e7bfa",
|
||||
"bytes": 134939,
|
||||
"aliasOf": "/candidate/artplayer.js"
|
||||
},
|
||||
"/uncompiled/artplayer/index.js": {
|
||||
"source": {
|
||||
"kind": "artifact",
|
||||
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\eng-06-build\\packages\\artplayer\\dist\\artplayer.js"
|
||||
},
|
||||
"sha256": "9ad044a601dafc24f449b690403fb1c343b40e99ffb31043272189fc3c1e7bfa",
|
||||
"bytes": 134939,
|
||||
"aliasOf": "/candidate/artplayer.js"
|
||||
},
|
||||
"/candidate/artplayer-plugin-chapter.js": {
|
||||
"source": {
|
||||
"kind": "artifact",
|
||||
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\eng-06-build\\packages\\artplayer-plugin-chapter\\dist\\artplayer-plugin-chapter.js"
|
||||
},
|
||||
"sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0",
|
||||
"bytes": 4973
|
||||
},
|
||||
"/compiled/artplayer-plugin-chapter.js": {
|
||||
"source": {
|
||||
"kind": "artifact",
|
||||
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\eng-06-build\\packages\\artplayer-plugin-chapter\\dist\\artplayer-plugin-chapter.js"
|
||||
},
|
||||
"sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0",
|
||||
"bytes": 4973,
|
||||
"aliasOf": "/candidate/artplayer-plugin-chapter.js"
|
||||
},
|
||||
"/uncompiled/artplayer-plugin-chapter/index.js": {
|
||||
"source": {
|
||||
"kind": "artifact",
|
||||
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\eng-06-build\\packages\\artplayer-plugin-chapter\\dist\\artplayer-plugin-chapter.js"
|
||||
},
|
||||
"sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0",
|
||||
"bytes": 4973,
|
||||
"aliasOf": "/candidate/artplayer-plugin-chapter.js"
|
||||
},
|
||||
"/published/artplayer.js": {
|
||||
"source": {
|
||||
"kind": "npm-release",
|
||||
"version": "5.4.0",
|
||||
"integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==",
|
||||
"member": "package/dist/artplayer.js"
|
||||
},
|
||||
"sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f",
|
||||
"bytes": 134037
|
||||
},
|
||||
"/published/artplayer-plugin-chapter.js": {
|
||||
"source": {
|
||||
"kind": "npm-release",
|
||||
"version": "1.1.0",
|
||||
"integrity": "sha512-ji66Lr2K2S+OjFMuMby3yq+pefWlOC51Sbpop/DIr2N6w3F0smrF+YZbshN1Dy8/Tx1TYpT7N6WkkZWy5Jix1w==",
|
||||
"member": "package/dist/artplayer-plugin-chapter.js"
|
||||
},
|
||||
"sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0",
|
||||
"bytes": 4973
|
||||
},
|
||||
"/test/pattern.mp4": {
|
||||
"source": {
|
||||
"kind": "generated-media",
|
||||
"file": "test/browser/media/pattern.mp4"
|
||||
},
|
||||
"sha256": "0ad1d7ad286aec6e0fa9e27fc1e487e7277df2e603e30040fb942365763d0233",
|
||||
"bytes": 251538
|
||||
},
|
||||
"/test/pattern.webm": {
|
||||
"source": {
|
||||
"kind": "generated-media",
|
||||
"file": "test/browser/media/pattern.webm"
|
||||
},
|
||||
"sha256": "9d3c2b3dec9581a599de2d6732992ef7846790e0d1368e6e131bd339b39e984f",
|
||||
"bytes": 162073
|
||||
},
|
||||
"/assets/sample/video.mp4": {
|
||||
"source": {
|
||||
"kind": "docs-media",
|
||||
"file": "docs/assets/sample/video.mp4"
|
||||
},
|
||||
"sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980",
|
||||
"bytes": 5818706
|
||||
}
|
||||
},
|
||||
"source": "ENG-06 isolated build output, not a newly packed npm candidate"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# ENG-05:可重复的真实浏览器入口
|
||||
|
||||
## 交付与结构
|
||||
|
||||
新增 [浏览器测试目录](../../test/browser/README.md) 和根 Playwright 配置。固定
|
||||
`@playwright/test` 1.63.0(仅开发依赖;连同 playwright/playwright-core 更新 yarn.lock),
|
||||
提供安装、执行和 HTML 报告脚本。原快速 Node 测试独立保留。
|
||||
|
||||
服务只绑定 loopback,独占 8084、拒绝复用已有服务;每次从当前 JS/TS 入口构建 core/chapter,
|
||||
或明确读取指定候选文件。发布基线校验归档/member hash。候选文件缺失直接失败;不会偷偷
|
||||
使用工作区或旧 docs 产物。docs 页面和样本沿用原路径,compiled/uncompiled 的核心和 chapter
|
||||
别名映射到已记录 SHA-256 的候选文件。尚未映射的其他包资源拒绝加载。
|
||||
|
||||
媒体服务实现单段 Range/后缀 Range、HEAD、416、固定 503 和按用例隔离的请求记录。
|
||||
新建两个自生成、无外部画面及音轨的短样本;播放用例还切换到既有 docs video.mp4。
|
||||
没有改动核心/插件的生产源码、公开 API 或声明。
|
||||
|
||||
CI 新增 macOS 三浏览器 smoke 和 always 上传报告/失败截图/trace;所有操作使用既有固定
|
||||
Action SHA。macOS 的选择用于更接近 Safari 的媒体栈;这不代表已经运行远端 CI 或真实 Safari。
|
||||
|
||||
## 实际验证
|
||||
|
||||
- Windows、Node 24.21.0、Yarn 1.22.22:冻结安装通过,21 个直接工具固定、1339 个依赖 selector。
|
||||
- Chromium 153.0.8010.12、Firefox 155.0、WebKit 26.6:12 项全部通过,无跳过、无重试。
|
||||
同组用例覆盖发布 core 5.4.0 / 当前 core 与当前 chapter;解码画面像素、服务端媒体请求、
|
||||
时间推进、暂停、seek、切源、销毁、503 与 video:error 均有断言。
|
||||
- 指定 ENG-06 隔离构建中的两个 UMD 文件后,Firefox 4 项通过;报告确认使用 artifact 来源。
|
||||
空候选映射负例在启动前以退出码 1 拒绝。该模式验证映射能力,不代表 ENG-07 tarball 消费完成。
|
||||
- `yarn ci:check`:原 54 项检查通过;actionlint 与 Git 差异检查通过。
|
||||
- [精简运行证据](../baselines/browser-validation.json) 保存版本、实际资源哈希、测试源指纹、
|
||||
各用例结果、事件与解码像素。完整本地报告/log 在忽略的 refactor/.cache 下。
|
||||
|
||||
首次 WebKit 用例用 videoWidth=320 和 page.waitForResponse 判定失败。隔离原生 video、
|
||||
实际截图和解码像素后确认视频确实播放:该 Windows 构建的 videoWidth 随布局变化,媒体请求
|
||||
不可靠地出现在 page 网络事件中。最终用解码像素、实际时长和服务端请求联合判定,未删掉
|
||||
播放或失败断言。此尺寸差异保留为环境限制,不能据此宣称所有媒体元数据兼容均已验证。
|
||||
|
||||
首次 Chromium 把正常媒体 Range 取消当成错误。最终只允许当前用例两条已知媒体 URL、同源、
|
||||
resourceType=media、精确 net::ERR_ABORTED;全部原始记录仍保存。其他错误与播放失败仍失败。
|
||||
|
||||
本机 Node 下载浏览器连接超时,curl 从 Playwright 官方 URL 下载相同归档,再经临时 loopback
|
||||
镜像交给官方 installer 解包;未改依赖或浏览器文件、未关闭 TLS 检查。镜像和诊断服务均已停止。
|
||||
Chromium 本机安装 headless shell,已验证 headless 项目;带界面模式需安装完整 Chromium。
|
||||
|
||||
## 剩余范围与回退
|
||||
|
||||
真实 Monaco Run/TS/存储与完整 docs 页面操作仍归 EX-03;本任务只验证 docs 路由可取及候选映射,
|
||||
轻量页面不替代编辑器验收。WebM 尚未加入播放断言;音频、移动真机、SDK、全插件组合和其他
|
||||
浏览器最低版本仍按原任务推进。CI-01/CI-04 负责更完整矩阵及远端运行证据;本地全绿不代表远端已绿。
|
||||
|
||||
API-01/04/05/07/09 的基础测试入口已经可供 chapter 迁移复用,完整契约覆盖继续随包任务补齐。
|
||||
回退本任务提交可移除新增依赖、测试及 CI 作业,不影响生产运行代码。下一任务优先推进 chapter
|
||||
契约与测试,再完成 ENG-07 候选消费并进入插件源码拆分/TS 迁移。
|
||||
@@ -12,6 +12,7 @@
|
||||
| `yarn test:unit` | 原播放/DASH 回归、同夹具的新旧公共契约与 JS/TS loader 验证 |
|
||||
| `yarn test:node` | test:unit 加工具链/文档构建回归,保留原 test:playback/test:dash-control 入口 |
|
||||
| `yarn test` | 统一执行 Node 与基线测试,源码/夹具维护入口见仓库 test/README.md |
|
||||
| `yarn test:browser` | 隔离服务执行 Chromium/Firefox/WebKit 真实播放;首次先运行 test:browser:install,详见 [入口](../test/browser/README.md) |
|
||||
| `yarn test:imports` | 3 个既有包导入 smoke,构建后执行以使用新产物;不把 console 示例视为完整行为断言 |
|
||||
| `yarn test:baseline` | 固定发布包完整性及本地 HTTP 基线测试;首次可能下载已固定归档到缓存 |
|
||||
| `yarn ci:check` | 严格 Node/Yarn/锁检查、计划、只读 lint、类型、Node 和基线测试;允许写忽略缓存,不修改源码 |
|
||||
@@ -26,7 +27,7 @@ scripts/build-docs.js 保留原 npm run build 子命令兼容入口,现在传
|
||||
|
||||
Actions 固定完整 SHA,Node 来自 .node-version,Yarn 固定 1.22.22;安装使用 frozen-lockfile。actionlint 1.7.12 从官方固定归档取得,先核对提交在 workflow 中的 SHA-256,再执行。没有执行 curl 管道脚本。日志在失败时也尽量上传,包含源码 SHA、工具版本、锁摘要和生成差异概览,保留 14 天。
|
||||
|
||||
本轮不缓存 node_modules;矩阵、下载缓存、覆盖率、浏览器 trace 和候选 tarball 汇总由 CI-01 等后续任务扩展。当前测试通过不代表尚未实现的类型消费者或浏览器矩阵通过。将来的稳定 required 汇总名称由 CI-01/CI-04 核实后设置,不把本地 YAML 当作分支保护已生效。
|
||||
不缓存 node_modules。ENG-04 已接入类型消费者;ENG-05 新增 macOS 的 `Browser playback smoke` 作业,运行三个浏览器并 always 上传 HTML/JSON、失败截图和 trace。本地 Windows 三浏览器已验证,远端 macOS 作业尚未运行。完整生态矩阵、下载缓存、覆盖率和候选 tarball 汇总由 CI-01 等后续任务扩展。稳定 required 汇总名称由 CI-01/CI-04 核实后设置,不把本地 YAML 当作分支保护已生效。
|
||||
|
||||
## Pages 隔离与启用条件
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ chapter 试点可使用 `http://localhost:8082/?libs=./uncompiled/artplayer-plug
|
||||
|
||||
在线编辑器兼容用例继续通过实际 Run、键盘和控件交互验证;大量核心状态回归可以复用同一示例和样本在轻量测试页面运行,避免每项测试都依赖 Monaco 初始化。两类用例均保留,不能用轻量页面通过替代编辑器验收。
|
||||
|
||||
发布阶段将相同页面/示例接到候选 tarball 的提取内容,使用明确的测试目录或服务路由映射保留相对路径,并记录映射及完整性。此能力待 ENG-05/ENG-07 实现;当前没有可用的新开关或命令。不能只检查开发 bundle 就批准 npm 发布。
|
||||
ENG-05 已提供 `yarn test:browser`、8084 隔离服务和 `ARTPLAYER_BROWSER_ARTIFACTS` 文件映射,见 [浏览器入口](../test/browser/README.md)。当前支持 core/chapter 的 compiled/uncompiled 别名,未映射的构建资源明确失败;每次报告保存实际文件 SHA-256。ENG-07 继续负责生成及核验候选 tarball,不能只检查开发 bundle 就批准 npm 发布。
|
||||
|
||||
## 任务归属与验收
|
||||
|
||||
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 186 / doing 0 / blocked 0 / done 28 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 185 / doing 0 / blocked 0 / done 29 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
| ENG-02 | workspace<br>拆分只读检查并建立 PR CI | ENG-01, ENG-PM-01 | lint/lint:fix、PR 与主线检查、独立部署任务;遵循 github-ci-cd.md,PR/重构分支触发、最小权限及 workflow 静态检查 | 仓库内检查可执行且不改源码、不发布;required checks 的外部设置状态列入发布台账,不阻塞本地框架建设 | M | done |
|
||||
| ENG-03 | workspace<br>建立公共行为与单元测试入口 | ENG-02, BASE-03 | 保留现有 node:test,测试目录/夹具/统一入口 | 已有 19 项回归保留,旧版与候选可用同一夹具运行 | M | done |
|
||||
| ENG-04 | workspace<br>建立类型测试基础 | ENG-02, BASE-05 | 根与分包 tsconfig、显式 TS 依赖、正反例测试 | 核心/试点与迁移模块严格检查,未迁移第三方/包历史问题独立台账;明确最低/当前 TS 和各环境类型;复用 BASE-05 的四种消费模式,不以 skipLibCheck 掩盖 BASE-TYPE-01/03 | M | done |
|
||||
| ENG-05 | workspace<br>建立真实浏览器测试服务 | ENG-03, BASE-04, BASE-08 | Playwright projects、本地 Range/失败媒体服务;复用 docs 页面/样本的状态隔离、错误采集与候选资源映射 | Chromium/Firefox/WebKit 的基础播放 smoke 和报告可执行;以媒体状态断言判定通过,区分轻量用例与真实编辑器交互 | M | todo |
|
||||
| ENG-05 | workspace<br>建立真实浏览器测试服务 | ENG-03, BASE-04, BASE-08 | Playwright projects、本地 Range/失败媒体服务;复用 docs 页面/样本的状态隔离、错误采集与候选资源映射 | Chromium/Firefox/WebKit 的基础播放 smoke 和报告可执行;以媒体状态断言判定通过,区分轻量用例与真实编辑器交互 | M | done |
|
||||
| ENG-06 | workspace<br>支持按包非交互与 JS/TS 构建 | ENG-02, BASE-05 | build/dev 入口解析、指定包参数、原交互保留 | 三种产物、Less/SVG/worker 和本地 8082 demo 正常;保持 BASE-05 的 AMD 同时写入全局行为及 i18n/legacy 入口 | H | done |
|
||||
| ENG-07 | workspace<br>建立 tarball 消费与产物检查 | ENG-04, ENG-06 | 隔离 npm 消费 fixtures、API/声明/入口差分 | 不借 workspace 源码通过,能识别缺文件与默认导出变化 | H | todo |
|
||||
| ENG-08 | workspace<br>增加覆盖率、资源与性能报告 | ENG-03, ENG-05, BASE-06 | 覆盖率基线、资源清理断言、性能报告与阈值 | 关键生命周期分支有门槛,报告不靠无意义断言堆数量;将 BASE-06 的同环境多组配对、计时/压缩审查阈值与资源异常分开接入候选,不把历史现象冻结成正常要求 | M | todo |
|
||||
@@ -430,4 +430,5 @@
|
||||
- ENG-02: [记录](changes/2026-09-10-ENG-02-readonly-ci.md) [记录](baselines/ci-validation.json)
|
||||
- ENG-03: [记录](changes/2026-09-10-ENG-03-unit-entry.md)
|
||||
- ENG-04: [记录](changes/2026-09-10-ENG-04-typechecking.md) [记录](typechecking.md)
|
||||
- ENG-05: [记录](changes/2026-09-10-ENG-05-browser-tests.md) [记录](baselines/browser-validation.json)
|
||||
- ENG-06: [记录](changes/2026-09-10-ENG-06-build-development.md) [记录](build-development.md) [记录](baselines/build-validation.json)
|
||||
@@ -1,5 +1,9 @@
|
||||
# 进度与证据
|
||||
|
||||
## 当前实施:ENG-05 真实浏览器入口完成
|
||||
|
||||
Playwright 三浏览器 12 项真实播放/失败/映射测试通过,指定候选 UMD 文件的 Firefox 4 项通过;新增 CI smoke、报告与自生成媒体。见 [ENG-05](changes/2026-09-10-ENG-05-browser-tests.md)。当前 214 项,29 完成、185 待办;生产源码 TS 迁移仍为零。已具备 chapter 试点所需浏览器入口,下一项 PKG-CHAPTER-01,接特有测试和 ENG-07,再迁移插件源码。远端 CI、完整编辑器及真机尚未验收。
|
||||
|
||||
## 当前实施:ENG-03 公共行为与单元入口完成
|
||||
|
||||
原 19 项播放/DASH 回归保留,JS/TS loader 与受控媒体夹具复用;同一份五组 Emitter 契约在发布 core 5.4.0、工作区和三格式候选上通过。统一 yarn test:unit/test 与 CI 入口已接入,见 [ENG-03](changes/2026-09-10-ENG-03-unit-entry.md)。当前 214 项,28 完成、186 待办;生产源码 TS 迁移仍为零。ENG-06 提交 b53461da,本任务独立提交,下一项 ENG-05 浏览器自动化。
|
||||
|
||||
+5
-2
@@ -532,11 +532,14 @@
|
||||
"BASE-04",
|
||||
"BASE-08"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "M",
|
||||
"deliverable": "Playwright projects、本地 Range/失败媒体服务;复用 docs 页面/样本的状态隔离、错误采集与候选资源映射",
|
||||
"acceptance": "Chromium/Firefox/WebKit 的基础播放 smoke 和报告可执行;以媒体状态断言判定通过,区分轻量用例与真实编辑器交互",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-10-ENG-05-browser-tests.md",
|
||||
"baselines/browser-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ENG-06",
|
||||
|
||||
@@ -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())
|
||||
@@ -1361,6 +1361,13 @@
|
||||
resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz"
|
||||
integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==
|
||||
|
||||
"@playwright/test@1.63.0":
|
||||
version "1.63.0"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.63.0.tgz#808020c46e8b6b91f9d628cd0ebcbb32bdfd8b6c"
|
||||
integrity sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==
|
||||
dependencies:
|
||||
playwright "1.63.0"
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.57.1":
|
||||
version "4.57.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz#add5e608d4e7be55bc3ca3d962490b8b1890e088"
|
||||
@@ -6533,6 +6540,18 @@ pkg-types@^2.3.0:
|
||||
exsolve "^1.0.7"
|
||||
pathe "^2.0.3"
|
||||
|
||||
playwright-core@1.63.0:
|
||||
version "1.63.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.63.0.tgz#e57665bc32846c213ac39a1e4d5bc6228e76b376"
|
||||
integrity sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==
|
||||
|
||||
playwright@1.63.0:
|
||||
version "1.63.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.63.0.tgz#99b56f9f69b1b70c44f00bf84b2fe52348ae2511"
|
||||
integrity sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==
|
||||
dependencies:
|
||||
playwright-core "1.63.0"
|
||||
|
||||
pluralize@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz"
|
||||
|
||||
Reference in new issue
Block a user