test(ci): [CI-01] verify installed Iframe tools across window boundaries

This commit is contained in:
Harvey Zhao committed 2026-09-15 04:34:28 +08:00
1 parent 4a9ef16b16
commit ce67eaa73e
19 files changed
+584 -28

No files matched your search

@@ -212,3 +212,28 @@ Physical devices, native Firefox/WebKit cached restoration and final distributio
remain separate gates. Desktop evidence does not establish the old helper or remain separate gates. Desktop evidence does not establish the old helper or
package-name migration. See ADR-026 and the task 05 history checkpoint for the package-name migration. See ADR-026 and the task 05 history checkpoint for the
exact fixture, automation limitations and source/main/legacy evidence. exact fixture, automation limitations and source/main/legacy evidence.
## Installed browser checks
`yarn test:package --browser` includes this tool in the shared installed roster.
The pack checker verifies the real `artplayer-plugin-iframe@1.0.0` archive and
the frozen workspace independently. The tool's required dist/type filenames
come from its verified workspace manifest and files; the old plugin/helper
names are not silently treated as releases of the tool. Publishing or restoring
the predecessor package remains a separate distribution decision.
The five `test/browser/iframe*.spec.js` files use `iframeBrowserCandidate()` from
`test/helpers/iframe.js`. With an installed map it must load the verified tool
UMD, reject an explicit artifact or frozen-workspace override, and record the
package/file/archive/source identity for each candidate parent and child.
Without a map the same tests retain their source and historical controls.
`iframeCandidate()` keeps its existing unit/history semantics: history may
inherit a core-only map, which does not select a tool build. Do not merge these
two fixture entrypoints or claim the separate BFCache suite used an installed
tool when it used a source/explicit tool. Browser scope configuration rejects
diagnostic flags that would remove historical Iframe rows.
The installed tests cover protocol, peer/document boundaries, native navigation,
old/new player combinations and the real docs editor. They do not replace the
special `test:iframe-history` browser channel or physical-device evidence. See
`../../refactor/changes/2026-09-15-CI-01-iframe-installed.md` for measured results.
@@ -0,0 +1,355 @@
{
"schemaVersion": 1,
"task": "CI-01",
"date": "2026-09-15",
"sourceHead": "4a9ef16b168eecc1f39b08290415d76a25852314",
"candidate": "Working tree in the accompanying Iframe installed-browser checkpoint; executable fingerprints normalize LF",
"environment": {
"node": "24.21.0",
"yarn": "1.22.22",
"platform": "win32"
},
"inputs": [
{
"file": "scripts/package-check.mjs",
"sha256LF": "fbab857fd7565dad953b7c0387bd31bb19f986e1e5f6adc5525b2e76e25923b0"
},
{
"file": "scripts/browser-validation/scope.ts",
"sha256LF": "510e857438df5dcd410a4506f074b3864b8eb1e8f05a32acfbca1ff9056ddf3f"
},
{
"file": "test/helpers/iframe.js",
"sha256LF": "46549ef9b090cd69605582b6d011900ad60068b97ce75b0df821194319b1d93e"
},
{
"file": "test/package-check.test.js",
"sha256LF": "81717f7f9dfd46b99d90d84135cfb53181e3453ab9e3d18dd350c101d8781766"
},
{
"file": "test/browser-validation.test.js",
"sha256LF": "d1598e4393b4bb2e4d1e590b6c9744a47cf6aaff7c0086f9fb8d3eb38644cf8c"
},
{
"file": "test/browser/iframe.spec.js",
"sha256LF": "bfa55adb681caef7dcc2ede58f75230f1af281591a2868624522511fa9507baa"
},
{
"file": "test/browser/iframe-boundaries.spec.js",
"sha256LF": "11cce4e116abc0f0d34aa2e84c736071064e7ffaf403bfb294375d60e8c545d1"
},
{
"file": "test/browser/iframe-navigation.spec.js",
"sha256LF": "eae9fa7e840d79bd60b3ab9966517768d7975bc6988a7a6f4ef50723bc017309"
},
{
"file": "test/browser/iframe-player.spec.js",
"sha256LF": "09cd9b63f56b62d403cdfba187d4c854a25a922913623633efe843234667756c"
},
{
"file": "test/browser/iframe-editor.spec.js",
"sha256LF": "fe8865c82287001cc6432a6b9aece8359c1bd5a8298e2386b0db14eb3882b60a"
}
],
"packages": {
"directory": "refactor/.cache/packages/run-qBFP13",
"reportSha256": "58dbe13edcf4a8f529ee973868c6125cbf13b3cfd3a2515c17cf9b2c95c2cfa3",
"count": 19,
"names": [
"artplayer",
"artplayer-plugin-chapter",
"artplayer-plugin-ambilight",
"artplayer-proxy-canvas",
"artplayer-plugin-document-pip",
"artplayer-plugin-ads",
"artplayer-plugin-audio-track",
"artplayer-plugin-vtt-thumbnail",
"artplayer-plugin-multiple-subtitles",
"artplayer-plugin-hls-control",
"artplayer-plugin-dash-control",
"artplayer-plugin-auto-thumbnail",
"artplayer-plugin-asr",
"artplayer-plugin-chromecast",
"artplayer-proxy-mediabunny",
"artplayer-plugin-jassub",
"artplayer-plugin-danmuku",
"artplayer-plugin-danmuku-mask",
"artplayer-tool-iframe"
],
"tool": {
"name": "artplayer-tool-iframe",
"version": "1.1.0",
"archive": "artplayer-tool-iframe.tgz",
"sha256": "e8df09c3edd98de8fa0c731f2cf8769b243560a74d02bddd3e924f8079053365",
"files": {
"package/ARCHITECTURE.md": "dcb0d58fd02730a7a2cc620e4e7e934fae22dbbf85cc9a6eae21ee5ac2cba167",
"package/README.md": "ef5af60bcb831667a78cc3421f6e3c7b5d9a50b3d2e571c5cd02d46fc533e9ce",
"package/dist/artplayer-tool-iframe.js": "7408fceccb000e1b304abe30ac9bc364da7860c3896ad5aaa6af427804c237e6",
"package/dist/artplayer-tool-iframe.legacy.js": "4e8bd39cdc37e98bff9cd19b5e0c9453ebe5231264242fc1c573aa96edb5ea8a",
"package/dist/artplayer-tool-iframe.mjs": "ac4c5efa670c0429fc95bdab7902df60f5b5540f0dad9c7e65c2bd660796dc83",
"package/package.json": "0de9c86eeb3f9a2fb46f9297105b5beddb8d6164163ffaa9874cf5cd68ba7a58",
"package/types/artplayer-tool-iframe.d.cts": "66850135f45c81d4bd7a0de33b6a2f63a655de9000f2698f43660039727919f3",
"package/types/artplayer-tool-iframe.d.mts": "cd394fd68e61fab3c4474daab29852a3f87dd0086b007e7937106871272676fb",
"package/types/artplayer-tool-iframe.d.ts": "e760de22559caf7ffff48d7c5713485bf9f0ddbe876a93080295fb6106535bfa"
}
},
"exitCode": 0,
"wallSeconds": 118.17,
"genericConsumerScope": [
"artplayer",
"artplayer-plugin-chapter"
],
"baseline": "Verified npm artplayer-plugin-iframe@1.0.0 plus frozen artplayer-tool-iframe workspace at 40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f; distinct package identities"
},
"collection": {
"directory": "refactor/.cache/ci01-iframe-collection-report",
"reportSha256": "71d7ae92094a1cac3327619a0ac25c91a75c4d5bb6e3b8a9b4c0beee70e9108c",
"cases": 2406,
"files": 72,
"perEngine": {
"chromium": 802,
"firefox": 802,
"webkit": 802
},
"collectionOnly": true
},
"installed": {
"directory": "refactor/.cache/ci01-iframe-installed-report",
"reportSha256": "0b69259f917ad79a32b9ea65b9d7ca36d91941ea6bf42a2d83e8e6b1786f1c29",
"invocationSha256": "b383fcf488927f9a5bbaa57d1c4d24fb4b558351487ae1043a25918f3d8edf0e",
"args": [
"test",
"--config=playwright.installed.config.js",
"iframe",
"--workers=2"
],
"result": {
"scope": "installed",
"exitCode": 0,
"signal": null,
"args": [
"test",
"--config=playwright.installed.config.js",
"iframe",
"--workers=2"
]
},
"stats": {
"startTime": "2026-09-14T20:25:57.627Z",
"duration": 279343.054,
"expected": 426,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"browsers": {
"chromium": "153.0.8010.12",
"firefox": "155.0",
"webkit": "26.6"
},
"files": {
"chromium/iframe-boundaries.spec.js": {
"expected": 20,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"firefox/iframe-boundaries.spec.js": {
"expected": 20,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"webkit/iframe-boundaries.spec.js": {
"expected": 20,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"chromium/iframe-editor.spec.js": {
"expected": 1,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"firefox/iframe-editor.spec.js": {
"expected": 1,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"webkit/iframe-editor.spec.js": {
"expected": 1,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"chromium/iframe-navigation.spec.js": {
"expected": 39,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"firefox/iframe-navigation.spec.js": {
"expected": 39,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"webkit/iframe-navigation.spec.js": {
"expected": 39,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"chromium/iframe-player.spec.js": {
"expected": 24,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"firefox/iframe-player.spec.js": {
"expected": 24,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"webkit/iframe-player.spec.js": {
"expected": 24,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"chromium/iframe.spec.js": {
"expected": 58,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"firefox/iframe.spec.js": {
"expected": 58,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"webkit/iframe.spec.js": {
"expected": 58,
"skipped": 0,
"unexpected": 0,
"flaky": 0
}
},
"casesWithCandidateIdentity": 264,
"identityRecords": 318,
"identityCountsByProject": {
"chromium": 106,
"firefox": 106,
"webkit": 106
},
"failures": []
},
"source": {
"directory": "refactor/.cache/ci01-iframe-source-report",
"reportSha256": "071cba96f1289cd4064bd777265e9c0c8ecab868c8cca77698f615a35236cb28",
"invocationSha256": "e2bd4beb7fee90c66918cd54cd84fcf479b88be3a7d51c01ea99816b90fb2cce",
"args": [
"test",
"--config=playwright.source.config.js",
"--project=chromium",
"--workers=2",
"--grep",
"(Iframe docs editor Run|Iframe player candidate/same-origin/candidate-source/candidate-source|Iframe document same-origin/attribute-navigation/candidate-source)"
],
"result": {
"scope": "source",
"exitCode": 0,
"signal": null,
"args": [
"test",
"--config=playwright.source.config.js",
"--project=chromium",
"--workers=2",
"--grep",
"(Iframe docs editor Run|Iframe player candidate/same-origin/candidate-source/candidate-source|Iframe document same-origin/attribute-navigation/candidate-source)"
]
},
"stats": {
"startTime": "2026-09-14T20:31:13.013Z",
"duration": 34660.732,
"expected": 3,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"browsers": {
"chromium": "153.0.8010.12"
},
"files": {
"chromium/iframe-editor.spec.js": {
"expected": 1,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"chromium/iframe-navigation.spec.js": {
"expected": 1,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"chromium/iframe-player.spec.js": {
"expected": 1,
"skipped": 0,
"unexpected": 0,
"flaky": 0
}
},
"casesWithCandidateIdentity": 3,
"identityRecords": 4,
"identityCountsByProject": {
"chromium": 4
},
"failures": []
},
"verification": {
"tests": {
"passed": 113,
"failed": 0,
"durationMs": 3402.3125,
"log": "refactor/.cache/ci01-iframe-tests.log",
"logSha256": "cf9dfd8f04bbd1bd5b458fdba9f99661fce167917fd5683d4db8e4c5dee044d5"
},
"lint": {
"exitCode": 0,
"log": "refactor/.cache/ci01-iframe-lint.log",
"logSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"libraryTypes": {
"exitCode": 0,
"log": "refactor/.cache/ci01-iframe-types.log",
"logSha256": "62ec4acffe7f959331ef7e06a10ebd9bc15263dc4c0624aedaed71e65e96c397"
},
"docsToolsTypes": {
"exitCode": 0,
"log": "refactor/.cache/ci01-iframe-docs-types.log",
"logSha256": "4ee15d1ddc61e077c26ded3c173c50a332ff4bc31c883febc247eba00f6826f3"
},
"toolchain": {
"exitCode": 0,
"log": "refactor/.cache/ci01-iframe-toolchain.log",
"logSha256": "0f342a63471393db876e60cb17b1e8ea4d21c2724848e8c50a05eba519ddcae6"
},
"ci": {
"exitCode": 0,
"log": "refactor/.cache/ci01-iframe-ci.log",
"logSha256": "2a42b55e8510c2b958e9dbd0cc99a359e6e80e6a1e1a3ddf0a27590ca3eb9741"
}
},
"limitations": [
"Collection is not a full run of all installed tests.",
"Candidate identity records can count both parent and child; they are not unique test counts. Pure historical rows preserve immutable predecessor/workspace inputs.",
"The separate BFCache/history server retains its source/explicit tool semantics and is not covered by this installed-tool claim.",
"No new npm package identity, helper alias, public API, type, dependency or version change; predecessor distribution decisions remain separate.",
"No remote OS matrix, physical-device or release-review/publication acceptance; CI-01 and PKG-IFRAME-05 remain in progress."
]
}
@@ -0,0 +1,69 @@
# CI-01 Iframe 安装产物与跨窗口组合
## 范围与兼容性
接续 `4a9ef16b168eecc1f39b08290415d76a25852314`。统一 browser 安装清单加入
artplayer-tool-iframe 和五个既有浏览器测试文件。生产源码、公开类型、API、
DOM/事件、依赖版本和实际发布入口没有变化,也没有新增测试重试或放宽等待。
这个包的真实 npm 前身是 artplayer-plugin-iframe@1.0.0,拥有独立的 plugin/
helper 文件;未发布工作区则使用 artplayer-tool-iframe。打包检查先核验真实
旧归档与冻结 Git 源文件,再从冻结 tool manifest/dist/types 提取四个必须
保留的工具文件名。不会伪造 tool 名下的旧 npm 版本,也不会把旧 helper 当成
新工具的等价入口。`historicalDistributionFiles()` 为此提供显式分支;其他
包继续核对同名 npm 分发。旧包名与 helper 的最终分发策略仍由原任务负责。
`iframeBrowserCandidate()` 在有安装 map 时通过共用 browserCandidate 核验
核心/Chapter/工具的新鲜度和安装文件,加载正常 UMD,拒绝显式 artifact 与
冻结工作区替换。父/子窗口候选分别附加安装成员、归档和源文件身份。纯历史
对照仍使用原来的固定 npm/工作区字节;source 入口清除继承的安装 map。
原来的 `iframeCandidate()` 保留 unit/history 的源码或显式产物语义。核对
调用方时确认 history 服务会继承只含核心的 map,因此不能直接让这个旧 helper
改读工具安装包。两种用途有独立入口和回归保护,history 的 BFCache 配置也
保持原样。安装范围拒绝 BASELINE、LIFECYCLE_ONLY、BOUNDARIES_ONLY 三个
诊断开关,防止候选被替换或历史行被删掉。源码诊断仍保留原功能。
## 实际验证
Windows、Node 24.21.0、Yarn 1.22.22;Chromium 153.0.8010.12、Firefox
155.0、WebKit 26.6。完整结果及输入指纹见
[机器证据](../baselines/ci-iframe-installed-validation.json)。
| 检查 | 实际结果 |
| --- | --- |
| `yarn test:package --browser` | 十九包正常构建、pack、隔离安装及离线冻结复装通过,118.17 秒;run-qBFP13 |
| installed 全量 collection | 72 文件、2,406 项,三个引擎各 802 项;只收集,不计为全量运行 |
| `yarn test:browser:installed iframe --workers=2` | 五文件、三个引擎 426/426 通过,279,343.054 ms;每个引擎 142 项,零 skip/失败/flaky/重试 |
| source 定向检查 | Chromium 的真实编辑器、属性导航、新核心新父/子工具组合 3/3 通过,34,660.732 ms |
| Node 单元与工程回归 | 113/113 通过,3,402.3125 ms;包含原 Iframe 行为/生命周期/边界/导航和打包/launcher 负例 |
| 工程检查 | 改动文件只读 ESLint、library/docs-tools 严格类型、固定工具链和 CI 契约通过 |
五文件每个引擎分别为:协议 58、边界 20、导航 39、播放器 24、编辑器 1。
实际协议包含新旧消息、同一时刻请求、销毁中请求及 clone 失败;边界/导航
覆盖同源、跨域、opaque sandbox、重定向、属性/子页面导航及历史对照。播放器
使用三代核心与新旧父/子工具,真实播放、seek、切源、全屏网页控制和销毁。
编辑器通过实际 docs index/Monaco/Run 检查清理;它仍使用既有观察子类和测试
广告脚本拦截,不扩展成生产行为。
安装用例有 264 项附带候选工具身份,共 318 条身份记录;父/子同时使用候选
时会记录两条,不能将身份条数当成用例数。其余 162 项为纯历史对照。源码
三个用例共有四条 source-build 身份,安装 map 为 null。通用 pack 的运行时/
类型消费仍仅 core/chapter:36 运行时、5 旧类型与 8 精确类型模式。Iframe
自己的公开类型证据保留在 PKG-IFRAME-04,本次不虚报全包通用类型覆盖。
报告在进程实际结束后归档为 `refactor/.cache/ci01-iframe-installed-report`、
`ci01-iframe-source-report` 和 `ci01-iframe-collection-report`;同前缀日志保存
真实终态输出。复跑先执行 test:package --browser、设置其 browser-artifacts.json,
再执行上述 installed 命令。源码定向正则见机器记录中的 args;全量 CI 不使用
这个定向正则。重跑前先归档同一 scope 的旧报告。
## 剩余门槛
CI-01、PKG-IFRAME-05 继续 doing;197/263 完成不变。此次没有重跑特殊 Chromium
channel 的 BFCache suite,也不声称 Firefox/WebKit 实际缓存恢复、物理设备、
旧包 helper 分发或发布复盘已完成。远端完整 OS × 引擎矩阵仍未运行。
没有 push、部署或 npm 发布。代码、测试、架构说明和证据随独立 CI-01 本地
检查点提交,之后执行提交审计。回退此提交只撤回安装验证接入及相关记录,
不会回退或改变已迁移的工具生产实现。
+7 -1
View File
@@ -5,10 +5,16 @@
核验的统一包清单,执行明确列出的 installed 子集;清单维护在 核验的统一包清单,执行明确列出的 installed 子集;清单维护在
`scripts/browser-validation/scope.ts`。两步不 `scripts/browser-validation/scope.ts`。两步不
吞失败,普通失败后仍保存另一范围的证据,分别上传browser-source/browser-installed。 吞失败,普通失败后仍保存另一范围的证据,分别上传browser-source/browser-installed。
入口与报告规则见[维护说明](../scripts/browser-validation/README.md)。source全量 入口与报告规则见[维护说明](../scripts/browser-validation/README.md)。
目前按 OS × 引擎拆分播放作业,消费者另行执行;各作业的实际远端耗时/60分钟预算、 目前按 OS × 引擎拆分播放作业,消费者另行执行;各作业的实际远端耗时/60分钟预算、
其余插件安装矩阵与远端运行仍待CI-01/CI-04,不计为已通过。 其余插件安装矩阵与远端运行仍待CI-01/CI-04,不计为已通过。
Iframe 接入后共同安装清单含十九包;新增五文件本地三引擎 426/426 通过,
完整 installed collection 为 72 文件/2,406 项,见
[安装记录](changes/2026-09-15-CI-01-iframe-installed.md)。原 browser-consumers 的
history 仍保留源码/显式工具加核心 map 的独立语义,不因新增安装清单而自动
宣称 BFCache 已使用安装工具验收。
## 本地命令 ## 本地命令
使用 toolchain-setup.md 的 Node/Yarn 环境与冻结安装。 使用 toolchain-setup.md 的 Node/Yarn 环境与冻结安装。
+2 -2
View File
@@ -509,7 +509,7 @@
- ENG-09: [记录](changes/2026-09-12-ENG-09-integration.md) [记录](baselines/engineering-integration.json) - ENG-09: [记录](changes/2026-09-12-ENG-09-integration.md) [记录](baselines/engineering-integration.json)
- ENG-10: [记录](changes/2026-09-10-ENG-10-test-reliability.md) [记录](test-reliability.md) - ENG-10: [记录](changes/2026-09-10-ENG-10-test-reliability.md) [记录](test-reliability.md)
- ENG-11: [记录](build-analysis.md) [记录](baselines/bundle-attribution.json) [记录](changes/2026-09-11-ENG-11-build-analysis.md) - ENG-11: [记录](build-analysis.md) [记录](baselines/bundle-attribution.json) [记录](changes/2026-09-11-ENG-11-build-analysis.md)
- CI-01: [记录](ci-setup.md) [记录](changes/2026-09-13-CI-01-matrix-summary.md) [记录](baselines/ci-matrix-validation.json) [记录](changes/2026-09-13-CI-01-node-consumers.md) [记录](baselines/node-consumer-validation.json) [记录](changes/2026-09-14-CI-01-installed-full.md) [记录](baselines/ci-installed-full-validation.json) [记录](changes/2026-09-14-CI-01-source-chromium.md) [记录](baselines/ci-source-chromium-validation.json) [记录](changes/2026-09-15-CI-01-ads-installed.md) [记录](baselines/ci-ads-installed-validation.json) [记录](changes/2026-09-15-CI-01-subtitles-installed.md) [记录](baselines/ci-subtitles-installed-validation.json) [记录](changes/2026-09-15-CI-01-adaptive-installed.md) [记录](baselines/ci-adaptive-installed-validation.json) [记录](changes/2026-09-15-CI-01-asr-cast-installed.md) [记录](baselines/ci-asr-cast-installed-validation.json) [记录](changes/2026-09-15-CI-01-mediabunny-installed.md) [记录](baselines/ci-mediabunny-installed-validation.json) [记录](changes/2026-09-15-CI-01-jassub-installed.md) [记录](baselines/ci-jassub-installed-validation.json) [记录](changes/2026-09-15-CI-01-danmuku-installed.md) [记录](baselines/ci-danmuku-installed-validation.json) [记录](changes/2026-09-15-CI-01-browser-matrix.md) [记录](baselines/ci-browser-matrix-validation.json) - CI-01: [记录](ci-setup.md) [记录](changes/2026-09-13-CI-01-matrix-summary.md) [记录](baselines/ci-matrix-validation.json) [记录](changes/2026-09-13-CI-01-node-consumers.md) [记录](baselines/node-consumer-validation.json) [记录](changes/2026-09-14-CI-01-installed-full.md) [记录](baselines/ci-installed-full-validation.json) [记录](changes/2026-09-14-CI-01-source-chromium.md) [记录](baselines/ci-source-chromium-validation.json) [记录](changes/2026-09-15-CI-01-ads-installed.md) [记录](baselines/ci-ads-installed-validation.json) [记录](changes/2026-09-15-CI-01-subtitles-installed.md) [记录](baselines/ci-subtitles-installed-validation.json) [记录](changes/2026-09-15-CI-01-adaptive-installed.md) [记录](baselines/ci-adaptive-installed-validation.json) [记录](changes/2026-09-15-CI-01-asr-cast-installed.md) [记录](baselines/ci-asr-cast-installed-validation.json) [记录](changes/2026-09-15-CI-01-mediabunny-installed.md) [记录](baselines/ci-mediabunny-installed-validation.json) [记录](changes/2026-09-15-CI-01-jassub-installed.md) [记录](baselines/ci-jassub-installed-validation.json) [记录](changes/2026-09-15-CI-01-danmuku-installed.md) [记录](baselines/ci-danmuku-installed-validation.json) [记录](changes/2026-09-15-CI-01-browser-matrix.md) [记录](baselines/ci-browser-matrix-validation.json) [记录](changes/2026-09-15-CI-01-iframe-installed.md) [记录](baselines/ci-iframe-installed-validation.json)
- CI-02: [记录](changes/2026-09-14-CI-02-pages-artifact.md) [记录](baselines/pages-artifact-validation.json) [记录](pages-deployment.md) - CI-02: [记录](changes/2026-09-14-CI-02-pages-artifact.md) [记录](baselines/pages-artifact-validation.json) [记录](pages-deployment.md)
- PILOT-01: [记录](changes/2026-09-10-PILOT-01-chapter.md) [记录](baselines/pilot-validation.json) - PILOT-01: [记录](changes/2026-09-10-PILOT-01-chapter.md) [记录](baselines/pilot-validation.json)
- CORE-01: [记录](changes/2026-09-10-CORE-01-typed-utils.md) [记录](baselines/core-utils-validation.json) - CORE-01: [记录](changes/2026-09-10-CORE-01-typed-utils.md) [记录](baselines/core-utils-validation.json)
@@ -639,7 +639,7 @@
- PKG-IFRAME-02: [记录](changes/2026-09-12-PKG-IFRAME-02-behavior.md) [记录](baselines/iframe-behavior-validation.json) - PKG-IFRAME-02: [记录](changes/2026-09-12-PKG-IFRAME-02-behavior.md) [记录](baselines/iframe-behavior-validation.json)
- PKG-IFRAME-03: [记录](changes/2026-09-12-PKG-IFRAME-03-requests-checkpoint.md) [记录](baselines/iframe-requests-checkpoint.json) [记录](changes/2026-09-12-PKG-IFRAME-03-boundaries-checkpoint.md) [记录](baselines/iframe-boundaries-checkpoint.json) [记录](iframe-message-boundary.md) [记录](changes/2026-09-13-PKG-IFRAME-03-navigation.md) [记录](baselines/iframe-navigation-validation.json) [记录](iframe-document-protocol.md) - PKG-IFRAME-03: [记录](changes/2026-09-12-PKG-IFRAME-03-requests-checkpoint.md) [记录](baselines/iframe-requests-checkpoint.json) [记录](changes/2026-09-12-PKG-IFRAME-03-boundaries-checkpoint.md) [记录](baselines/iframe-boundaries-checkpoint.json) [记录](iframe-message-boundary.md) [记录](changes/2026-09-13-PKG-IFRAME-03-navigation.md) [记录](baselines/iframe-navigation-validation.json) [记录](iframe-document-protocol.md)
- PKG-IFRAME-04: [记录](changes/2026-09-13-PKG-IFRAME-04-types.md) [记录](baselines/iframe-types-validation.json) - PKG-IFRAME-04: [记录](changes/2026-09-13-PKG-IFRAME-04-types.md) [记录](baselines/iframe-types-validation.json)
- PKG-IFRAME-05: [记录](changes/2026-09-13-PKG-IFRAME-05-integration.md) [记录](baselines/iframe-integration-validation.json) [记录](changes/2026-09-13-PKG-IFRAME-05-history.md) [记录](baselines/iframe-history-validation.json) - PKG-IFRAME-05: [记录](changes/2026-09-13-PKG-IFRAME-05-integration.md) [记录](baselines/iframe-integration-validation.json) [记录](changes/2026-09-13-PKG-IFRAME-05-history.md) [记录](baselines/iframe-history-validation.json) [记录](changes/2026-09-15-CI-01-iframe-installed.md) [记录](baselines/ci-iframe-installed-validation.json)
- PKG-TOOL-THUMB-01: [记录](baselines/thumbnail-release.json) [记录](baselines/thumbnail-contract.md) [记录](changes/2026-09-13-PKG-TOOL-THUMB-01-contract.md) [记录](baselines/thumbnail-contract-validation.json) - PKG-TOOL-THUMB-01: [记录](baselines/thumbnail-release.json) [记录](baselines/thumbnail-contract.md) [记录](changes/2026-09-13-PKG-TOOL-THUMB-01-contract.md) [记录](baselines/thumbnail-contract-validation.json)
- PKG-TOOL-THUMB-02: [记录](baselines/thumbnail-behavior-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-02-behavior.md) - PKG-TOOL-THUMB-02: [记录](baselines/thumbnail-behavior-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-02-behavior.md)
- 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-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)
+9
View File
@@ -1,5 +1,14 @@
# 进度与证据 # 进度与证据
## CI-01 Iframe 安装包与真实跨窗口组合
统一清单现含十九包,保留 Iframe 真实旧 npm 包与冻结 tool 工作区的不同身份。
五文件三引擎 426/426 全部通过,零跳过/失败/重试;覆盖历史协议、窗口/文档
边界、导航、新旧核心/父子工具和实际编辑器。源码定向 3/3、单元及工程 113/113。
见[记录](changes/2026-09-15-CI-01-iframe-installed.md)。完整安装 collection
为 72 文件/2,406 项,并未全跑;197/263 完成。CI-01/IFRAME-05 继续 doing,
其余生态分发、特殊 BFCache、设备和远端矩阵门槛保留。
## CI-01 浏览器引擎与消费者作业拆分 ## CI-01 浏览器引擎与消费者作业拆分
播放检查改为 3 个 OS × 3 个引擎;Node/React/Vue/iframe history/性能每个 OS 播放检查改为 3 个 OS × 3 个引擎;Node/React/Vue/iframe history/性能每个 OS
+8 -4
View File
@@ -776,9 +776,11 @@
"changes/2026-09-15-CI-01-jassub-installed.md", "changes/2026-09-15-CI-01-jassub-installed.md",
"baselines/ci-jassub-installed-validation.json", "baselines/ci-jassub-installed-validation.json",
"changes/2026-09-15-CI-01-danmuku-installed.md", "changes/2026-09-15-CI-01-danmuku-installed.md",
"baselines/ci-danmuku-installed-validation.json", "baselines/ci-danmuku-installed-validation.json",
"changes/2026-09-15-CI-01-browser-matrix.md", "changes/2026-09-15-CI-01-browser-matrix.md",
"baselines/ci-browser-matrix-validation.json" "baselines/ci-browser-matrix-validation.json",
"changes/2026-09-15-CI-01-iframe-installed.md",
"baselines/ci-iframe-installed-validation.json"
] ]
}, },
{ {
@@ -4080,7 +4082,9 @@
"changes/2026-09-13-PKG-IFRAME-05-integration.md", "changes/2026-09-13-PKG-IFRAME-05-integration.md",
"baselines/iframe-integration-validation.json", "baselines/iframe-integration-validation.json",
"changes/2026-09-13-PKG-IFRAME-05-history.md", "changes/2026-09-13-PKG-IFRAME-05-history.md",
"baselines/iframe-history-validation.json" "baselines/iframe-history-validation.json",
"changes/2026-09-15-CI-01-iframe-installed.md",
"baselines/ci-iframe-installed-validation.json"
] ]
}, },
{ {
+9
View File
@@ -20,6 +20,15 @@ and source/build freshness. `scope.ts` lists the currently supported installed
test files. This is a growing installed subset, not full ecosystem acceptance. test files. This is a growing installed subset, not full ecosystem acceptance.
Source checks remain complete when the installed list grows. Source checks remain complete when the installed list grows.
Iframe adds five installed files through `iframeBrowserCandidate()`. Its actual
published predecessor is `artplayer-plugin-iframe`; required current tool paths
are checked against the frozen workspace manifest rather than invented npm tool
contents. Candidate parent/child evidence includes installed provenance, while
historical rows retain their own names and bytes. Installed scope rejects the
Iframe baseline/lifecycle-only/boundaries-only diagnostic flags. The separate
history server still uses `iframeCandidate()` and may receive a core-only map;
its source/explicit tool selection has not become installed-tool acceptance.
Ads media/lifecycle and UI files use `test/helpers/browser-candidate.js` to select Ads media/lifecycle and UI files use `test/helpers/browser-candidate.js` to select
the verified installed plugin whenever an artifact map is present. An additional the verified installed plugin whenever an artifact map is present. An additional
ARTPLAYER_ADS_ARTIFACT override is rejected instead of mixing candidates. Missing, ARTPLAYER_ADS_ARTIFACT override is rejected instead of mixing candidates. Missing,
+7 -1
View File
@@ -2,7 +2,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
export type BrowserScope = 'source' | 'installed' export type BrowserScope = 'source' | 'installed'
export const installedPackages = ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight', 'artplayer-proxy-canvas', 'artplayer-plugin-document-pip', 'artplayer-plugin-ads', 'artplayer-plugin-audio-track', 'artplayer-plugin-vtt-thumbnail', 'artplayer-plugin-multiple-subtitles', 'artplayer-plugin-hls-control', 'artplayer-plugin-dash-control', 'artplayer-plugin-auto-thumbnail', 'artplayer-plugin-asr', 'artplayer-plugin-chromecast', 'artplayer-proxy-mediabunny', 'artplayer-plugin-jassub', 'artplayer-plugin-danmuku', 'artplayer-plugin-danmuku-mask'] export const installedPackages = ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight', 'artplayer-proxy-canvas', 'artplayer-plugin-document-pip', 'artplayer-plugin-ads', 'artplayer-plugin-audio-track', 'artplayer-plugin-vtt-thumbnail', 'artplayer-plugin-multiple-subtitles', 'artplayer-plugin-hls-control', 'artplayer-plugin-dash-control', 'artplayer-plugin-auto-thumbnail', 'artplayer-plugin-asr', 'artplayer-plugin-chromecast', 'artplayer-proxy-mediabunny', 'artplayer-plugin-jassub', 'artplayer-plugin-danmuku', 'artplayer-plugin-danmuku-mask', 'artplayer-tool-iframe']
export const installedTests = [ export const installedTests = [
'playback.spec.js', 'playback.spec.js',
'lifecycle.spec.js', 'lifecycle.spec.js',
@@ -71,6 +71,11 @@ export const installedTests = [
'danmuku-scheduler.spec.js', 'danmuku-scheduler.spec.js',
'danmuku-stability.spec.js', 'danmuku-stability.spec.js',
'danmuku-timing-diagnostic.spec.js', 'danmuku-timing-diagnostic.spec.js',
'iframe.spec.js',
'iframe-boundaries.spec.js',
'iframe-navigation.spec.js',
'iframe-player.spec.js',
'iframe-editor.spec.js',
] ]
export function browserInvocation(scope: string, args: string[], environment: NodeJS.ProcessEnv) { export function browserInvocation(scope: string, args: string[], environment: NodeJS.ProcessEnv) {
@@ -84,6 +89,7 @@ export function browserInvocation(scope: string, args: string[], environment: No
assert(env.ARTPLAYER_BROWSER_ARTIFACTS, 'Installed browser checks require ARTPLAYER_BROWSER_ARTIFACTS') assert(env.ARTPLAYER_BROWSER_ARTIFACTS, 'Installed browser checks require ARTPLAYER_BROWSER_ARTIFACTS')
assert(env.ARTPLAYER_MB_BROWSER_CANDIDATE !== '1', 'Installed browser checks must retain MediaBunny historical controls') assert(env.ARTPLAYER_MB_BROWSER_CANDIDATE !== '1', 'Installed browser checks must retain MediaBunny historical controls')
assert(env.ARTPLAYER_MASK_PROFILE !== '1', 'Installed browser checks cannot enable Mask CPU profiling') assert(env.ARTPLAYER_MASK_PROFILE !== '1', 'Installed browser checks cannot enable Mask CPU profiling')
assert(!['ARTPLAYER_IFRAME_BASELINE', 'ARTPLAYER_IFRAME_LIFECYCLE_ONLY', 'ARTPLAYER_IFRAME_BOUNDARIES_ONLY'].some(name => env[name] === '1'), 'Installed browser checks must retain Iframe candidate and historical controls')
assert(!env.ARTPLAYER_DASH_DIAGNOSTIC_SDK || env.ARTPLAYER_DASH_DIAGNOSTIC_SDK === 'none', 'Installed browser checks require the unchanged DASH SDK') assert(!env.ARTPLAYER_DASH_DIAGNOSTIC_SDK || env.ARTPLAYER_DASH_DIAGNOSTIC_SDK === 'none', 'Installed browser checks require the unchanged DASH SDK')
assert(!['ARTPLAYER_DASH_DIAGNOSE_STALL', 'ARTPLAYER_DASH_DIAGNOSE_GETTER', 'ARTPLAYER_DASH_DIAGNOSE_METRICS'].some(name => env[name] === '1'), 'Installed browser checks cannot use DASH recovery diagnostics') assert(!['ARTPLAYER_DASH_DIAGNOSE_STALL', 'ARTPLAYER_DASH_DIAGNOSE_GETTER', 'ARTPLAYER_DASH_DIAGNOSE_METRICS'].some(name => env[name] === '1'), 'Installed browser checks cannot use DASH recovery diagnostics')
} }
+18 -1
View File
@@ -43,6 +43,22 @@ export function checkFiles(manifest, files, historical = []) {
assert(!files.includes('package/tsconfig.json'), 'Implementation tsconfig leaked into package') assert(!files.includes('package/tsconfig.json'), 'Implementation tsconfig leaked into package')
} }
export function historicalDistributionFiles(name, { baseline, sources }) {
if (name !== 'artplayer-tool-iframe') {
assert.equal(baseline.release.name, name, 'Review renamed package distribution separately')
return Object.keys(baseline.release.files)
}
assert.equal(baseline.release.name, 'artplayer-plugin-iframe', 'Iframe keeps its distinct published predecessor')
const prefix = `packages/${name}/`
const manifest = JSON.parse(sources.get(`${prefix}package.json`))
assert.equal(manifest.name, name, 'Iframe distribution must use the frozen tool manifest')
const files = Object.keys(baseline.source).filter(file => file.startsWith(`${prefix}dist/`) || file.startsWith(`${prefix}types/`))
assert(files.length > 0 && files.every(file => sources.has(file)), 'Missing verified iframe workspace distribution')
const members = files.map(file => `package/${file.slice(prefix.length)}`)
checkFiles(manifest, members)
return members
}
export function packedFiles(archive) { export function packedFiles(archive) {
const args = { encoding: 'utf8', windowsHide: true } const args = { encoding: 'utf8', windowsHide: true }
const names = execFileSync('tar', ['-tzf', archive], args).trim().split(/\r?\n/) const names = execFileSync('tar', ['-tzf', archive], args).trim().split(/\r?\n/)
@@ -99,6 +115,7 @@ export async function checkPackages({ release = false, include = [] } = {}) {
'artplayer-plugin-jassub': async () => (await import('../refactor/scripts/jassub-contract.mjs')).verifyJassubContract(), 'artplayer-plugin-jassub': async () => (await import('../refactor/scripts/jassub-contract.mjs')).verifyJassubContract(),
'artplayer-plugin-danmuku': async () => (await import('../refactor/scripts/danmuku-contract.mjs')).verifyDanmukuContract(), 'artplayer-plugin-danmuku': async () => (await import('../refactor/scripts/danmuku-contract.mjs')).verifyDanmukuContract(),
'artplayer-plugin-danmuku-mask': async () => (await import('../refactor/scripts/danmuku-mask-contract.mjs')).verifyDanmukuMaskContract(), 'artplayer-plugin-danmuku-mask': async () => (await import('../refactor/scripts/danmuku-mask-contract.mjs')).verifyDanmukuMaskContract(),
'artplayer-tool-iframe': async () => (await import('../refactor/scripts/iframe-contract.mjs')).verifyIframeContract(),
'artplayer-plugin-audio-track': async () => (await import('../refactor/scripts/audio-contract.mjs')).verifyAudioContract(), 'artplayer-plugin-audio-track': async () => (await import('../refactor/scripts/audio-contract.mjs')).verifyAudioContract(),
'artplayer-plugin-vtt-thumbnail': async () => (await import('../refactor/scripts/vtt-thumbnail-contract.mjs')).verifyVttThumbnailContract(), 'artplayer-plugin-vtt-thumbnail': async () => (await import('../refactor/scripts/vtt-thumbnail-contract.mjs')).verifyVttThumbnailContract(),
'artplayer-plugin-multiple-subtitles': async () => (await import('../refactor/scripts/multiple-subtitles-contract.mjs')).verifyMultipleSubtitlesContract(), 'artplayer-plugin-multiple-subtitles': async () => (await import('../refactor/scripts/multiple-subtitles-contract.mjs')).verifyMultipleSubtitlesContract(),
@@ -108,7 +125,7 @@ export async function checkPackages({ release = false, include = [] } = {}) {
for (const name of include) { for (const name of include) {
assert(Object.hasOwn(supported, name), `Additional package needs a reviewed contract: ${name}`) assert(Object.hasOwn(supported, name), `Additional package needs a reviewed contract: ${name}`)
const contract = await supported[name]() const contract = await supported[name]()
historicalFiles.set(name, Object.keys(contract.baseline.release.files)) historicalFiles.set(name, historicalDistributionFiles(name, contract))
} }
const names = [...defaultNames, ...include] const names = [...defaultNames, ...include]
const parent = path.join(workspace, 'refactor/.cache/packages') const parent = path.join(workspace, 'refactor/.cache/packages')
+5
View File
@@ -39,6 +39,11 @@ test('Installed validation rejects diagnostic SDK substitutions and recovery pro
const profile = { ...base, ARTPLAYER_MASK_PROFILE: '1' } const profile = { ...base, ARTPLAYER_MASK_PROFILE: '1' }
assert.throws(() => browserInvocation('installed', [], profile), /Mask CPU profiling/) assert.throws(() => browserInvocation('installed', [], profile), /Mask CPU profiling/)
assert.equal(browserInvocation('source', [], profile).env.ARTPLAYER_MASK_PROFILE, '1') assert.equal(browserInvocation('source', [], profile).env.ARTPLAYER_MASK_PROFILE, '1')
for (const name of ['ARTPLAYER_IFRAME_BASELINE', 'ARTPLAYER_IFRAME_LIFECYCLE_ONLY', 'ARTPLAYER_IFRAME_BOUNDARIES_ONLY']) {
const env = { ...base, [name]: '1' }
assert.throws(() => browserInvocation('installed', [], env), /retain Iframe candidate and historical controls/)
assert.equal(browserInvocation('source', [], env).env[name], '1')
}
for (const mode of ['upstream4', 'bufferlevel4']) { for (const mode of ['upstream4', 'bufferlevel4']) {
const env = { ...base, ARTPLAYER_DASH_DIAGNOSTIC_SDK: mode } const env = { ...base, ARTPLAYER_DASH_DIAGNOSTIC_SDK: mode }
assert.throws(() => browserInvocation('installed', [], env), /unchanged DASH SDK/) assert.throws(() => browserInvocation('installed', [], env), /unchanged DASH SDK/)
+3 -3
View File
@@ -1,9 +1,9 @@
import process from 'node:process' import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs' import { hash } from '../../refactor/scripts/releases.mjs'
import { iframeCandidate, iframeHistorical } from '../helpers/iframe.js' import { iframeBrowserCandidate, iframeHistorical } from '../helpers/iframe.js'
import { expect, test } from './fixtures.js' import { expect, test } from './fixtures.js'
const candidate = await iframeCandidate() const candidate = await iframeBrowserCandidate()
const old = (await iframeHistorical()).filter(item => ['published-artplayer-plugin-iframe.js', 'workspace.js'].includes(item.name)) const old = (await iframeHistorical()).filter(item => ['published-artplayer-plugin-iframe.js', 'workspace.js'].includes(item.name))
const cases = ['same-origin', 'cross-origin', 'opaque-origin', 'redirect'].flatMap(relation => ['parent-guard', 'child-guard', 'malformed'].map(scenario => ({ relation, scenario, parent: candidate, child: candidate }))) const cases = ['same-origin', 'cross-origin', 'opaque-origin', 'redirect'].flatMap(relation => ['parent-guard', 'child-guard', 'malformed'].map(scenario => ({ relation, scenario, parent: candidate, child: candidate })))
if (process.env.ARTPLAYER_IFRAME_BOUNDARIES_ONLY !== '1') { if (process.env.ARTPLAYER_IFRAME_BOUNDARIES_ONLY !== '1') {
@@ -139,7 +139,7 @@ for (const item of cases) {
} }
const packets = await page.evaluate(() => window.packets) const packets = await page.evaluate(() => window.packets)
expect(packets.filter(packet => packet.fromChild).every(packet => packet.origin === eventOrigin)).toBe(true) expect(packets.filter(packet => packet.fromChild).every(packet => packet.origin === eventOrigin)).toBe(true)
await testInfo.attach('iframe-boundary', { contentType: 'application/json', body: JSON.stringify({ relation: item.relation, scenario: item.scenario, parent: { name: item.parent.name, sha256: hash(item.parent.code) }, child: { name: item.child.name, sha256: hash(item.child.code) }, parentOrigin, initialUrl, childUrl, eventOrigin, redirects, result, packets, scope: 'Actual window peers and native source/origin. Redirect is an HTTP 302 from the local test server; opaque peer is sandbox allow-scripts. Mixed-version rows cover normal wire compatibility only, not security of an unchanged historical peer. No player/media integration.' }) }) await testInfo.attach('iframe-boundary', { contentType: 'application/json', body: JSON.stringify({ relation: item.relation, scenario: item.scenario, parent: { name: item.parent.name, sha256: hash(item.parent.code), provenance: item.parent.provenance || null }, child: { name: item.child.name, sha256: hash(item.child.code), provenance: item.child.provenance || null }, parentOrigin, initialUrl, childUrl, eventOrigin, redirects, result, packets, scope: 'Actual window peers and native source/origin. Redirect is an HTTP 302 from the local test server; opaque peer is sandbox allow-scripts. Mixed-version rows cover normal wire compatibility only, not security of an unchanged historical peer. No player/media integration.' }) })
} }
finally { finally {
await page.evaluate(() => { await page.evaluate(() => {
+3 -3
View File
@@ -1,9 +1,9 @@
import fs from 'node:fs' import fs from 'node:fs'
import { hash } from '../../refactor/scripts/releases.mjs' import { hash } from '../../refactor/scripts/releases.mjs'
import { iframeCandidate } from '../helpers/iframe.js' import { iframeBrowserCandidate } from '../helpers/iframe.js'
import { expect, test } from './fixtures.js' import { expect, test } from './fixtures.js'
const candidate = await iframeCandidate() const candidate = await iframeBrowserCandidate()
test('Iframe docs editor Run releases the previous tool and its pending requests', async ({ page }, testInfo) => { test('Iframe docs editor Run releases the previous tool and its pending requests', async ({ page }, testInfo) => {
await page.route('https://pagead2.googlesyndication.com/**', route => route.fulfill({ contentType: 'text/javascript', body: '' })) await page.route('https://pagead2.googlesyndication.com/**', route => route.fulfill({ contentType: 'text/javascript', body: '' }))
@@ -67,6 +67,6 @@ test('Iframe docs editor Run releases the previous tool and its pending requests
await expect.poll(() => page.evaluate(() => window.exampleFinished)).toBe(true) await expect.poll(() => page.evaluate(() => window.exampleFinished)).toBe(true)
expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0) expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0)
expect(await page.evaluate(() => window.previousPlayers.every(art => art.isDestroy))).toBe(true) expect(await page.evaluate(() => window.previousPlayers.every(art => art.isDestroy))).toBe(true)
await testInfo.attach('iframe-editor', { contentType: 'application/json', body: JSON.stringify({ candidateSha256: hash(candidate.code), files: Object.fromEntries(['docs/index.html', 'docs/iframe.html', 'docs/assets/example/iframe.js', 'docs/assets/js/common.js'].map(file => [file, hash(fs.readFileSync(file))])), scope: 'Actual docs index, local Monaco and Run button. Only the iframe constructor is observed through a test subclass; external advertising script is stubbed in this test only. No physical-device/BFCache claim.' }) }) await testInfo.attach('iframe-editor', { contentType: 'application/json', body: JSON.stringify({ candidateSha256: hash(candidate.code), provenance: candidate.provenance || null, files: Object.fromEntries(['docs/index.html', 'docs/iframe.html', 'docs/assets/example/iframe.js', 'docs/assets/js/common.js'].map(file => [file, hash(fs.readFileSync(file))])), scope: 'Actual docs index, local Monaco and Run button. Only the iframe constructor is observed through a test subclass; external advertising script is stubbed in this test only. No physical-device/BFCache claim.' }) })
await page.evaluate(() => window.demoTools.forEach(tool => tool.destroy())) await page.evaluate(() => window.demoTools.forEach(tool => tool.destroy()))
}) })
+4 -4
View File
@@ -1,9 +1,9 @@
import fs from 'node:fs' import fs from 'node:fs'
import { hash } from '../../refactor/scripts/releases.mjs' import { hash } from '../../refactor/scripts/releases.mjs'
import { iframeCandidate, iframeHistorical } from '../helpers/iframe.js' import { iframeBrowserCandidate, iframeHistorical } from '../helpers/iframe.js'
import { expect, test } from './fixtures.js' import { expect, test } from './fixtures.js'
const implementation = await iframeCandidate() const implementation = await iframeBrowserCandidate()
const scenarios = ['inject-before-load', 'duplicate-inject', 'attribute-navigation', 'child-navigation', 'rapid-navigation', 'same-source-reload', 'fragment-navigation', 'attribute-fragment', 'stale-packets', 'history-back', 'srcdoc-navigation'] const scenarios = ['inject-before-load', 'duplicate-inject', 'attribute-navigation', 'child-navigation', 'rapid-navigation', 'same-source-reload', 'fragment-navigation', 'attribute-fragment', 'stale-packets', 'history-back', 'srcdoc-navigation']
const childHtml = fs.readFileSync(new URL('./iframe-boundary-child.html', import.meta.url), 'utf8') const childHtml = fs.readFileSync(new URL('./iframe-boundary-child.html', import.meta.url), 'utf8')
@@ -222,7 +222,7 @@ for (const relation of ['same-origin', 'cross-origin', 'opaque-origin']) {
} }
const timeline = await page.evaluate(() => window.timeline) const timeline = await page.evaluate(() => window.timeline)
expect(await page.evaluate(() => window.callbacks.some(packet => packet.type === 'artplayer-tool-iframe:session'))).toBe(false) expect(await page.evaluate(() => window.callbacks.some(packet => packet.type === 'artplayer-tool-iframe:session'))).toBe(false)
await testInfo.attach('iframe-navigation', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), relation, scenario, parentOrigin, childOrigin, result, timeline, scope: 'Actual native frame navigation and postMessage; history-back records actual pageshow.persisted rather than assuming bfcache. Delayed-load barrier forces injection before load. Stale-packet tests replay old metadata from the controlled current frame to isolate document filtering. No media/player integration.' }) }) await testInfo.attach('iframe-navigation', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), provenance: implementation.provenance || null, relation, scenario, parentOrigin, childOrigin, result, timeline, scope: 'Actual native frame navigation and postMessage; history-back records actual pageshow.persisted rather than assuming bfcache. Delayed-load barrier forces injection before load. Stale-packet tests replay old metadata from the controlled current frame to isolate document filtering. No media/player integration.' }) })
} }
finally { finally {
releaseBarrier?.() releaseBarrier?.()
@@ -278,7 +278,7 @@ for (const historical of (await iframeHistorical()).filter(item => ['published-a
expect(result.states.new.value).toBe('2') expect(result.states.new.value).toBe('2')
expect(result.states.old.error).toBe('The iframe document has changed') expect(result.states.old.error).toBe('The iframe document has changed')
expect(result.pending).toBe(0) expect(result.pending).toBe(0)
await testInfo.attach('iframe-navigation', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), relation, scenario: 'legacy-child-source-change', child: { name: historical.name, sha256: hash(historical.code) }, parentOrigin, childOrigin, result, scope: 'Actual source attribute navigation with an immutable historical child. No document metadata or internal child navigation protection is claimed for an unchanged legacy peer.' }) }) await testInfo.attach('iframe-navigation', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), provenance: implementation.provenance || null, relation, scenario: 'legacy-child-source-change', child: { name: historical.name, sha256: hash(historical.code) }, parentOrigin, childOrigin, result, scope: 'Actual source attribute navigation with an immutable historical child. No document metadata or internal child navigation protection is claimed for an unchanged legacy peer.' }) })
} }
finally { finally {
await page.evaluate(() => { await page.evaluate(() => {
+3 -3
View File
@@ -1,9 +1,9 @@
import fs from 'node:fs' import fs from 'node:fs'
import { hash } from '../../refactor/scripts/releases.mjs' import { hash } from '../../refactor/scripts/releases.mjs'
import { iframeCandidate, iframeHistorical } from '../helpers/iframe.js' import { iframeBrowserCandidate, iframeHistorical } from '../helpers/iframe.js'
import { expect, test } from './fixtures.js' import { expect, test } from './fixtures.js'
const candidate = await iframeCandidate() const candidate = await iframeBrowserCandidate()
const published = (await iframeHistorical()).find(item => item.name === 'published-artplayer-plugin-iframe.js') const published = (await iframeHistorical()).find(item => item.name === 'published-artplayer-plugin-iframe.js')
const demo = fs.readFileSync('docs/assets/example/iframe.js', 'utf8') const demo = fs.readFileSync('docs/assets/example/iframe.js', 'utf8')
const childHtml = fs.readFileSync('docs/iframe.html', 'utf8') const childHtml = fs.readFileSync('docs/iframe.html', 'utf8')
@@ -116,7 +116,7 @@ for (const core of ['published-4.5.9', 'published', 'candidate']) {
return 17 return 17
})) }))
expect(afterDestroy).toBe(17) expect(afterDestroy).toBe(17)
await testInfo.attach('iframe-player', { contentType: 'application/json', body: JSON.stringify({ core, coreSha256: hash(coreBytes), parent: { name: parentTool.name, sha256: hash(parentTool.code) }, child: { name: childTool.name, sha256: hash(childTool.code) }, relation, initial, seek, switched, destroyed, events, demoSha256: hash(demo), adaptedDemoSha256: hash(adaptedDemo), childHtmlSha256: hash(childHtml), scope: 'Actual docs iframe.html, parent example and CSS in a minimal parent harness; only source URL and explicit test-only old global-name aliases are adapted. Actual media plays/seeks/switches, fullscreenWeb control sends messages and both core/tool lifetimes are tested independently. No complete Monaco/device/BFCache claim.' }) }) await testInfo.attach('iframe-player', { contentType: 'application/json', body: JSON.stringify({ core, coreSha256: hash(coreBytes), parent: { name: parentTool.name, sha256: hash(parentTool.code), provenance: parentTool.provenance || null }, child: { name: childTool.name, sha256: hash(childTool.code), provenance: childTool.provenance || null }, relation, initial, seek, switched, destroyed, events, demoSha256: hash(demo), adaptedDemoSha256: hash(adaptedDemo), childHtmlSha256: hash(childHtml), scope: 'Actual docs iframe.html, parent example and CSS in a minimal parent harness; only source URL and explicit test-only old global-name aliases are adapted. Actual media plays/seeks/switches, fullscreenWeb control sends messages and both core/tool lifetimes are tested independently. No complete Monaco/device/BFCache claim.' }) })
} }
finally { finally {
if (child && !child.isDetached()) { if (child && !child.isDetached()) {
+3 -3
View File
@@ -1,9 +1,9 @@
import process from 'node:process' import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs' import { hash } from '../../refactor/scripts/releases.mjs'
import { iframeCandidate, iframeHistorical } from '../helpers/iframe.js' import { iframeBrowserCandidate, iframeHistorical } from '../helpers/iframe.js'
import { expect, test } from './fixtures.js' import { expect, test } from './fixtures.js'
const candidate = { ...await iframeCandidate(), lifecycle: true } const candidate = { ...await iframeBrowserCandidate(), lifecycle: true }
const implementations = process.env.ARTPLAYER_IFRAME_LIFECYCLE_ONLY === '1' const implementations = process.env.ARTPLAYER_IFRAME_LIFECYCLE_ONLY === '1'
? [candidate] ? [candidate]
: [...(await iframeHistorical()).filter(item => item.global !== 'ArtplayerHelperIframe'), candidate] : [...(await iframeHistorical()).filter(item => item.global !== 'ArtplayerHelperIframe'), candidate]
@@ -213,7 +213,7 @@ for (const implementation of implementations) {
} }
const packets = await page.evaluate(() => window.packets) const packets = await page.evaluate(() => window.packets)
expect(packets.filter(packet => packet.fromChild).every(packet => packet.origin === childOrigin)).toBe(true) expect(packets.filter(packet => packet.fromChild).every(packet => packet.origin === childOrigin)).toBe(true)
await testInfo.attach('iframe-behavior', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), relation, scenario, parentOrigin, childOrigin, foreignOrigin, result, packets, lifecycleAcceptance: Boolean(implementation.lifecycle), scope: 'Actual browser iframe WindowProxy/source/origin and postMessage. Local route fulfills immutable library bytes. Date.now is controlled for collision and distinct clone-failure IDs. Only the deliberate child error/DataCloneError is intercepted. Historical defects are asserted for historical rows; lifecycle acceptance requires cleanup and absence of uncaught clone errors. Navigation and trust are not candidate acceptance yet.' }) }) await testInfo.attach('iframe-behavior', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), provenance: implementation.provenance || null, relation, scenario, parentOrigin, childOrigin, foreignOrigin, result, packets, lifecycleAcceptance: Boolean(implementation.lifecycle), scope: 'Actual browser iframe WindowProxy/source/origin and postMessage. Local route fulfills immutable library bytes. Date.now is controlled for collision and distinct clone-failure IDs. Only the deliberate child error/DataCloneError is intercepted. Historical defects are asserted for historical rows; lifecycle acceptance requires cleanup and absence of uncaught clone errors. Navigation and trust are not candidate acceptance yet.' }) })
} }
finally { finally {
await page.evaluate(() => { await page.evaluate(() => {
+14 -2
View File
@@ -1,14 +1,26 @@
import assert from 'node:assert/strict'
import fs from 'node:fs' import fs from 'node:fs'
import process from 'node:process' import process from 'node:process'
import vm from 'node:vm' import vm from 'node:vm'
import { verifyIframeContract } from '../../refactor/scripts/iframe-contract.mjs' import { verifyIframeContract } from '../../refactor/scripts/iframe-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs' import { hash, readMember } from '../../refactor/scripts/releases.mjs'
import { browserCandidate } from './browser-candidate.js'
import { compilePackage } from './load.js' import { compilePackage } from './load.js'
export async function iframeCandidate() { export async function iframeCandidate() {
if (process.env.ARTPLAYER_IFRAME_BASELINE === '1') if (process.env.ARTPLAYER_IFRAME_BASELINE === '1')
return (await iframeHistorical()).find(item => item.name === 'workspace.js') return (await iframeHistorical()).find(item => item.name === 'workspace.js')
return { name: process.env.ARTPLAYER_IFRAME_ARTIFACT ? 'candidate-artifact' : 'candidate-source', global: 'ArtplayerToolIframe', namespace: false, code: process.env.ARTPLAYER_IFRAME_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_IFRAME_ARTIFACT, 'utf8') : await compilePackage('artplayer-tool-iframe', 'umd') } const artifact = process.env.ARTPLAYER_IFRAME_ARTIFACT
const code = artifact ? fs.readFileSync(artifact, 'utf8') : await compilePackage('artplayer-tool-iframe', 'umd')
return { name: artifact ? 'candidate-artifact' : 'candidate-source', global: 'ArtplayerToolIframe', namespace: false, code, provenance: { kind: artifact ? 'explicit-artifact' : 'source-build', file: artifact || null, sha256: hash(code) } }
}
export async function iframeBrowserCandidate() {
if (!process.env.ARTPLAYER_BROWSER_ARTIFACTS)
return iframeCandidate()
assert(process.env.ARTPLAYER_IFRAME_BASELINE !== '1', 'Installed Iframe cannot use the frozen workspace')
const loaded = await browserCandidate('artplayer-tool-iframe', process.env.ARTPLAYER_IFRAME_ARTIFACT)
return { name: 'candidate-installed', global: 'ArtplayerToolIframe', namespace: false, ...loaded }
} }
export async function iframeHistorical() { export async function iframeHistorical() {
+32 -1
View File
@@ -4,8 +4,9 @@ import path from 'node:path'
import process from 'node:process' import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- This fixture exercises the repository's Node test runner. // eslint-disable-next-line test/no-import-node-test -- This fixture exercises the repository's Node test runner.
import { test } from 'node:test' import { test } from 'node:test'
import { verifyIframeContract } from '../refactor/scripts/iframe-contract.mjs'
import { installedPackages } from '../scripts/browser-validation/scope.ts' import { installedPackages } from '../scripts/browser-validation/scope.ts'
import { checkFiles, checkPackages, packageOptions, publishedConsumer } from '../scripts/package-check.mjs' import { checkFiles, checkPackages, historicalDistributionFiles, packageOptions, publishedConsumer } from '../scripts/package-check.mjs'
import { removeConsumer, runtimeConsumer } from '../scripts/package-consumer.mjs' import { removeConsumer, runtimeConsumer } from '../scripts/package-consumer.mjs'
import { emitterContracts } from './contracts/emitter.js' import { emitterContracts } from './contracts/emitter.js'
import { ambilightCandidate } from './helpers/ambilight.js' import { ambilightCandidate } from './helpers/ambilight.js'
@@ -13,6 +14,7 @@ import { autoThumbnailCandidate } from './helpers/auto-thumbnail.js'
import { browserCandidate } from './helpers/browser-candidate.js' import { browserCandidate } from './helpers/browser-candidate.js'
import { canvasCandidate } from './helpers/canvas.js' import { canvasCandidate } from './helpers/canvas.js'
import { dpipCandidate } from './helpers/dpip.js' import { dpipCandidate } from './helpers/dpip.js'
import { iframeBrowserCandidate, iframeCandidate } from './helpers/iframe.js'
import { mbCandidate } from './helpers/mediabunny.js' import { mbCandidate } from './helpers/mediabunny.js'
import { multipleSubtitlesCandidate } from './helpers/multiple-subtitles.js' import { multipleSubtitlesCandidate } from './helpers/multiple-subtitles.js'
import { vttThumbnailCandidate } from './helpers/vtt-thumbnail.js' import { vttThumbnailCandidate } from './helpers/vtt-thumbnail.js'
@@ -30,10 +32,30 @@ test('Additional browser packages cannot be mistaken for full release consumer a
await assert.rejects(checkPackages({ release: true, include: ['artplayer-plugin-ambilight'] }), /not full release acceptance/) await assert.rejects(checkPackages({ release: true, include: ['artplayer-plugin-ambilight'] }), /not full release acceptance/)
}) })
test('Iframe installation preserves verified tool paths without inventing a renamed npm archive', async () => {
const contract = await verifyIframeContract()
const expected = ['package/dist/artplayer-tool-iframe.js', 'package/dist/artplayer-tool-iframe.legacy.js', 'package/dist/artplayer-tool-iframe.mjs', 'package/types/artplayer-tool-iframe.d.ts']
assert.deepEqual(historicalDistributionFiles('artplayer-tool-iframe', contract).sort(), expected)
assert.throws(() => historicalDistributionFiles('artplayer-unreviewed-name', contract), /renamed package/)
for (const member of expected) {
const file = member.replace('package/', 'packages/artplayer-tool-iframe/')
const sources = new Map(contract.sources)
sources.delete(file)
assert.throws(() => historicalDistributionFiles('artplayer-tool-iframe', { ...contract, sources }), /Missing verified iframe/)
const baseline = structuredClone(contract.baseline)
delete baseline.source[file]
assert.throws(() => historicalDistributionFiles('artplayer-tool-iframe', { ...contract, baseline }), /Missing .* file/)
}
const baseline = structuredClone(contract.baseline)
baseline.release.name = 'artplayer-tool-iframe'
assert.throws(() => historicalDistributionFiles('artplayer-tool-iframe', { ...contract, baseline }), /distinct published predecessor/)
})
test('Explicit installed plugin maps never fall back to source or frozen workspace', async () => { test('Explicit installed plugin maps never fall back to source or frozen workspace', async () => {
const keys = ['ARTPLAYER_BROWSER_ARTIFACTS', 'ARTPLAYER_AMBILIGHT_BASELINE', 'ARTPLAYER_CANVAS_BASELINE', 'ARTPLAYER_DPIP_BASELINE', 'ARTPLAYER_DPIP_ARTIFACT', 'ARTPLAYER_VTT_THUMBNAIL_BASELINE', 'ARTPLAYER_VTT_THUMBNAIL_ARTIFACT', 'ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT'] const keys = ['ARTPLAYER_BROWSER_ARTIFACTS', 'ARTPLAYER_AMBILIGHT_BASELINE', 'ARTPLAYER_CANVAS_BASELINE', 'ARTPLAYER_DPIP_BASELINE', 'ARTPLAYER_DPIP_ARTIFACT', 'ARTPLAYER_VTT_THUMBNAIL_BASELINE', 'ARTPLAYER_VTT_THUMBNAIL_ARTIFACT', 'ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT']
keys.push('ARTPLAYER_AUTO_THUMBNAIL_BASELINE', 'ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT') keys.push('ARTPLAYER_AUTO_THUMBNAIL_BASELINE', 'ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT')
keys.push('ARTPLAYER_MB_BASELINE', 'ARTPLAYER_MB_ARTIFACT') keys.push('ARTPLAYER_MB_BASELINE', 'ARTPLAYER_MB_ARTIFACT')
keys.push('ARTPLAYER_IFRAME_BASELINE', 'ARTPLAYER_IFRAME_ARTIFACT')
const previous = Object.fromEntries(keys.map(key => [key, process.env[key]])) const previous = Object.fromEntries(keys.map(key => [key, process.env[key]]))
try { try {
process.env.ARTPLAYER_BROWSER_ARTIFACTS = path.resolve('refactor/.cache/absent-installed-plugin-map.json') process.env.ARTPLAYER_BROWSER_ARTIFACTS = path.resolve('refactor/.cache/absent-installed-plugin-map.json')
@@ -49,6 +71,15 @@ test('Explicit installed plugin maps never fall back to source or frozen workspa
delete process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT delete process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT
delete process.env.ARTPLAYER_MB_BASELINE delete process.env.ARTPLAYER_MB_BASELINE
delete process.env.ARTPLAYER_MB_ARTIFACT delete process.env.ARTPLAYER_MB_ARTIFACT
delete process.env.ARTPLAYER_IFRAME_BASELINE
delete process.env.ARTPLAYER_IFRAME_ARTIFACT
await assert.rejects(iframeBrowserCandidate(), { code: 'ENOENT' })
assert.equal((await iframeCandidate()).provenance.kind, 'source-build', 'Existing unit/history helpers do not consume the core-only map as a tool artifact')
process.env.ARTPLAYER_IFRAME_BASELINE = '1'
await assert.rejects(iframeBrowserCandidate(), /cannot use the frozen workspace/)
delete process.env.ARTPLAYER_IFRAME_BASELINE
process.env.ARTPLAYER_IFRAME_ARTIFACT = 'override.js'
await assert.rejects(iframeBrowserCandidate(), /cannot override artplayer-tool-iframe artifact/)
await assert.rejects(mbCandidate(), { code: 'ENOENT' }) await assert.rejects(mbCandidate(), { code: 'ENOENT' })
process.env.ARTPLAYER_MB_BASELINE = '1' process.env.ARTPLAYER_MB_BASELINE = '1'
await assert.rejects(mbCandidate(), /cannot use the frozen workspace/) await assert.rejects(mbCandidate(), /cannot use the frozen workspace/)
+8
View File
@@ -1,5 +1,13 @@
# Installed package checks # Installed package checks
`historicalDistributionFiles()` in `scripts/package-check.mjs` distinguishes a
same-name npm release from the Iframe package rename. Iframe verifies the real
plugin archive plus the frozen tool workspace, then requires all four original
tool dist/type paths. Missing verified workspace members fail packing; legacy
plugin/helper filenames remain a separate distribution gate. This does not
change the generic core/chapter runtime/type consumer scope. The shared browser
roster now also checks the installed Iframe tool in five browser test files.
For independent core/plugin rollback, run `yarn test:package` then For independent core/plugin rollback, run `yarn test:package` then
`yarn test:rollback`. The seven-step rehearsal restores frozen consumer locks, `yarn test:rollback`. The seven-step rehearsal restores frozen consumer locks,
checks complete installed file inventories and runtime entrypoints, and saves checks complete installed file inventories and runtime entrypoints, and saves