diff --git a/packages/artplayer-tool-iframe/ARCHITECTURE.md b/packages/artplayer-tool-iframe/ARCHITECTURE.md index 91d46aeba..3225747f0 100644 --- a/packages/artplayer-tool-iframe/ARCHITECTURE.md +++ b/packages/artplayer-tool-iframe/ARCHITECTURE.md @@ -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 package-name migration. See ADR-026 and the task 05 history checkpoint for the 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. diff --git a/refactor/baselines/ci-iframe-installed-validation.json b/refactor/baselines/ci-iframe-installed-validation.json new file mode 100644 index 000000000..bfa14fdb7 --- /dev/null +++ b/refactor/baselines/ci-iframe-installed-validation.json @@ -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." + ] +} diff --git a/refactor/changes/2026-09-15-CI-01-iframe-installed.md b/refactor/changes/2026-09-15-CI-01-iframe-installed.md new file mode 100644 index 000000000..2e9f8c48d --- /dev/null +++ b/refactor/changes/2026-09-15-CI-01-iframe-installed.md @@ -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 本地 +检查点提交,之后执行提交审计。回退此提交只撤回安装验证接入及相关记录, +不会回退或改变已迁移的工具生产实现。 diff --git a/refactor/ci-setup.md b/refactor/ci-setup.md index cd4fb2a71..ea24c10fe 100644 --- a/refactor/ci-setup.md +++ b/refactor/ci-setup.md @@ -5,10 +5,16 @@ 核验的统一包清单,执行明确列出的 installed 子集;清单维护在 `scripts/browser-validation/scope.ts`。两步不 吞失败,普通失败后仍保存另一范围的证据,分别上传browser-source/browser-installed。 -入口与报告规则见[维护说明](../scripts/browser-validation/README.md)。source全量 +入口与报告规则见[维护说明](../scripts/browser-validation/README.md)。 目前按 OS × 引擎拆分播放作业,消费者另行执行;各作业的实际远端耗时/60分钟预算、 其余插件安装矩阵与远端运行仍待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 环境与冻结安装。 diff --git a/refactor/plan.md b/refactor/plan.md index 98f5104e5..573a5ee6d 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -509,7 +509,7 @@ - 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-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) - 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) @@ -639,7 +639,7 @@ - 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-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-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) diff --git a/refactor/progress.md b/refactor/progress.md index dddfd6a42..d764e5489 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -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 浏览器引擎与消费者作业拆分 播放检查改为 3 个 OS × 3 个引擎;Node/React/Vue/iframe history/性能每个 OS diff --git a/refactor/tasks.json b/refactor/tasks.json index c2d97e8df..bd9a824ab 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -776,9 +776,11 @@ "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" + "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" ] }, { @@ -4080,7 +4082,9 @@ "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" + "baselines/iframe-history-validation.json", + "changes/2026-09-15-CI-01-iframe-installed.md", + "baselines/ci-iframe-installed-validation.json" ] }, { diff --git a/scripts/browser-validation/README.md b/scripts/browser-validation/README.md index f6833e812..6376747dc 100644 --- a/scripts/browser-validation/README.md +++ b/scripts/browser-validation/README.md @@ -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. 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 the verified installed plugin whenever an artifact map is present. An additional ARTPLAYER_ADS_ARTIFACT override is rejected instead of mixing candidates. Missing, diff --git a/scripts/browser-validation/scope.ts b/scripts/browser-validation/scope.ts index f1ca394f3..ee9562a2a 100644 --- a/scripts/browser-validation/scope.ts +++ b/scripts/browser-validation/scope.ts @@ -2,7 +2,7 @@ import type { PlaywrightTestConfig } from '@playwright/test' import assert from 'node:assert/strict' 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 = [ 'playback.spec.js', 'lifecycle.spec.js', @@ -71,6 +71,11 @@ export const installedTests = [ 'danmuku-scheduler.spec.js', 'danmuku-stability.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) { @@ -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_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(!['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(!['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') } diff --git a/scripts/package-check.mjs b/scripts/package-check.mjs index 0d0c05681..9742ad930 100644 --- a/scripts/package-check.mjs +++ b/scripts/package-check.mjs @@ -43,6 +43,22 @@ export function checkFiles(manifest, files, historical = []) { 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) { const args = { encoding: 'utf8', windowsHide: true } 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-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-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-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(), @@ -108,7 +125,7 @@ export async function checkPackages({ release = false, include = [] } = {}) { for (const name of include) { assert(Object.hasOwn(supported, name), `Additional package needs a reviewed contract: ${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 parent = path.join(workspace, 'refactor/.cache/packages') diff --git a/test/browser-validation.test.js b/test/browser-validation.test.js index a3e75b3ff..9969aca43 100644 --- a/test/browser-validation.test.js +++ b/test/browser-validation.test.js @@ -39,6 +39,11 @@ test('Installed validation rejects diagnostic SDK substitutions and recovery pro const profile = { ...base, ARTPLAYER_MASK_PROFILE: '1' } assert.throws(() => browserInvocation('installed', [], profile), /Mask CPU profiling/) 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']) { const env = { ...base, ARTPLAYER_DASH_DIAGNOSTIC_SDK: mode } assert.throws(() => browserInvocation('installed', [], env), /unchanged DASH SDK/) diff --git a/test/browser/iframe-boundaries.spec.js b/test/browser/iframe-boundaries.spec.js index 163846374..7e2b757f8 100644 --- a/test/browser/iframe-boundaries.spec.js +++ b/test/browser/iframe-boundaries.spec.js @@ -1,9 +1,9 @@ import process from 'node:process' 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' -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 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') { @@ -139,7 +139,7 @@ for (const item of cases) { } const packets = await page.evaluate(() => window.packets) 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 { await page.evaluate(() => { diff --git a/test/browser/iframe-editor.spec.js b/test/browser/iframe-editor.spec.js index 405d0b64f..3151e71d7 100644 --- a/test/browser/iframe-editor.spec.js +++ b/test/browser/iframe-editor.spec.js @@ -1,9 +1,9 @@ import fs from 'node:fs' 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' -const candidate = await iframeCandidate() +const candidate = await iframeBrowserCandidate() 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: '' })) @@ -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) expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0) 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())) }) diff --git a/test/browser/iframe-navigation.spec.js b/test/browser/iframe-navigation.spec.js index cfb77ba31..eecc02357 100644 --- a/test/browser/iframe-navigation.spec.js +++ b/test/browser/iframe-navigation.spec.js @@ -1,9 +1,9 @@ import fs from 'node:fs' 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' -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 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) 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 { 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.old.error).toBe('The iframe document has changed') 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 { await page.evaluate(() => { diff --git a/test/browser/iframe-player.spec.js b/test/browser/iframe-player.spec.js index d3c317ac2..36c5488af 100644 --- a/test/browser/iframe-player.spec.js +++ b/test/browser/iframe-player.spec.js @@ -1,9 +1,9 @@ import fs from 'node:fs' 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' -const candidate = await iframeCandidate() +const candidate = await iframeBrowserCandidate() const published = (await iframeHistorical()).find(item => item.name === 'published-artplayer-plugin-iframe.js') const demo = fs.readFileSync('docs/assets/example/iframe.js', '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 })) 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 { if (child && !child.isDetached()) { diff --git a/test/browser/iframe.spec.js b/test/browser/iframe.spec.js index 66b558e90..153aaf598 100644 --- a/test/browser/iframe.spec.js +++ b/test/browser/iframe.spec.js @@ -1,9 +1,9 @@ import process from 'node:process' 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' -const candidate = { ...await iframeCandidate(), lifecycle: true } +const candidate = { ...await iframeBrowserCandidate(), lifecycle: true } const implementations = process.env.ARTPLAYER_IFRAME_LIFECYCLE_ONLY === '1' ? [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) 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 { await page.evaluate(() => { diff --git a/test/helpers/iframe.js b/test/helpers/iframe.js index a739678f3..fce72eae4 100644 --- a/test/helpers/iframe.js +++ b/test/helpers/iframe.js @@ -1,14 +1,26 @@ +import assert from 'node:assert/strict' import fs from 'node:fs' import process from 'node:process' import vm from 'node:vm' 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' export async function iframeCandidate() { if (process.env.ARTPLAYER_IFRAME_BASELINE === '1') 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() { diff --git a/test/package-check.test.js b/test/package-check.test.js index 069a177b2..20214b365 100644 --- a/test/package-check.test.js +++ b/test/package-check.test.js @@ -4,8 +4,9 @@ import path from 'node:path' import process from 'node:process' // eslint-disable-next-line test/no-import-node-test -- This fixture exercises the repository's Node test runner. import { test } from 'node:test' +import { verifyIframeContract } from '../refactor/scripts/iframe-contract.mjs' 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 { emitterContracts } from './contracts/emitter.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 { canvasCandidate } from './helpers/canvas.js' import { dpipCandidate } from './helpers/dpip.js' +import { iframeBrowserCandidate, iframeCandidate } from './helpers/iframe.js' import { mbCandidate } from './helpers/mediabunny.js' import { multipleSubtitlesCandidate } from './helpers/multiple-subtitles.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/) }) +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 () => { 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_MB_BASELINE', 'ARTPLAYER_MB_ARTIFACT') + keys.push('ARTPLAYER_IFRAME_BASELINE', 'ARTPLAYER_IFRAME_ARTIFACT') const previous = Object.fromEntries(keys.map(key => [key, process.env[key]])) try { 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_MB_BASELINE 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' }) process.env.ARTPLAYER_MB_BASELINE = '1' await assert.rejects(mbCandidate(), /cannot use the frozen workspace/) diff --git a/test/package/README.md b/test/package/README.md index 9e571636d..9959abca5 100644 --- a/test/package/README.md +++ b/test/package/README.md @@ -1,5 +1,13 @@ # 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 `yarn test:rollback`. The seven-step rehearsal restores frozen consumer locks, checks complete installed file inventories and runtime entrypoints, and saves