refactor(docs): [SITE-SMOKE-01] verify example readiness and cleanup

This commit is contained in:
Harvey Zhao committed 2026-09-14 07:02:49 +08:00
1 parent d74ecc43cc
commit 1a5440a67f
25 files changed
+3510 -4770

No files matched your search

File diff suppressed because it is too large. Load diff
+3 -2
View File
@@ -2,7 +2,7 @@
<head>
<meta charset="utf-8" />
<title>Mocha Tests</title>
<title>Documentation Readiness Smoke</title>
<link href="../assets/css/normalize.css" rel="stylesheet" />
<link href="./mocha.css" rel="stylesheet" />
<style>
@@ -23,6 +23,7 @@
</head>
<body>
<p>These tests check example evaluation and player readiness. Interactive features require separate validation.</p>
<div class="container">
<div class="artplayer-app"></div>
</div>
@@ -48,4 +49,4 @@
<script>mocha.run();</script>
</body>
</html>
</html>
+595 -4606
View File
File diff suppressed because it is too large. Load diff
+10 -6
View File
@@ -40,15 +40,15 @@
"test:dash-control": "node --test test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.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,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix",
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"scripts/docs-smoke/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.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,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix",
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"scripts/docs-smoke/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.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/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js test/ci-summary.test.js test/package-runtime.test.js test/library-build.test.js",
"test:baseline": "node --test refactor/scripts/*.test.mjs",
"ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn lint && yarn check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test",
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn lint && yarn typecheck:docs-tools && yarn check:docs-smoke && yarn check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test",
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:test && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js",
"typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/danmuku-mask-lifecycle.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js test/jassub-offscreen.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-exports.test.js test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js",
@@ -110,7 +110,7 @@
"test:auto-thumbnail-types-package": "node refactor/scripts/auto-thumbnail-package-types.mjs",
"probe:auto-thumbnail-rendering": "node refactor/scripts/auto-thumbnail-rendering-probe.mjs",
"dev:react": "vite example/react.js --config example/react.js/vite.config.js",
"typecheck:react": "tsc -p example/react.js/tsconfig.json --noEmit",
"typecheck:react": "node node_modules/typescript/bin/tsc -p example/react.js/tsconfig.json --noEmit",
"build:react": "yarn typecheck:react && vite build example/react.js --config example/react.js/vite.config.js --outDir ../../refactor/.cache/react-example-dist",
"test:react-consumer": "node refactor/scripts/react-consumer.mjs",
"lint:react": "eslint example/react.js --no-fix",
@@ -118,12 +118,16 @@
"typecheck:vue": "vue-tsc -p example/vue.js/tsconfig.json --noEmit",
"build:vue": "yarn typecheck:vue && vite build example/vue.js --config example/vue.js/vite.config.js --outDir ../../refactor/.cache/vue-example-dist",
"test:vue-consumer": "node refactor/scripts/vue-consumer.mjs",
"lint:vue": "eslint example/vue.js --no-fix"
"lint:vue": "eslint example/vue.js --no-fix",
"typecheck:docs-tools": "node node_modules/typescript/bin/tsc -p scripts/tsconfig.docs.json --noEmit",
"check:docs-smoke": "node scripts/build-test.js --check"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
"@antfu/eslint-config": "5.4.1",
"@playwright/test": "1.63.0",
"@types/markdown-it": "14.1.2",
"@types/node": "24.10.0",
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"@vitejs/plugin-react": "5.0.0",
+3 -2
View File
@@ -43,8 +43,9 @@ The repository generators have different ownership:
- `scripts/build-types.mjs`: core public declaration sources to existing `types/`.
- `scripts/build-ts.js`: standalone editor declarations and the editor library list.
- `scripts/build-test.js`: extracts Chinese Run Code blocks into `docs/test/test.js`.
It is currently a smoke generator with a fixed 100 ms completion delay, not
proof of asynchronous playback or plugin success. SITE-02 owns its replacement.
The TS implementation in `scripts/docs-smoke/` produces deterministic readiness
smoke cases with owned frames, error observation and cleanup. It does not prove
complete playback or plugin behavior. See its maintenance README and SITE-SMOKE-01.
- `scripts/build-i18n.js`: core language source bundles and copies to `docs/compiled/i18n/`.
- `scripts/build-docs.js`: this VitePress build.
- `scripts/build-llm.js` and `scripts/trans-docs.js`: explicit remote DeepSeek
@@ -0,0 +1,584 @@
{
"task": "SITE-SMOKE-01",
"baseline": "d74ecc43ccee99dcd2e3090356a6fb761825e6af",
"node": "24.21.0",
"yarn": "1.22.22",
"tools": {
"primaryTypeScript": "5.9.3",
"historicalSharedTscBin": "5.1.6",
"addedRootDevDependencies": {
"@types/node": "24.10.0",
"@types/markdown-it": "14.1.2"
},
"productionTsFiles": 406,
"workspaces": 22,
"pinnedTools": 38,
"dependencySelectors": 1470
},
"selectedExamples": {
"examples": 233,
"files": 11,
"runtimeScripts": 11,
"preservedCodeAfterLfNormalization": true,
"fullBrowserExecution": false
},
"parserBefore": {
"sourceCommit": "d74ecc43ccee99dcd2e3090356a6fb761825e6af",
"reproduction": "Fixed old extractor VM ERR_SCRIPT_EXECUTION_TIMEOUT for malformed snippet; committed test verifies finite new parse and syntax rejection."
},
"validation": {
"baseline": {
"file": "refactor/.cache/docs-smoke-baseline.log",
"sha256": "364e7e17a40399436e42eef2a9e6a895830fc9abc70f1067050a90df3a097fed",
"exitCode": 0,
"pass": 522
},
"targeted": {
"file": "refactor/.cache/docs-smoke-tests.log",
"sha256": "7170a7bb34967be812c4b20f57e35fa4bf1701e1b4ae5c5adb3c67a81003379f",
"exitCode": 0,
"pass": 5
},
"ci": {
"file": "refactor/.cache/docs-smoke-ci-tests.log",
"sha256": "18ef50db081f0106e31c322e2d836add7866c11f22d53ebf8770a1b4f91a707e",
"exitCode": 0,
"pass": 50
},
"productionTypes": {
"file": "refactor/.cache/docs-smoke-production-types.log",
"sha256": "a57e291b4168679e50533f35ab39b835c66a1dde22ad4b8a045d65db27e14e81",
"exitCode": 0
},
"docsTypes": {
"file": "refactor/.cache/docs-smoke-types-final.log",
"sha256": "d082eef2f531b831e25fc38c50f2f0727b049b5f1c8c915c67b12fb8c42a80e7",
"exitCode": 0
},
"reactTypes": {
"file": "refactor/.cache/docs-smoke-react-types.log",
"sha256": "076a3287d3c0eff3c331b25972a92239476928b31b83d84cb6fee512fc8d2ad5",
"exitCode": 0
},
"frozen": {
"file": "refactor/.cache/docs-smoke-frozen.log",
"sha256": "002782fd8a6091b824463b26b2711b695f7154b6f3cd7b6c7ca4e86e775153e0",
"exitCode": 0
},
"toolchain": {
"file": "refactor/.cache/docs-smoke-toolchain.log",
"sha256": "ff37a9bff5aeaac042303412efefbcbe0ffc01ae6c615b72e3be4b2bedc522a5",
"exitCode": 0
},
"rootLint": {
"file": "refactor/.cache/docs-smoke-root-lint.log",
"sha256": "3cc99e4aad769bb1d791f64cfbe2691d46a65b67c1ae7de25ca09e7630033ee5",
"exitCode": 0
},
"targetedLint": {
"file": "refactor/.cache/docs-smoke-final-lint.log",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"exitCode": 0
},
"readonlyCheck": {
"file": "refactor/.cache/docs-smoke-check.log",
"sha256": "c5f08a17633392d6e1680f92db95be60da5e7e3fb029adb69a25f0303a8558bb",
"exitCode": 0
},
"compilerAliasBefore": {
"file": "refactor/.cache/docs-smoke-tsc-alias-before.log",
"sha256": "b0a1234a8a71fed3658891302f314f246f13c802423c4a16c1e2b802c9df8dea",
"exitCode": 2
},
"browserBefore": {
"file": "refactor/.cache/docs-smoke-browser-final.log",
"sha256": "3cd781617dcc585c7cb7d5fdc8d3b9345cff9863ba81c2fc85f8d98995d2bb85",
"exitCode": 1,
"pass": 31
},
"browserAccepted": {
"file": "refactor/.cache/docs-smoke-browser-verified.log",
"sha256": "d4c01724c6d5960b33efc242b861553a65da0f7842e6def593e899b1f7387217",
"exitCode": 0,
"pass": 33
}
},
"rootLintExistingWarnings": 1,
"browser": {
"file": "refactor/.cache/docs-smoke-browser-accepted/report.json",
"sha256": "cc96afc1e1b53e78db3063679bb4505fb19524d720019ad856e270d6bd6397f0",
"stats": {
"startTime": "2026-09-13T22:43:58.881Z",
"duration": 32387.1,
"expected": 33,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"cases": [
{
"title": "readiness smoke uses actual core/media and releases each frame without touching a parent player",
"project": "chromium",
"status": "expected",
"attempts": 1,
"browser": "153.0.8010.12",
"platform": "win32",
"errors": [],
"observations": [
{
"name": "readiness-0",
"contentType": "application/json",
"content": "{\"id\":\"fixture.md:1\",\"instances\":1,\"ready\":1,\"media\":[{\"readyState\":4,\"width\":320,\"height\":180,\"duration\":8}]}"
},
{
"name": "readiness-1",
"contentType": "application/json",
"content": "{\"id\":\"fixture.md:1\",\"instances\":1,\"ready\":1,\"media\":[{\"readyState\":4,\"width\":320,\"height\":180,\"duration\":8}]}"
},
{
"name": "readiness-2",
"contentType": "application/json",
"content": "{\"id\":\"fixture.md:1\",\"instances\":1,\"ready\":1,\"media\":[{\"readyState\":4,\"width\":320,\"height\":180,\"duration\":8}]}"
}
]
},
{
"title": "smoke remains pending beyond 100ms until real media becomes ready",
"project": "chromium",
"status": "expected",
"attempts": 1,
"browser": "153.0.8010.12",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "generated real documentation cases run with controlled native media",
"project": "chromium",
"status": "expected",
"attempts": 1,
"browser": "153.0.8010.12",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "parallel calls are rejected while a controlled case is still active",
"project": "chromium",
"status": "expected",
"attempts": 1,
"browser": "153.0.8010.12",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports synchronous error and removes its frame",
"project": "chromium",
"status": "expected",
"attempts": 1,
"browser": "153.0.8010.12",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports returned rejection and removes its frame",
"project": "chromium",
"status": "expected",
"attempts": 1,
"browser": "153.0.8010.12",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports unhandled rejection and removes its frame",
"project": "chromium",
"status": "expected",
"attempts": 1,
"browser": "153.0.8010.12",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports readiness callback error and removes its frame",
"project": "chromium",
"status": "expected",
"attempts": 1,
"browser": "153.0.8010.12",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports cleanup error and removes its frame",
"project": "chromium",
"status": "expected",
"attempts": 1,
"browser": "153.0.8010.12",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "missing script and never-ready examples fail instead of becoming green after a delay",
"project": "chromium",
"status": "expected",
"attempts": 1,
"browser": "153.0.8010.12",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "frame-owned timers and globals do not survive into the next example",
"project": "chromium",
"status": "expected",
"attempts": 1,
"browser": "153.0.8010.12",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "readiness smoke uses actual core/media and releases each frame without touching a parent player",
"project": "firefox",
"status": "expected",
"attempts": 1,
"browser": "155.0",
"platform": "win32",
"errors": [],
"observations": [
{
"name": "readiness-0",
"contentType": "application/json",
"content": "{\"id\":\"fixture.md:1\",\"instances\":1,\"ready\":1,\"media\":[{\"readyState\":4,\"width\":320,\"height\":180,\"duration\":8}]}"
},
{
"name": "readiness-1",
"contentType": "application/json",
"content": "{\"id\":\"fixture.md:1\",\"instances\":1,\"ready\":1,\"media\":[{\"readyState\":4,\"width\":320,\"height\":180,\"duration\":8}]}"
},
{
"name": "readiness-2",
"contentType": "application/json",
"content": "{\"id\":\"fixture.md:1\",\"instances\":1,\"ready\":1,\"media\":[{\"readyState\":4,\"width\":320,\"height\":180,\"duration\":8}]}"
}
]
},
{
"title": "smoke remains pending beyond 100ms until real media becomes ready",
"project": "firefox",
"status": "expected",
"attempts": 1,
"browser": "155.0",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "generated real documentation cases run with controlled native media",
"project": "firefox",
"status": "expected",
"attempts": 1,
"browser": "155.0",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "parallel calls are rejected while a controlled case is still active",
"project": "firefox",
"status": "expected",
"attempts": 1,
"browser": "155.0",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports synchronous error and removes its frame",
"project": "firefox",
"status": "expected",
"attempts": 1,
"browser": "155.0",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports returned rejection and removes its frame",
"project": "firefox",
"status": "expected",
"attempts": 1,
"browser": "155.0",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports unhandled rejection and removes its frame",
"project": "firefox",
"status": "expected",
"attempts": 1,
"browser": "155.0",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports readiness callback error and removes its frame",
"project": "firefox",
"status": "expected",
"attempts": 1,
"browser": "155.0",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports cleanup error and removes its frame",
"project": "firefox",
"status": "expected",
"attempts": 1,
"browser": "155.0",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "missing script and never-ready examples fail instead of becoming green after a delay",
"project": "firefox",
"status": "expected",
"attempts": 1,
"browser": "155.0",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "frame-owned timers and globals do not survive into the next example",
"project": "firefox",
"status": "expected",
"attempts": 1,
"browser": "155.0",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "readiness smoke uses actual core/media and releases each frame without touching a parent player",
"project": "webkit",
"status": "expected",
"attempts": 1,
"browser": "26.6",
"platform": "win32",
"errors": [],
"observations": [
{
"name": "readiness-0",
"contentType": "application/json",
"content": "{\"id\":\"fixture.md:1\",\"instances\":1,\"ready\":1,\"media\":[{\"readyState\":4,\"width\":320,\"height\":180,\"duration\":8}]}"
},
{
"name": "readiness-1",
"contentType": "application/json",
"content": "{\"id\":\"fixture.md:1\",\"instances\":1,\"ready\":1,\"media\":[{\"readyState\":4,\"width\":320,\"height\":180,\"duration\":8}]}"
},
{
"name": "readiness-2",
"contentType": "application/json",
"content": "{\"id\":\"fixture.md:1\",\"instances\":1,\"ready\":1,\"media\":[{\"readyState\":4,\"width\":320,\"height\":180,\"duration\":8}]}"
}
]
},
{
"title": "smoke remains pending beyond 100ms until real media becomes ready",
"project": "webkit",
"status": "expected",
"attempts": 1,
"browser": "26.6",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "generated real documentation cases run with controlled native media",
"project": "webkit",
"status": "expected",
"attempts": 1,
"browser": "26.6",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "parallel calls are rejected while a controlled case is still active",
"project": "webkit",
"status": "expected",
"attempts": 1,
"browser": "26.6",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports synchronous error and removes its frame",
"project": "webkit",
"status": "expected",
"attempts": 1,
"browser": "26.6",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports returned rejection and removes its frame",
"project": "webkit",
"status": "expected",
"attempts": 1,
"browser": "26.6",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports unhandled rejection and removes its frame",
"project": "webkit",
"status": "expected",
"attempts": 1,
"browser": "26.6",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports readiness callback error and removes its frame",
"project": "webkit",
"status": "expected",
"attempts": 1,
"browser": "26.6",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "smoke reports cleanup error and removes its frame",
"project": "webkit",
"status": "expected",
"attempts": 1,
"browser": "26.6",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "missing script and never-ready examples fail instead of becoming green after a delay",
"project": "webkit",
"status": "expected",
"attempts": 1,
"browser": "26.6",
"platform": "win32",
"errors": [],
"observations": []
},
{
"title": "frame-owned timers and globals do not survive into the next example",
"project": "webkit",
"status": "expected",
"attempts": 1,
"browser": "26.6",
"platform": "win32",
"errors": [],
"observations": []
}
],
"before": {
"pass": 31,
"fail": 2,
"reason": "Windows WebKit reports 640x360 layout dimensions for 320x180 media; existing DPIP-MEDIA-01. Exact pairs only, not closed.",
"archive": "refactor/.cache/docs-smoke-browser-before-g6DmAA"
}
},
"inputs": {
"scripts/build-test.js": {
"sha256Lf": "b032fc230d716f0f33650113a04564e64b827c7d637bbbf07afa990fa8755027"
},
"scripts/tsconfig.docs.json": {
"sha256Lf": "8d2adc75cbe68f612cb83c3b4586b547cd33ddb724e0dba2471ea5d160fd20e5"
},
"scripts/docs-smoke/parser.ts": {
"sha256Lf": "d1f19833124954922849d4f40f853380b0ddd7bbf232f7cdf9c07dd8d6403e1c"
},
"scripts/docs-smoke/generator.ts": {
"sha256Lf": "2ae4144f27b3050869217e437ddca75ccc25897bf78f75efc91dd1f7d2ca3dc2"
},
"scripts/docs-smoke/runtime.ts": {
"sha256Lf": "9fae3aa1bc6a6ef9728d9a677de9e283d40314022b710b2ecf19a9b951f6291d"
},
"refactor/scripts/docs-smoke.test.mjs": {
"sha256Lf": "ddb417851665bfc2bd9e3a52c43b0fbc450fcd8eff7e0d63427bd6c2665704d5"
},
"test/browser/docs-smoke.spec.js": {
"sha256Lf": "7e3efb737346da3f9da8daf29f5bd15ad8162bb906ced456002e4c8222d8ed9a"
},
"docs/test/index.html": {
"sha256Lf": "5f525951d54590c358f060c8a4829abf322dcff89f31a25f3892d9030ad4e4c8"
},
"package.json": {
"sha256Lf": "3ef07142a1b5a3141292694e23de7af26d25812ecd2863a7f2a3deb856ce7554"
},
"yarn.lock": {
"sha256Lf": "8139d40cc1a40e4089c1bcf21f9edece6a8726256ba9246775680add45622512"
},
"packages/artplayer-vitepress/docs/advanced/built-in.md": {
"sha256Lf": "332f7b86bdbbb2957fddfd94aa779f3b9c40c172695877d65b4edbdc0e67b39e"
},
"packages/artplayer-vitepress/docs/advanced/class.md": {
"sha256Lf": "1d5c1c53beda9b5d6d40ab94587028e9d57a562b03604cb32e7d049058945ebf"
},
"packages/artplayer-vitepress/docs/advanced/event.md": {
"sha256Lf": "a5b2bf5bba605914efaa76e7a21b16b52575e92e8fe1ec682f46ea0e4faf7a55"
},
"packages/artplayer-vitepress/docs/advanced/global.md": {
"sha256Lf": "d9990b5c0293df1a8ded04044efbd59bdea6d9977bcf2bc0b0f8ee7d3df7d2ba"
},
"packages/artplayer-vitepress/docs/advanced/plugin.md": {
"sha256Lf": "21b4dc45a245d7747fdf0bf0b86548d3bb945a037ac5493f290e81066ba65bb7"
},
"packages/artplayer-vitepress/docs/advanced/property.md": {
"sha256Lf": "0c1a40d37bd3a87c31eb768acae3104286976b7feb0d560c187d31b0805421e6"
},
"packages/artplayer-vitepress/docs/component/contextmenu.md": {
"sha256Lf": "50ee2ef72eca5f0ea4ef42283d98a0f5bd375d661b26f0ccd58d82c5d99aeb4e"
},
"packages/artplayer-vitepress/docs/component/controls.md": {
"sha256Lf": "ef215c3e831835edc002f4e7e22fa1f619435936012684d23d251e78d4d08e79"
},
"packages/artplayer-vitepress/docs/component/layers.md": {
"sha256Lf": "7bc9eea79ab23b1280aa34a7946ab4b98b0a52051e014378609f45fcd1f074ee"
},
"packages/artplayer-vitepress/docs/component/setting.md": {
"sha256Lf": "4387c3b15e32ac3176d4c9279642262073c37df8fb213379eae8cee6d7718a1f"
},
"packages/artplayer-vitepress/docs/start/option.md": {
"sha256Lf": "336c1d69cd484e18fbebd6a029069a0ab734af2a256bb696b6272003a1eadb15"
}
},
"outputs": {
"docs/test/test.js": {
"sha256Lf": "d56a24949b5b4c97b85abfb8233050b2e62c2869a0c15b1e3ad7c83d4987adfb"
},
"docs/test/examples.json": {
"sha256Lf": "5af15520ebca3d84887391ea8b202fb0de7c2a2809dbe45cdad199f91f9cdf2a"
}
},
"limitations": [
"Readiness harness, not complete interaction/playback acceptance of 233 examples.",
"No remote SDK endpoint, native Safari/device, manual Chrome/iab or remote CI validation.",
"Same-origin iframe is not a security sandbox; storage restoration requires exclusive test origin.",
"No push, publication or SITE-02 declaration-generator completion."
]
}
+15 -3
View File
@@ -17939,7 +17939,7 @@
{
"source": "docs/test/index.html",
"route": "/test/",
"sha256Lf": "67132147ebbb63867c05be182ec18fc6c3129df89808bf17819c92cee9a4769c",
"sha256Lf": "5f525951d54590c358f060c8a4829abf322dcff89f31a25f3892d9030ad4e4c8",
"kind": "demo-or-editor",
"scriptSources": [
"./chai.js",
@@ -18113,7 +18113,7 @@
},
{
"file": "scripts/build-test.js",
"sha256Lf": "1e8ce7bce686b507bcf9f3fd02dfe03102b1940daa0fc14a487abadb909e93c7"
"sha256Lf": "b032fc230d716f0f33650113a04564e64b827c7d637bbbf07afa990fa8755027"
},
{
"file": "scripts/build-i18n.js",
@@ -18154,6 +18154,18 @@
{
"file": "packages/artplayer-vitepress/docs/.vitepress/config.js",
"sha256Lf": "9b2741347afb49d830dafa2a847625e6b1d6a820e930fb119c91b22dbbcc52d1"
},
{
"file": "scripts/docs-smoke/generator.ts",
"sha256Lf": "2ae4144f27b3050869217e437ddca75ccc25897bf78f75efc91dd1f7d2ca3dc2"
},
{
"file": "scripts/docs-smoke/parser.ts",
"sha256Lf": "d1f19833124954922849d4f40f853380b0ddd7bbf232f7cdf9c07dd8d6403e1c"
},
{
"file": "scripts/docs-smoke/runtime.ts",
"sha256Lf": "9fae3aa1bc6a6ef9728d9a677de9e283d40314022b710b2ecf19a9b951f6291d"
}
],
"generationCommands": {
@@ -18163,7 +18175,7 @@
"build:i18n": "node ./scripts/build-i18n.js",
"build:test": "node ./scripts/build-test.js",
"build:all": "yarn ci:build && yarn lint",
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:test && yarn build:docs && yarn test:imports",
"build:types": "node scripts/build-types.mjs --write"
},
"siteManifest": {
@@ -0,0 +1,63 @@
# SITE-SMOKE-01 文档示例生成与就绪检查
起点 `d74ecc43ccee99dcd2e3090356a6fb761825e6af`。从 SITE-02 拆出示例生成和运行器,
保留原父任务负责编辑器声明生成。保留 `yarn build:test`、脚本路径、测试页面及
`docs/test/test.js` URL;没有修改生产包 API、公开声明、播放器 DOM、事件或入口。
## 实现与缺陷
- `scripts/docs-smoke/parser.ts` 用现有 MarkdownIt tokens 替换共享正则游标。
原生成器在异常围栏中反复 continue 且不推进,测试对固定旧提交使用 VM 超时
复现;新解析有限结束,并在语法检查阶段带文件/行号失败。未闭合、空示例和
marker 后不是 JavaScript 围栏也明确报错。只支持现有 Run Code 标记约定。
- `generator.ts` 负责确定性枚举、语法编译、页面脚本依赖和 esbuild 打包;生成前
验证全部输入。保留 11 个中文文件的 233 段旧代码(仅统一 CRLF 为 LF),新增
examples.json 清单。重复生成一致,无时间戳;`--check` 只读拒绝漂移。
- `runtime.ts` 管理每例独立 iframe、顺序依赖、返回 Promise、当前实例 ready、
error/unhandledrejection 和清理。移除固定 100ms 无条件成功;4500ms 是失败
deadline,ready 后仅多一个事件循环任务用于接收未处理拒绝。销毁失败仍移除
frame、恢复 storage,并将原错误和清理错误一起报告。拒绝并发案例。
- 原 JS CLI 是兼容入口,由严格 checkJs 覆盖;自有实现拆成 TS。维护地图和边界见
`scripts/docs-smoke/README.md`,文档站、测试与 CI 说明同次更新。
## 工具链与 CI
根开发依赖新增精确 `@types/node` 24.10.0、`@types/markdown-it` 14.1.2,分别为
Node 工具和已有 MarkdownIt 14.1.0 提供严格类型;没有播放器运行依赖变化。只维护
根 yarn.lock,Node 24.21.0 原生类型擦除执行工具,Yarn Classic 1.22.22 frozen
安装成功。新增 typecheck:docs-tools/check:docs-smoke 加入 ci:check,build:test
加入 ci:build;没有把本地配置视为远端 CI 通过。
安装后发现共享 tsc bin 指向 `typescript-runtime-compat` 5.1.6,而主编译器是
5.9.3。旧别名检查 Node 24 类型因 esnext.disposable/Symbol.dispose 等退出 2;
新工具与根 React 检查改用显式 `node node_modules/typescript/bin/tsc`,保留旧
编译器用于历史消费测试。严格检查不使用 skipLibCheck 掩盖错误。工具配置的
ES2021 lib 用于 Node parser 的 replaceAll;浏览器产物 target 仍为 ES2020。
## 验证与限制
- 5 项生成器回归通过:固定旧代码比较、旧卡死复现、围栏反例、确定性与真实
Mocha 注册、CLI 只读/失败不覆盖既有结果。完整 baseline 522 通过、0 跳过。
- 三浏览器各 11 项,共 33 通过、0 跳过/重试通过:真实核心/受控媒体就绪、
超过旧 100ms 后仍等待、连续挂载清理、三段未改原示例、并发拒绝、同步/返回/
未处理/ready 回调/销毁错误、脚本失败、永不 ready、跨案例定时器与全局隔离。
- 初次扩展测试有 31 通过/2 失败,均为 Windows WebKit 报告 640x360 而非媒体
320x180。原报告保留。按既有 DPIP-MEDIA-01 记录精确两组尺寸,其他浏览器只
接受 320x180;不扩大超时、不重试取绿,也未关闭原生尺寸问题或真实 Safari 门槛。
- 主编译器的工具/React 检查、CI 50 项测试、根 lint(0 error/1 既有生成声明
warning)、严格工具链(22 workspace/38 固定工具/1470 依赖选择器)通过。
完整生产与消费者类型检查结果及输入/输出/日志指纹见验证 JSON。
iframe 是同源归属隔离,不是安全沙箱。需独占测试 origin;storage 恢复会覆盖
其他同源并发写入,不为跨 tab 合并机制。案例交给其它窗口的资源不由本运行器
接管。就绪不等于播放/seek/点击/延迟回调功能验收;完整 233 例、外部 SDK、
所有语言/插件文档与真实设备仍由 EX-03、SITE-04 和包验收覆盖。本次没有手动
连接 Chrome/iab、远端 CI、推送或发布。没有声称全部示例已通过浏览器执行。
## 状态与回退
SITE-SMOKE-01 单独完成提交;SITE-02 的声明生成器尚未完成。生成器生成资产,
不得手改 test.js;解析/语法失败保证写入前退出,磁盘多文件写入不承诺事务性。
回退本任务提交并执行 frozen 安装可恢复旧生成器和依赖;旧无限循环与 100ms
假通过会随之恢复,因此优先修复具体回归。后续改动运行 README 对应命令并
更新当前 site-inventory,历史验证 JSON 保持原证据。
+4 -3
View File
@@ -6,10 +6,11 @@
| 命令 | 行为 |
| --- | --- |
| `yarn lint` | 只读 ESLint,覆盖包源码/声明、JS/MJS 工具、测试和编辑器声明 |
| `yarn lint` | 只读 ESLint,覆盖包源码/声明、JS/MJS 工具、docs-smoke TS 模块、测试和编辑器声明 |
| `yarn lint:fix` | 显式自动修复相同范围 |
| `yarn typecheck` | 根/迁移包严格检查、当前与兼容 TS 消费;历史 NodeNext ESM 错误单独核对,见 typechecking.md |
| `yarn typecheck:react` / `yarn typecheck:vue` | 原 React TSX / Vue SFC 示例严格检查,ci:check 同时执行对应 lint |
| `yarn typecheck:docs-tools` / `yarn check:docs-smoke` | 严格检查 TS 示例生成器及 JS 命令门面;只读核对确定性生成的 readiness smoke,ci:check 执行 |
| `yarn test:react-consumer` / `yarn test:vue-consumer` | 仓库外 tarball 安装、原框架示例、开发/生产三引擎;browser-smoke 执行并上传独立目录,本地证据不代替远端矩阵 |
| `yarn test:unit` | 原播放/DASH 回归、同夹具的新旧公共契约与 JS/TS loader 验证 |
| `yarn test:node` | test:unit 加工具链/文档构建回归,保留原 test:playback/test:dash-control 入口 |
@@ -21,9 +22,9 @@
| `yarn check:contracts --report` | 校验12类契约/22包归属、版本及报告对应;--write更新静态表,详见 [维护说明](contract-coverage.md) |
| `yarn ci:check` | 严格 Node/Yarn/锁检查、计划、只读 lint、类型、Node 和基线测试;允许写忽略缓存,不修改源码 |
| `yarn check:ci` | 只读校验实际工作流的完整系统矩阵、安装、缓存、报告和最终检查;已接入 ci:check |
| `yarn test:ci` | CI 汇总退出码、工作流/影响分析反例与隔离运行时校验,共 44 项 |
| `yarn test:ci` | CI 汇总退出码、工作流/影响分析反例与隔离运行时校验;SITE-SMOKE-01 实测 50 项 |
| `yarn test:package:runtime` | 标准 Node 重装同一已检查 tarball;须先运行 test:package,其他 Node 使用显式 --expected-node |
| `yarn ci:build` | 21 库包、i18n、编辑器声明和文档站构建,以及构建后包导入 smoke;会生成 dist 和 docs 内容 |
| `yarn ci:build` | 21 库包、i18n、编辑器声明、文档 readiness smoke 和文档站构建,以及构建后包导入 smoke;会生成 dist 和 docs 内容 |
| `yarn check:impact --report` | 读取实际依赖/验证关系和Git变更,核对workflow必需命令,写CI影响报告;已接入ci:check,见[影响映射](impact-analysis.md) |
| `yarn build:all` | 保留旧入口,执行 ci:build 后只读 lint |
+6 -4
View File
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 239 项,范围 22 个包及工作区/示例。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 240 项,范围 22 个包及工作区/示例。
状态:todo 60 / doing 15 / blocked 0 / done 164 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 60 / doing 15 / blocked 0 / done 165 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -35,7 +35,7 @@
| artplayer-proxy-mediabunny | 1.2.0 | PKG-MB-01, PKG-MB-02, PKG-MB-03, PKG-MB-04, PKG-MB-05, PKG-MB-06, PKG-MB-07, PKG-MB-08, PKG-MB-09, PKG-MB-10 |
| artplayer-tool-iframe | 1.1.0 | PKG-IFRAME-01, PKG-IFRAME-02, PKG-IFRAME-03, PKG-IFRAME-04, PKG-IFRAME-05, PKG-IFRAME-06 |
| artplayer-tool-thumbnail | 4.4.0 | PKG-TOOL-THUMB-01, PKG-TOOL-THUMB-02, PKG-TOOL-THUMB-03, PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-05, PKG-TOOL-THUMB-06 |
| artplayer-vitepress | 1.1.0 | SITE-01, SITE-02, SITE-03, SITE-04, SITE-05, SITE-06, SITE-07 |
| artplayer-vitepress | 1.1.0 | SITE-01, SITE-SMOKE-01, SITE-02, SITE-03, SITE-04, SITE-05, SITE-06, SITE-07 |
## 0 规划
@@ -381,7 +381,8 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| SITE-01 | artplayer-vitepress, workspace<br>清点文档/示例/生成链 | BASE-04, BASE-05 | 中英文文档、插件页面、demo URL、编辑器声明与生成目录清单;六类 HTML 入口、prod/libs/code/example 加载行为及移动重定向 | 所有公开 API/插件有对应页面或明确补充任务;核实 BASE-DEMO-01 的历史 thumbnail 插件来源及 29 示例/36 HTML 路径;明确 BASE-05 所记录站点实际分发与未设 private 的 manifest 意图;接续 VENDOR-05/06/07/08 和 BASE-SITE-01/BASE-MEDIA-01,核对字体、Monaco、vConsole、console bundle 与样本来源及分发范围 | M | done |
| SITE-02 | artplayer-vitepress<br>整理声明与示例生成器 | SITE-01, ENG-04, ENG-06 | build-ts/build-test 生成链的可验证 TS 脚本 | 不靠字符串拼接掩盖声明错误,生成示例有真实断言或仅标 smoke;替换固定 100ms 成功判定,明确异步错误、清理和生成覆盖限制;坏代码块解析必须终止,覆盖 malformed 分支不推进 regexp 的回归 | M | todo |
| SITE-SMOKE-01 | artplayer-vitepress, workspace<br>迁移示例生成器并验证真实就绪与清理 | SITE-01 | TS Markdown 解析/生成与浏览器运行模块;旧命令和 URL、稳定实例就绪 smoke、错误与资源清理 | 复现旧 malformed 死循环并保证有限失败;保留全部既有示例内容、生成确定且 check 只读;不再以 100ms 作为成功,验证原生就绪、异常、取消与清理;三浏览器检查,明确延迟交互与全部 233 示例仍需 EX-03 | M | done |
| SITE-02 | artplayer-vitepress<br>整理声明与示例生成器 | SITE-01, ENG-04, ENG-06, SITE-SMOKE-01 | build-ts/build-test 生成链的可验证 TS 脚本 | 不靠字符串拼接掩盖声明错误,生成示例有真实断言或仅标 smoke;替换固定 100ms 成功判定,明确异步错误、清理和生成覆盖限制;坏代码块解析必须终止,覆盖 malformed 分支不推进 regexp 的回归 | M | todo |
| SITE-03 | artplayer-vitepress<br>整理 i18n/文档/LLM 生成流程 | SITE-02 | build-i18n/build-docs/build-llm/trans-docs 的任务边界和错误处理 | 原命令兼容、生成可复现,翻译步骤不隐式运行远程服务;覆盖移动 loader 失败恢复 define、脚本依赖顺序、localhost/127.0.0.1 Run Code 目标和语言重定向 | M | todo |
| SITE-04 | artplayer-vitepress<br>交叉核对逐包持续维护的文档 | CORE-21, SITE-03, PKG-CHAPTER-04, PKG-AMBILIGHT-04, PKG-AUDIO-04, PKG-AUTO-THUMB-04, PKG-VTT-THUMB-04, PKG-HLS-04, PKG-DASH-04, PKG-MULTI-SUB-04, PKG-JASSUB-04, PKG-MASK-04, PKG-ASR-04, PKG-ADS-04, PKG-VAST-04, PKG-CAST-04, PKG-DPIP-04, PKG-CANVAS-04, PKG-IFRAME-04, PKG-TOOL-THUMB-04, PKG-DANMUKU-06, PKG-MB-08 | 已随实现更新的中文/英文 API、包内实现地图、旧 JS 示例及已知能力限制的全包核对 | 未把缺环境的能力写成已验证,静态核对不等待设备任务;最终 demo 仍由 EX-03 验收;按 SITE-01 声明成员/候选标题清单逐项语义核对,补齐缺失的双语插件说明和 Danmuku 英文入口 | M | todo |
| SITE-05 | artplayer-vitepress<br>构建文档站和验证链接/示例 | SITE-04, EX-01, EX-02, SITE-07 | VitePress 构建、链接与嵌入 demo 检查 | 文档构建、链接、嵌入路径与声明注入通过;真实完整 demo 保留 EX-03 独立门槛;核对 ENG-PM-01 登记的搜索 peer 范围和真实搜索行为 | M | todo |
@@ -606,6 +607,7 @@
- PKG-TOOL-THUMB-03: [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-input-checkpoint.md) [记录](baselines/thumbnail-input-checkpoint.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md) [记录](baselines/thumbnail-lifecycle-validation.json)
- PKG-TOOL-THUMB-04: [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md) [记录](baselines/thumbnail-runtime-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md) [记录](baselines/thumbnail-public-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-emitter.md) [记录](baselines/thumbnail-emitter-validation.json)
- SITE-01: [记录](site-inventory.md) [记录](baselines/site-inventory.json) [记录](baselines/site-provenance.json) [记录](baselines/demo-additions.json) [记录](changes/2026-09-14-SITE-01-site-inventory.md) [记录](baselines/site-inventory-validation.json)
- SITE-SMOKE-01: [记录](changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md) [记录](baselines/docs-smoke-validation.json) [记录](scripts/docs-smoke.test.mjs)
- SITE-07: [记录](site-inventory.md) [记录](baselines/site-provenance.json)
- EX-01: [记录](changes/2026-09-14-EX-01-react-consumer.md) [记录](baselines/react-consumer-validation.json) [记录](scripts/react-consumer.mjs)
- EX-02: [记录](changes/2026-09-14-EX-02-vue-consumer.md) [记录](baselines/vue-consumer-validation.json) [记录](scripts/vue-consumer.mjs)
+15
View File
@@ -1,5 +1,20 @@
# 进度与证据
## SITE-SMOKE-01 示例生成器与就绪检查完成
从 SITE-02 拆出文档示例工具,TS parser/generator/runtime 与严格检查的原 JS
入口职责分开。固定旧版复现异常围栏无限循环,保留 11 文件 233 段旧示例;
生成清单和浏览器 bundle 可重复、可只读检查。每例 iframe 等待真实 ready、
收集同步/异步错误并清理,替换旧固定 100ms 成功。三浏览器共 33 项、生成器
5 项、完整 baseline 522 项、CI 50 项及 406 生产 TS/消费者检查通过;frozen
安装、工具链与 lint 通过(根 lint 1 既有 warning)。新增两项根开发类型依赖,
显式使用主 TS 5.9.3,修复共享 tsc bin 指向兼容 5.1.6 的检查歧义。Windows
WebKit 原尺寸差异仍保留 DPIP-MEDIA-01;完整 233 例交互/SDK/设备没有计为通过。
见[变更](changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md)与
[证据](baselines/docs-smoke-validation.json)。当前 240 项:165 done、15 doing、
60 todo。下一步 SITE-02 编辑器声明生成器;VAST 兼容选择、Auto Thumbnail 首帧
和各包最终验收仍开放。仅本地独立提交,无推送或发布。
## SITE-01 文档/示例/生成链清点完成
建立当前 22 包、27 Markdown、30 示例、36 HTML、963 声明成员和 204 资产
+122
View File
@@ -0,0 +1,122 @@
import assert from 'node:assert/strict'
import { execFileSync, spawnSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- Repository baseline test runner.
import test from 'node:test'
import vm from 'node:vm'
import { collectExamples, generateDocumentationSmoke } from '../../scripts/docs-smoke/generator.ts'
import { extractExamples } from '../../scripts/docs-smoke/parser.ts'
const root = process.cwd()
const old = execFileSync('git', ['show', 'd74ecc43ccee99dcd2e3090356a6fb761825e6af:scripts/build-test.js'], {
encoding: 'utf8',
})
const oldDefinitions = old.slice(0, old.indexOf('// Main execution')).replace(/^import .*$/gm, '')
const marker = '<div className="run-code">Run Code</div>'
const fence = code => `${marker}\n\n\`\`\`js{2}\n${code}\n\`\`\`\n`
test('Markdown tokens keep all 233 current examples identical after LF normalization', () => {
const examples = collectExamples('packages/artplayer-vitepress/docs')
const context = vm.createContext({ fs, path, console: { warn() {} } })
vm.runInContext(oldDefinitions, context)
for (const file of new Set(examples.map(example => example.file))) {
context.content = fs.readFileSync(`packages/artplayer-vitepress/docs/${file}`, 'utf8').replaceAll('\r\n', '\n')
const oldCodes = vm.runInContext(
'JSON.stringify(extractCodeBlocks(content, "current.md").map(item => item.code))',
context,
{ timeout: 1000 },
)
assert.deepEqual(
examples.filter(example => example.file === file).map(example => example.code),
JSON.parse(oldCodes),
)
}
assert.equal(examples.length, 233)
assert.equal(new Set(examples.map(example => example.id)).size, examples.length)
})
test('Malformed historical regex loops; new parsing terminates and syntax validation rejects it', () => {
const content = fence('var valid = 1;\n## misplaced heading\nvar more = 2;')
const context = vm.createContext({ fs, path, content, console: { warn() {} } })
vm.runInContext(oldDefinitions, context)
assert.throws(() => vm.runInContext('extractCodeBlocks(content, "broken.md")', context, { timeout: 100 }), {
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
})
const [example] = extractExamples(content, 'broken.md')
assert.throws(() => new vm.Script(example.code, { filename: example.id }), SyntaxError)
assert.throws(
() => extractExamples(`${marker}\n\n\`\`\`js\nvar unfinished = 1;`, 'unclosed.md'),
/unclosed.md:1: Unclosed/,
)
})
test('Markdown syntax handles CRLF and ignores markers inside ordinary code fences', () => {
const content = `\`\`\`html\n${marker}\n\`\`\`\n${fence('const value = "\\n## inside a JS string";')}`
const result = extractExamples(content.replaceAll('\n', '\r\n'), 'quoted.md')
assert.equal(result.length, 1)
assert.equal(result[0].line, 7)
assert.throws(() => extractExamples(marker, 'missing.md'), /must be followed/)
assert.throws(() => extractExamples(fence('').replace('js{2}', 'css'), 'wrong.md'), /JavaScript fence/)
assert.throws(() => extractExamples(fence(''), 'empty.md'), /Empty/)
})
test('Smoke output is deterministic and includes isolation instead of the old 100ms completion', async () => {
const first = await generateDocumentationSmoke(root)
assert.deepEqual(await generateDocumentationSmoke(root), first)
assert(!first.get('docs/test/test.js').includes('Generated at:'))
assert(!first.get('docs/test/test.js').includes('setTimeout(() => done(), 100)'))
const data = JSON.parse(first.get('docs/test/examples.json'))
assert.equal(data.examples.length, 233)
assert.equal(data.scripts.length, 11)
assert(data.scope.includes('Not complete feature'))
const registered = []
const context = vm.createContext({
describe(_name, callback) {
callback.call({ timeout() {} })
},
it(name, callback) {
registered.push({ name, callback })
},
})
vm.runInContext(first.get('docs/test/test.js'), context)
assert.deepEqual(
registered.map(item => item.name),
data.examples.map(item => item.id),
)
assert(registered.every(item => typeof item.callback === 'function'))
})
test('CLI checks drift and validates all source examples before changing any output', () => {
const directory = fs.mkdtempSync(path.join(root, 'refactor/.cache/docs-smoke-test-'))
try {
fs.mkdirSync(path.join(directory, 'packages/artplayer-vitepress/docs'), { recursive: true })
fs.mkdirSync(path.join(directory, 'docs/test'), { recursive: true })
fs.cpSync(path.join(root, 'scripts/docs-smoke'), path.join(directory, 'scripts/docs-smoke'), { recursive: true })
const source = path.join(directory, 'packages/artplayer-vitepress/docs/index.md')
fs.writeFileSync(source, fence('var number = 1;'))
fs.writeFileSync(path.join(directory, 'docs/test/index.html'), '<script src="../compiled/artplayer.js"></script>')
const run = args =>
spawnSync(process.execPath, [path.join(root, 'scripts/build-test.js'), ...args], {
cwd: directory,
encoding: 'utf8',
timeout: 30000,
})
assert.equal(run([]).status, 0)
assert.equal(run(['--check']).status, 0)
const output = path.join(directory, 'docs/test/test.js')
fs.appendFileSync(output, '// drift\n')
const drift = run(['--check'])
assert.notEqual(drift.status, 0)
assert.match(drift.stderr, /Generated documentation smoke drift/)
const preserved = fs.readFileSync(output)
fs.writeFileSync(source, fence('const invalid = ;'))
assert.notEqual(run([]).status, 0)
assert.deepEqual(fs.readFileSync(output), preserved)
}
finally {
assert(directory.startsWith(path.join(root, 'refactor/.cache/docs-smoke-test-')))
fs.rmSync(directory, { recursive: true, force: true })
}
})
+1 -1
View File
@@ -78,7 +78,7 @@ export function captureSiteInventory() {
pages: demo.pages,
baselinePaths: { examplesAdded: demo.examples.filter(row => !historical.examples.some(old => old.source === row.source)).map(row => row.source), examplesRemoved: historical.examples.filter(row => !demo.examples.some(now => now.source === row.source)).map(row => row.source), htmlAdded: demo.pages.filter(row => !historical.pages.some(old => old.source === row.source)).map(row => row.source), htmlRemoved: historical.pages.filter(row => !demo.pages.some(now => now.source === row.source)).map(row => row.source) },
editorDeclarations: [...read('docs/assets/js/common.js').matchAll(/'\.\/assets\/ts\/([^']+\.d\.ts)'/g)].map(match => ({ file: `docs/assets/ts/${match[1]}`, exists: fs.existsSync(path.join(root, 'docs/assets/ts', match[1])), owner: 'SITE-02' })),
scripts: scriptFiles.map(file => ({ file, sha256Lf: hash(read(file)) })),
scripts: [...scriptFiles, ...files('scripts/docs-smoke').filter(file => file.endsWith('.ts'))].map(file => ({ file, sha256Lf: hash(read(file)) })),
generationCommands: Object.fromEntries(Object.entries(json('package.json').scripts).filter(([name]) => /^(?:build:(?:types|ts|test|i18n|docs|llm|all)|ci:build)$/.test(name))),
siteManifest: json('packages/artplayer-vitepress/package.json'),
assets,
+5
View File
@@ -1,5 +1,10 @@
# 文档站、示例与生成链清单
SITE-SMOKE-01 后续已将 build:test 拆为 TS 解析、生成和浏览器运行模块,新增
确定性 examples.json 和 readonly check,替换 100ms done 为实例 ready/错误/清理。
以下 SITE-01 的旧生成器问题属于历史发现;当前维护入口见
[docs-smoke README](../scripts/docs-smoke/README.md)。声明生成仍归 SITE-02。
SITE-01 在 `d62ab13a35c756ea567c426d4fbe00d893dd7e2b` 后核对当前源码。
可重跑清单见 [site-inventory.json](baselines/site-inventory.json);它登记
22 包、27 Markdown、30 示例、36 HTML、963 条声明成员和 204 个资产指纹。
+23 -1
View File
@@ -4104,6 +4104,27 @@
"baselines/site-inventory-validation.json"
]
},
{
"id": "SITE-SMOKE-01",
"phase": "6 文档与消费者",
"title": "迁移示例生成器并验证真实就绪与清理",
"scope": [
"artplayer-vitepress",
"workspace"
],
"dependsOn": [
"SITE-01"
],
"status": "done",
"risk": "M",
"deliverable": "TS Markdown 解析/生成与浏览器运行模块;旧命令和 URL、稳定实例就绪 smoke、错误与资源清理",
"acceptance": "复现旧 malformed 死循环并保证有限失败;保留全部既有示例内容、生成确定且 check 只读;不再以 100ms 作为成功,验证原生就绪、异常、取消与清理;三浏览器检查,明确延迟交互与全部 233 示例仍需 EX-03",
"evidence": [
"changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md",
"baselines/docs-smoke-validation.json",
"scripts/docs-smoke.test.mjs"
]
},
{
"id": "SITE-02",
"phase": "6 文档与消费者",
@@ -4114,7 +4135,8 @@
"dependsOn": [
"SITE-01",
"ENG-04",
"ENG-06"
"ENG-06",
"SITE-SMOKE-01"
],
"status": "todo",
"risk": "M",
+10 -134
View File
@@ -1,137 +1,13 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { generateDocumentationSmoke } from './docs-smoke/generator.ts'
// 使用中文文档作为源(中文是原始文档,英文是 AI 翻译的)
const docsDir = path.resolve('packages/artplayer-vitepress/docs')
const outputFile = path.resolve('docs/test/test.js')
// 排除的目录
const excludeDirs = ['en', '.vitepress', 'public', 'plugin']
// Regex to match code blocks after "Run Code" markers
// Match until we find a closing ``` that's on its own line
const runCodePattern = /<div className="run-code">.*?<\/div>[\t\v\f\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*\n\s*```js[^\n]*\n([\s\S]*?)\n```(?=\s*\n|$)/g
function extractCodeBlocks(content, filePath) {
const blocks = []
let match = runCodePattern.exec(content)
while (match !== null) {
const code = match[1].trim()
// Skip if code contains markdown syntax (unclosed code block issue)
if (code.includes('\n## ') || code.includes('\n:::') || code.includes('<div className="run-code">')) {
console.warn(`⚠️ Skipping malformed code block in ${filePath}`)
continue
}
blocks.push({
code,
file: filePath,
})
match = runCodePattern.exec(content)
}
return blocks
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use yarn build:test [--check]')
const outputs = await generateDocumentationSmoke(process.cwd())
for (const [file, content] of outputs) {
if (process.argv.includes('--check'))
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), content, `Generated documentation smoke drift: ${file}`)
else fs.writeFileSync(file, content)
}
function processMarkdownFiles(dir, relativePath = '') {
const results = []
const items = fs.readdirSync(dir)
for (const item of items) {
// 跳过排除的目录
if (excludeDirs.includes(item)) {
continue
}
const fullPath = path.join(dir, item)
const relPath = path.join(relativePath, item)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
results.push(...processMarkdownFiles(fullPath, relPath))
}
else if (item.endsWith('.md')) {
const content = fs.readFileSync(fullPath, 'utf-8')
const blocks = extractCodeBlocks(content, relPath)
if (blocks.length > 0) {
results.push({
file: relPath,
blocks,
})
}
}
}
return results
}
function generateTestCode(fileResults) {
const lines = []
lines.push(`// Auto-generated smoke tests from documentation code blocks`)
lines.push(`// Generated at: ${new Date().toISOString()}`)
lines.push(`// Do not edit manually - run 'node scripts/build-test.js' to regenerate`)
lines.push(``)
lines.push(`describe('Documentation Code Examples', function () {`)
lines.push(` this.timeout(5000);`)
lines.push(``)
lines.push(` beforeEach(function () {`)
lines.push(` // Destroy any existing instances`)
lines.push(` [...Artplayer.instances].forEach((art) => art.destroy());`)
lines.push(` // Reset container`)
lines.push(` const container = document.querySelector('.artplayer-app');`)
lines.push(` if (container) {`)
lines.push(` container.innerHTML = '';`)
lines.push(` }`)
lines.push(` });`)
lines.push(``)
lines.push(` afterEach(function () {`)
lines.push(` [...Artplayer.instances].forEach((art) => art.destroy());`)
lines.push(` });`)
lines.push(``)
for (const { file, blocks } of fileResults) {
const suiteName = file.replace(/\.md$/, '').replace(/\//g, ' > ')
lines.push(` describe('${suiteName}', function () {`)
blocks.forEach((block, index) => {
const testName = `Example ${index + 1}`
lines.push(` it('${testName}', function (done) {`)
lines.push(` try {`)
lines.push(` ${block.code.split('\n').join('\n ')}`)
lines.push(` // Wait a bit for async initialization`)
lines.push(` setTimeout(() => done(), 100);`)
lines.push(` } catch (err) {`)
lines.push(` done(err);`)
lines.push(` }`)
lines.push(` });`)
lines.push(``)
})
lines.push(` });`)
lines.push(``)
}
lines.push(`});`)
return lines.join('\n')
}
// Main execution
const fileResults = processMarkdownFiles(docsDir)
let totalBlocks = 0
for (const { file, blocks } of fileResults) {
console.log(`📄 ${file}: ${blocks.length} code blocks`)
totalBlocks += blocks.length
}
const testCode = generateTestCode(fileResults)
fs.writeFileSync(outputFile, testCode)
console.log(`\n✅ Generated ${totalBlocks} test cases from ${fileResults.length} files`)
console.log(`📝 Output: ${outputFile}`)
console.log(`Documentation readiness smoke ${process.argv.includes('--check') ? 'checked' : 'generated'}: ${outputs.size} outputs; complete demo acceptance remains separate`)
+75
View File
@@ -0,0 +1,75 @@
# Documentation readiness smoke
This generator keeps the existing `yarn build:test` / `node scripts/build-test.js`
entrypoint and `docs/test/test.js` URL. Use the pinned Node 24.21.0 runtime and
Yarn 1.22.22. Node runs the owned TypeScript modules using native type stripping;
the command shim remains JavaScript and is included in strict `checkJs` checking.
The root typecheck commands use `node node_modules/typescript/bin/tsc` explicitly:
Yarn's shared `tsc` binary can point at the installed 5.1.6 compatibility alias.
The main 5.9.3 compiler and older compatibility compilers retain distinct roles.
| File | Ownership |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| `parser.ts` | Markdown tokens, Run Code/fence adjacency, source locations and closing fences |
| `generator.ts` | Deterministic file discovery, syntax compilation without execution, dependency list and browser bundle |
| `runtime.ts` | One sequential example frame, script loading, error/readiness observation, teardown and Mocha registration |
| `../build-test.js` | Original CLI path, argument validation and write/read-only check |
| `../tsconfig.docs.json` | Strict Node/DOM tools check, including the JavaScript shim |
```sh
yarn build:test
yarn check:docs-smoke
yarn typecheck:docs-tools
node --test refactor/scripts/docs-smoke.test.mjs
yarn test:browser test/browser/docs-smoke.spec.js
```
`docs/test/examples.json` records stable source file/line IDs, original code and
the ordered script list from `docs/test/index.html`. No timestamps enter generated
content. Generation validates every selected example before writing either output;
`--check` compares both files without modifying them. Parse/syntax errors fail the
command with a source location. This is not a transactional multi-file filesystem
write if the filesystem itself fails during output.
The scope stays Chinese JavaScript fences after Run Code markers, excluding
`en`, `plugin`, `public` and `.vitepress`. There are 233 current cases in 11 files.
CRLF is normalized to LF. New cases must be reviewed and the deliberate count
assertion updated. JS syntax compilation does not execute snippets or validate
their API semantics. TypeScript/editor declaration generation remains SITE-02.
The browser bundle registers cases with the existing Mocha BDD page. Each case:
1. Creates an iframe with its own ArtPlayer container and loads the page's runtime
scripts in order, excluding Mocha, Chai and the generated test script itself.
2. Evaluates the snippet as a classic script in that frame, awaits its returned
promise if any, and waits for the currently created players' `ready` events.
3. Observes thrown errors, unhandled rejections and media/player errors until that
readiness boundary plus one task turn. A 4500 ms deadline produces failure,
never success. Cases without players cover evaluation only.
4. Detaches observer listeners, destroys frame players, removes the iframe and
restores local/session storage. Cleanup errors are failures and retain an
earlier failure in the error list. Concurrent calls are rejected.
The iframe is an owned browsing context, **not a security sandbox**. It shares
the parent origin. Run the test page on an isolated test origin with no concurrent
same-origin work: storage restoration does not merge writes from other tabs.
Unchanged browser storage is restored after each case, while frame-owned timers
and globals disappear when the frame is removed. Arbitrary external resources
that a snippet deliberately hands to another window are outside this ownership.
Readiness is not playback, interaction or complete asynchronous feature acceptance.
Delayed actions scheduled after this boundary are not verified; examples involving
clicks, seek, SDKs, windows and full lifecycle behavior remain EX-03/package tests.
The automated harness tests use actual core/media and three selected unchanged
documentation snippets with controlled media. They do not certify all 233 examples
or the remote SDK endpoints in the full manual page.
Windows WebKit has the existing DPIP-MEDIA-01 dimension discrepancy: the same
320x180 media can report 640x360 layout dimensions. The harness retains exact raw
readings and only these documented pairs; it does not close intrinsic-size or
real Safari/device acceptance. See `refactor/site-inventory.md` for broader ownership.
Dependencies: root development-only `@types/node` 24.10.0 and
`@types/markdown-it` 14.1.2 provide strict tools typing. Runtime parsing uses the
existing MarkdownIt 14.1.0 and browser bundling uses the existing pinned esbuild.
No player package runtime dependency changes.
+41
View File
@@ -0,0 +1,41 @@
import type { DocumentationExample } from './parser.ts'
import fs from 'node:fs'
import path from 'node:path'
import { Script } from 'node:vm'
import { build } from 'esbuild'
import { extractExamples } from './parser.ts'
export function collectExamples(directory: string, prefix = ''): DocumentationExample[] {
return fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, 'en')).flatMap((entry) => {
if (['en', '.vitepress', 'public', 'plugin'].includes(entry.name))
return []
const relative = prefix ? `${prefix}/${entry.name}` : entry.name
const file = path.join(directory, entry.name)
if (entry.isDirectory())
return collectExamples(file, relative)
if (!entry.name.endsWith('.md'))
return []
const examples = extractExamples(fs.readFileSync(file, 'utf8'), relative)
for (const example of examples) {
// eslint-disable-next-line no-new -- Compile syntax without executing documentation code.
new Script(example.code, { filename: example.id })
}
return examples
})
}
export async function generateDocumentationSmoke(root: string): Promise<Map<string, string>> {
const examples = collectExamples(path.join(root, 'packages/artplayer-vitepress/docs'))
const html = fs.readFileSync(path.join(root, 'docs/test/index.html'), 'utf8').replace(/<!--[\s\S]*?-->/g, '')
const scripts = [...html.matchAll(/<script\b[^>]+\bsrc=["']([^"']+)["']/g)].map(match => match[1]!).filter(url => !['./chai.js', './mocha.js', './test.js'].includes(url))
if (!scripts.some(url => /\/artplayer\.js$/.test(url)))
throw new Error('Documentation smoke page must load the core')
const bundle = await build({ stdin: { contents: `import { registerDocumentationSmoke } from './runtime.ts';\nregisterDocumentationSmoke(${JSON.stringify(examples)}, ${JSON.stringify(scripts)});`, resolveDir: path.join(root, 'scripts/docs-smoke'), sourcefile: 'documentation-smoke-entry.ts', loader: 'ts' }, bundle: true, write: false, platform: 'browser', format: 'iife', target: 'es2020', legalComments: 'none', logLevel: 'silent' })
const script = bundle.outputFiles?.[0]?.text
if (!script)
throw new Error('Documentation smoke build produced no script')
return new Map([
['docs/test/test.js', `// Generated by yarn build:test. Readiness smoke only; do not edit.\n${script}`],
['docs/test/examples.json', `${JSON.stringify({ scope: 'Chinese Run Code JavaScript; excludes en/plugin/public/.vitepress. Waits for created players to become ready, observes errors until that boundary, then destroys the frame. Not complete feature, delayed-callback or interaction acceptance.', examples, scripts }, null, 2)}\n`],
])
}
+37
View File
@@ -0,0 +1,37 @@
import MarkdownIt from 'markdown-it'
export interface DocumentationExample {
id: string
file: string
line: number
code: string
}
const markdown = new MarkdownIt({ html: true })
export function extractExamples(content: string, file: string): DocumentationExample[] {
const normalized = content.replaceAll('\r\n', '\n')
const lines = normalized.split('\n')
const tokens = markdown.parse(normalized, {})
const examples: DocumentationExample[] = []
for (const [index, marker] of tokens.entries()) {
if (marker.type !== 'html_block' || !/<div\s+class(?:Name)?="run-code"[\s>]/.test(marker.content))
continue
const line = (marker.map?.[0] ?? 0) + 1
const invalid = (message: string) => new Error(`${file}:${line}: ${message}`)
const fence = tokens[index + 1]
if (!fence || fence.type !== 'fence' || !/^js(?:\{|\s|$)/.test(fence.info))
throw invalid('Run Code must be followed by a JavaScript fence')
const range = fence.map
if (!range)
throw invalid('Missing code fence source range')
const closing = lines[range[1] - 1]?.trim() || ''
if (closing.length < fence.markup.length || [...closing].some(character => character !== fence.markup[0]))
throw invalid('Unclosed Run Code fence')
const code = fence.content.trim()
if (!code)
throw invalid('Empty Run Code example')
examples.push({ id: `${file}:${range[0] + 2}`, file, line: range[0] + 2, code })
}
return examples
}
+223
View File
@@ -0,0 +1,223 @@
import type Artplayer from '../../packages/artplayer/public/artplayer.ts'
import type { DocumentationExample } from './parser.ts'
interface FrameWindow extends Window {
Artplayer?: typeof Artplayer
eval: (code: string) => unknown
}
export interface SmokeResult {
id: string
instances: number
ready: number
media: { readyState: number, width: number, height: number, duration: number }[]
}
let running = false
/** Evaluate one example in an owned browsing context; readiness is not feature acceptance. */
export async function runExample(
example: DocumentationExample,
scripts: string[],
timeoutMs = 4500,
): Promise<SmokeResult> {
if (running)
throw new Error('Documentation smoke cases must run sequentially')
const savedStorage = [localStorage, sessionStorage].map(storage => ({ storage, entries: Object.entries(storage) }))
const iframe = document.createElement('iframe')
iframe.dataset.documentationSmoke = example.id
iframe.title = `Documentation smoke: ${example.id}`
iframe.style.cssText = 'width:640px;height:360px;border:0;display:block'
const context: { frame: FrameWindow | null } = { frame: null }
let active = true
const disposers: (() => void)[] = []
let fail: (error: unknown) => void = () => {}
const failure = new Promise<never>((_resolve, reject) => {
fail = reject
})
const deadline = window.setTimeout(
() => fail(new Error(`${example.id}: readiness timeout (${timeoutMs}ms)`)),
timeoutMs,
)
running = true
const ensureActive = () => {
if (!active)
throw new Error('Documentation context was disposed')
}
function loaded(target: EventTarget, action: () => void, description: string): Promise<void> {
return new Promise((resolve, reject) => {
const done = () => {
cleanup()
resolve()
}
const error = () => {
cleanup()
reject(new Error(`Failed to load ${description}`))
}
function cleanup() {
target.removeEventListener('load', done)
target.removeEventListener('error', error)
}
target.addEventListener('load', done, { once: true })
target.addEventListener('error', error, { once: true })
disposers.push(() => {
cleanup()
reject(new Error(`Cancelled ${description}`))
})
action()
})
}
const work = async (): Promise<SmokeResult> => {
await loaded(
iframe,
() => {
iframe.srcdoc
= '<!doctype html><html><body style="margin:0"><div class="artplayer-app" style="width:640px;height:360px"></div></body></html>'
document.body.appendChild(iframe)
},
'documentation frame',
)
ensureActive()
const frame = iframe.contentWindow as FrameWindow | null
context.frame = frame
if (!frame)
throw new Error('Documentation frame is unavailable')
const onError = (event: ErrorEvent) => {
event.preventDefault()
fail(new Error(`${example.id}: ${event.message}`))
}
const onRejection = (event: PromiseRejectionEvent) => {
event.preventDefault()
fail(new Error(`${example.id}: ${String(event.reason)}`))
}
frame.addEventListener('error', onError)
frame.addEventListener('unhandledrejection', onRejection)
disposers.push(() => {
frame?.removeEventListener('error', onError)
frame?.removeEventListener('unhandledrejection', onRejection)
})
for (const url of scripts) {
const script = frame.document.createElement('script')
script.src = new URL(url, location.href).href
await loaded(script, () => frame.document.head.appendChild(script), script.src)
ensureActive()
}
if (!frame.Artplayer)
throw new Error('Artplayer was not loaded in the documentation frame')
// Preserve classic-script globals while keeping timers, DOM and listeners inside this frame.
await frame.eval(`${example.code}\n//# sourceURL=artplayer-doc-example-${encodeURIComponent(example.id)}.js`)
ensureActive()
const instances = frame.Artplayer.instances.slice()
await Promise.all(
instances.map(
instance =>
new Promise<void>((resolve, reject) => {
if (instance.isReady) {
resolve()
return
}
const ready = () => {
cleanup()
resolve()
}
const error = () => {
cleanup()
reject(new Error(`${example.id}: player readiness failed`))
}
function cleanup() {
instance.off('ready', ready)
instance.off('error', error)
instance.off('video:error', error)
}
instance.on('ready', ready)
instance.on('error', error)
instance.on('video:error', error)
disposers.push(() => {
cleanup()
reject(new Error('Cancelled player readiness'))
})
}),
),
)
// A task boundary delivers unhandled promise rejections from evaluation/ready callbacks.
await new Promise<void>((resolve) => {
const timer = window.setTimeout(resolve, 0)
disposers.push(() => window.clearTimeout(timer))
})
ensureActive()
return {
id: example.id,
instances: instances.length,
ready: instances.filter(instance => instance.isReady).length,
media: instances.map(instance => ({
readyState: instance.video.readyState,
width: instance.video.videoWidth,
height: instance.video.videoHeight,
duration: instance.video.duration,
})),
}
}
let result: SmokeResult | undefined
let problem: unknown
let failed = false
try {
result = await Promise.race([work(), failure])
}
catch (error) {
failed = true
problem = error
}
finally {
active = false
window.clearTimeout(deadline)
for (const dispose of disposers) dispose()
const cleanupErrors: unknown[] = []
for (const instance of context.frame?.Artplayer?.instances.slice() || []) {
try {
instance.destroy(true)
}
catch (error) {
cleanupErrors.push(error)
}
}
iframe.remove()
for (const { storage, entries } of savedStorage) {
try {
storage.clear()
for (const [key, value] of entries) storage.setItem(key, value)
}
catch (error) {
cleanupErrors.push(error)
}
}
running = false
if (cleanupErrors.length) {
const errors = failed ? [problem, ...cleanupErrors] : cleanupErrors
problem = Object.assign(new Error(`Documentation cleanup failed: ${errors.map(String).join('; ')}`), { errors })
failed = true
}
}
if (failed)
throw problem
if (!result)
throw new Error('Documentation smoke did not produce a result')
return result
}
interface MochaHost {
describe: (name: string, callback: (this: { timeout: (ms: number) => void }) => void) => void
it: (name: string, callback: () => Promise<void>) => void
}
export function registerDocumentationSmoke(examples: DocumentationExample[], scripts: string[]): void {
const host = globalThis as unknown as MochaHost
if (typeof host.describe !== 'function' || typeof host.it !== 'function')
throw new Error('Documentation smoke requires the existing Mocha BDD page')
host.describe('Documentation readiness smoke (not feature acceptance)', function () {
this.timeout(6000)
for (const example of examples) {
host.it(example.id, async () => {
await runExample(example, scripts)
})
}
})
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"types": ["node"],
"allowImportingTsExtensions": true,
"checkJs": true
},
"include": ["docs-smoke/**/*.ts", "build-test.js"]
}
+9
View File
@@ -2,6 +2,15 @@
Use the pinned Node/Yarn toolchain from `../refactor/toolchain-setup.md`.
`yarn build:test` generates deterministic documentation readiness smoke and its
source manifest. `yarn check:docs-smoke` is read-only, and
`yarn typecheck:docs-tools` checks the TS modules and legacy JS command shim.
`refactor/scripts/docs-smoke.test.mjs` covers the historical malformed-parser loop,
all 233 current source snippets, stable generation and CLI drift/error handling.
`test/browser/docs-smoke.spec.js` uses actual core/media in three engines for
readiness, failures, frame cleanup, storage restoration and repeated execution.
See `scripts/docs-smoke/README.md` for the explicit limits of readiness smoke.
`yarn test:vue-consumer` checks the actual Vue example with packed core/Danmuku/
Document PiP in an outside-workspace install. It also installs its pinned Vue
compiler there, checks all declaration paths for isolation, and runs development
+8
View File
@@ -1,5 +1,13 @@
# Browser regression entry
`docs-smoke.spec.js` verifies the documentation runner in three engines with
actual core/media, controlled failures and three selected generated snippets.
It checks pending readiness beyond the former 100ms timer, returned/unhandled
rejections, ready/destroy errors, missing scripts, deadlines, frame/global/timer
cleanup and storage restoration. It does not execute all 233 documentation cases
or prove remote SDKs/features. DPIP-MEDIA-01's exact WebKit intrinsic/layout size
pairs remain explicit observations; this suite does not close that risk.
`yarn test:browser test/browser/ads.spec.js` tests actual Ads 1.0.6 and candidate code
with published 4.5.5/5.4.1 and candidate cores using local image/video media. It covers actual
decoded pixels, countdown, skipping, content restoration, 404 and source replacement.
+212
View File
@@ -0,0 +1,212 @@
import fs from 'node:fs'
import { build } from 'esbuild'
import { expect, test } from './fixtures.js'
const compiled = await build({
entryPoints: ['scripts/docs-smoke/runtime.ts'],
bundle: true,
write: false,
format: 'iife',
globalName: 'DocumentationSmoke',
target: 'es2020',
logLevel: 'silent',
})
const runtime = compiled.outputFiles[0].text
const core = '/candidate/artplayer.js'
const example = code => ({ id: 'fixture.md:1', file: 'fixture.md', line: 1, code })
const player = 'var art = new Artplayer({ container: \'.artplayer-app\', url: \'/test/pattern.mp4\', muted: true });'
const documented = JSON.parse(fs.readFileSync('docs/test/examples.json', 'utf8'))
.examples
.filter(item => item.file === 'start/option.md')
.slice(0, 3)
function expectDimensions(media, browserName) {
// DPIP-MEDIA-01: Windows WebKit can report either intrinsic or styled dimensions.
// Keep exact observed pairs; readiness smoke does not close intrinsic-size compatibility.
const sizes = browserName === 'webkit' ? [[320, 180], [640, 360]] : [[320, 180]]
expect(sizes).toContainEqual([media.width, media.height])
}
test.beforeEach(async ({ page }) => {
await page.goto('/test/player.html')
await page.addScriptTag({ content: runtime })
})
test('readiness smoke uses actual core/media and releases each frame without touching a parent player', async ({
page,
browserName,
}, testInfo) => {
await page.evaluate(() => {
window.createPlayer('/test/pattern.mp4')
localStorage.setItem('smoke-existing', 'preserved')
sessionStorage.setItem('smoke-session', 'preserved')
})
for (let index = 0; index < 3; index++) {
const result = await page.evaluate(
async ({ item, core }) => {
return window.DocumentationSmoke.runExample(item, [core])
},
{
item: example(
`${player}\nlocalStorage.clear(); sessionStorage.clear(); localStorage.setItem('smoke-leak', 'bad');`,
),
core,
},
)
expect(result.instances).toBe(1)
expect(result.ready).toBe(1)
expectDimensions(result.media[0], browserName)
expect(result.media[0].duration).toBeGreaterThan(0)
await testInfo.attach(`readiness-${index}`, { body: JSON.stringify(result), contentType: 'application/json' })
expect(await page.locator('iframe[data-documentation-smoke]').count()).toBe(0)
}
expect(
await page.evaluate(() => ({
players: window.Artplayer.instances.length,
kept: localStorage.getItem('smoke-existing'),
session: sessionStorage.getItem('smoke-session'),
leak: localStorage.getItem('smoke-leak'),
})),
).toEqual({ players: 1, kept: 'preserved', session: 'preserved', leak: null })
await page.evaluate(() => window.art.destroy())
})
test('smoke remains pending beyond 100ms until real media becomes ready', async ({ page }) => {
let release
const gate = new Promise((resolve) => {
release = resolve
})
await page.route('**/test/pattern.mp4', async (route) => {
await gate
await route.continue()
})
try {
await page.evaluate(
({ item, core }) => {
window.smokeState = { done: false }
window.smokePromise = window.DocumentationSmoke.runExample(item, [core]).then((value) => {
window.smokeState = { done: true, value }
})
window.oldCompletion = new Promise(resolve => setTimeout(resolve, 100))
},
{ item: example(player), core },
)
await page.evaluate(() => window.oldCompletion)
expect(await page.evaluate(() => window.smokeState.done)).toBe(false)
release()
await page.evaluate(() => window.smokePromise)
expect(await page.evaluate(() => window.smokeState.value.ready)).toBe(1)
}
finally {
release()
}
})
test('generated real documentation cases run with controlled native media', async ({ page, browserName }) => {
await page.route('**/assets/sample/video.mp4', route =>
route.fulfill({ contentType: 'video/mp4', body: fs.readFileSync('test/browser/media/pattern.mp4') }))
for (const item of documented) {
const result = await page.evaluate(({ item, core }) => window.DocumentationSmoke.runExample(item, [core]), {
item,
core,
})
expect(result.ready).toBe(1)
expectDimensions(result.media[0], browserName)
}
})
test('parallel calls are rejected while a controlled case is still active', async ({ page }) => {
const state = await page.evaluate(
async ({ item, core }) => {
const first = window.DocumentationSmoke.runExample(item, [core])
let rejected
try {
await window.DocumentationSmoke.runExample(item, [core])
}
catch (error) {
rejected = String(error)
}
await first
return rejected
},
{ item: example(player), core },
)
expect(state).toContain('must run sequentially')
})
for (const [name, code, message] of [
['synchronous error', `${player}\nthrow new Error('sync fixture');`, 'sync fixture'],
['returned rejection', `${player}\nPromise.reject(new Error('returned fixture'));`, 'returned fixture'],
['unhandled rejection', `${player}\nPromise.reject(new Error('unhandled fixture')); void 0;`, 'unhandled fixture'],
[
'readiness callback error',
`${player}\nart.on('ready', () => { throw new Error('ready fixture'); });`,
'ready fixture',
],
['cleanup error', `${player}\nart.on('destroy', () => { throw new Error('destroy fixture'); });`, 'destroy fixture'],
]) {
test(`smoke reports ${name} and removes its frame`, async ({ page }) => {
const result = await page.evaluate(
async ({ item, core }) => {
try {
await window.DocumentationSmoke.runExample(item, [core])
return 'unexpected success'
}
catch (error) {
return String(error)
}
},
{ item: example(code), core },
)
expect(result).toContain(message)
expect(await page.locator('iframe[data-documentation-smoke]').count()).toBe(0)
expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0)
})
}
test('missing script and never-ready examples fail instead of becoming green after a delay', async ({ page }) => {
await page.route('**/missing-smoke.js', route => route.fulfill({ status: 404, body: 'missing fixture' }))
const missing = await page.evaluate(async (item) => {
try {
await window.DocumentationSmoke.runExample(item, ['/missing-smoke.js'])
return 'unexpected success'
}
catch (error) {
return String(error)
}
}, example('void 0;'))
expect(missing).toContain('Failed to load')
const timeout = await page.evaluate(
async ({ item, core }) => {
try {
await window.DocumentationSmoke.runExample(item, [core], 1000)
return 'unexpected success'
}
catch (error) {
return String(error)
}
},
{ item: example('new Artplayer({ container: \'.artplayer-app\', url: \'\' });'), core },
)
expect(timeout).toContain('readiness timeout')
expect(await page.locator('iframe[data-documentation-smoke]').count()).toBe(0)
})
test('frame-owned timers and globals do not survive into the next example', async ({ page }) => {
await page.evaluate(
async ({ first, second, core }) => {
await window.DocumentationSmoke.runExample(first, [core])
await window.DocumentationSmoke.runExample(second, [core])
},
{
first: example(
`${player}\nwindow.smokeLeak = 1; setTimeout(() => { throw new Error('late frame callback'); }, 700);`,
),
second: example(
`${player}\nif (window.smokeLeak) throw new Error('global leaked');\nnew Promise(resolve => setTimeout(resolve, 900));`,
),
core,
},
)
expect(await page.locator('iframe[data-documentation-smoke]').count()).toBe(0)
})
+20 -8
View File
@@ -1970,6 +1970,14 @@
resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz"
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
"@types/markdown-it@14.1.2", "@types/markdown-it@^14.1.2":
version "14.1.2"
resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz"
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
dependencies:
"@types/linkify-it" "^5"
"@types/mdurl" "^2"
"@types/markdown-it@^12.2.3":
version "12.2.3"
resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz"
@@ -1978,14 +1986,6 @@
"@types/linkify-it" "*"
"@types/mdurl" "*"
"@types/markdown-it@^14.1.2":
version "14.1.2"
resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz"
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
dependencies:
"@types/linkify-it" "^5"
"@types/mdurl" "^2"
"@types/mdast@^4.0.0":
version "4.0.4"
resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz"
@@ -2013,6 +2013,13 @@
resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz"
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
"@types/node@24.10.0":
version "24.10.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.0.tgz#6b79086b0dfc54e775a34ba8114dcc4e0221f31f"
integrity sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==
dependencies:
undici-types "~7.16.0"
"@types/normalize-package-data@^2.4.0":
version "2.4.4"
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz"
@@ -8227,6 +8234,11 @@ uhyphen@^0.2.0:
resolved "https://registry.yarnpkg.com/uhyphen/-/uhyphen-0.2.0.tgz#8fdf0623314486e020a3c00ee5cc7a12fe722b81"
integrity sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==
undici-types@~7.16.0:
version "7.16.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
unicorn-magic@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.4.0.tgz#78c6a090fd6d07abd2468b83b385603e00dfdb24"