mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
test(iframe): [PKG-IFRAME-01] freeze historical exports and message contract
This commit is contained in:
1 parent
7e37fd4d0e
commit
a52f79a0cc
12 files changed
+505
-8
No files matched your search
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"task": "PKG-IFRAME-01",
|
||||
"status": "done",
|
||||
"sourcesSha256LF": {
|
||||
"refactor/scripts/iframe-contract.mjs": "a709828ac0f3d35eb8f94ff915d8243e42903adb4e75f32a4907829e4fdf38b1",
|
||||
"refactor/scripts/iframe-contract.test.mjs": "c1ab0bfbb88d0604c6b9e5467f5e581f0fdae75c5da4fea55b00ebcf2a3bb69f",
|
||||
"test/helpers/iframe.js": "fd05f4fc10e97f1a0b8961ea8f82976ea9d58d0ea75bb652af2ee13b8a89c9a0",
|
||||
"refactor/baselines/iframe-release.json": "c58a952a4a5a2f0c02e7c76521537898874eb6d7955b9daa6b427c51aeadece7",
|
||||
"refactor/baselines/iframe-contract.md": "ac1c2ed68c02f0981ecff21edeeafde260dbf739caa763ca1b28c91831dba087"
|
||||
},
|
||||
"frozenArchiveMembers": 8,
|
||||
"frozenWorkspaceInputs": 11,
|
||||
"contractTests": 19,
|
||||
"checks": [
|
||||
{
|
||||
"command": "node --test refactor/scripts/iframe-contract.test.mjs",
|
||||
"result": "pass",
|
||||
"log": "refactor/.cache/iframe01-contract-final.log"
|
||||
},
|
||||
{
|
||||
"command": "yarn ci:check",
|
||||
"result": "pass",
|
||||
"tests": 1437,
|
||||
"unit": 1263,
|
||||
"engineering": 14,
|
||||
"baseline": 160,
|
||||
"repeatedContracts": 44,
|
||||
"strictProductionTypeScriptFiles": 324,
|
||||
"log": "refactor/.cache/iframe01-ci.log"
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"Registry 404 is time scoped; tool-name publication history is not inferred.",
|
||||
"No lifecycle fixes, production migration, origin policy change, actual iframe browser or installed-type acceptance in this contract task.",
|
||||
"Auxiliary helper is a distinct historical protocol, not an alias for the main class."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# iframe 工具的发布与工作区契约
|
||||
|
||||
来源:[冻结清单](iframe-release.json)。查询npm时artplayer-tool-iframe返回404;这只说明
|
||||
该次查询不可获得包,不能推导历史上从未发布。实际旧包artplayer-plugin-iframe只有1.0.0,
|
||||
发布于2022-11-05,gitHead关联核心4.5.9;仓库改名提交为1c75731ca。
|
||||
当前工具版本1.1.0和发布包不是同一版本/包名,不得编造tool@1.1.0归档或拿重构产物代替旧包。
|
||||
|
||||
## 入口与文件
|
||||
|
||||
| 范围 | 已核实的真实形状 |
|
||||
| --- | --- |
|
||||
| npm plugin@1.0.0 | 8文件,含README、package.json、src、声明、4个JS产物;无module/exports |
|
||||
| 主main/legacy | CommonJS是含default的namespace,浏览器window.ArtplayerPluginIframe是类 |
|
||||
| 额外helper main/legacy | namespace.default及window.ArtplayerHelperIframe;有不同协议,不是主类别名 |
|
||||
| 旧声明 | export = / export as namespace ArtplayerPluginIframe;与运行时require返回namespace存在既有偏差 |
|
||||
| 冻结工作区tool@1.1.0 | main/legacy直接导出类,浏览器ArtplayerToolIframe;mjs只有default;root/legacy条件入口 |
|
||||
| 工作区声明 | default类ArtplayerToolIframe,旧readonly字段和resove拼写保留;静态onMessage仍声明void |
|
||||
|
||||
初始工作区11个输入由Git固定:源、类型、README、manifest、npmignore、三产物、父demo、
|
||||
子iframe.html和编辑器声明。旧归档SHA512/SHA256与每个成员SHA256均核验;auxiliary helper
|
||||
也在验证中,不能漏掉它后再说旧包只有一个公共类。旧包导入名与新工具名的分发迁移、额外
|
||||
helper入口是否提供独立兼容门面,由IFRAME-04/06明确处理,不能把旧helper强行映射成新类。
|
||||
|
||||
## 主类与协议
|
||||
|
||||
构造参数仍为必需的{iframe: HTMLIFrameElement, url: string}。校验属于当前realm的iframe;
|
||||
先注册message监听,再设置src。自身可枚举字段顺序为url、$iframe、promises、injected、
|
||||
destroyed、messageCallback、绑定的onMessage。旧发布包非法参数抛Error,工作区已改为TypeError,
|
||||
这是改造前已有差异;不能把两者报告为完全相同。
|
||||
|
||||
- static iframe读取window.top !== window;inject/postMessage/onMessage只能用于子frame。
|
||||
- inject先发{type:'inject', data:undefined, id:0},再addEventListener;没有静态destroy。
|
||||
- static postMessage默认id=0,向window.parent发送,targetOrigin='*'。
|
||||
- 实例postMessage返回Promise;未inject时每200ms轮询,发出时使用Date.now()覆盖传入id。
|
||||
- promises[id]的公开字段是resove/reject,不能直接“纠正”resove拼写。
|
||||
- 收到inject设置injected;同id的error拒绝Error(data),其他type也会resolve(data),然后删除条目。
|
||||
- message回调收到{type,data}而不是id,this是实例;message与destroy返回undefined。
|
||||
- commit要求function,只截取toString中大括号内的body,不传闭包/参数。子端用new Function执行。
|
||||
匹配resolve(...)时包入Promise(resolve),否则同步执行并返回response;出错发error后重新抛出。
|
||||
- static onMessage实际返回Promise,而旧/当前声明为void;commit类型是Promise<ReturnType<T>>,
|
||||
保留历史推导,不在本任务随意修正嵌套Promise或callback resolver类型。
|
||||
- destroy设置destroyed并移除实例监听;未清空promises,也未取消已安排的轮询,具体失败测试由02接续。
|
||||
|
||||
## 额外helper是另一代协议
|
||||
|
||||
helper使用实例isInject/isDestroy,没有iframe getter或主类的构造/函数参数校验。静态inject设置
|
||||
isInject/isDestroy,静态destroy会发送destroy包并移除静态监听;收到destroy也会进入相应流程。
|
||||
其静态onMessage出错发送error但不重新抛出;实例销毁后等待inject的Promise可一直悬挂。
|
||||
它没有对应独立d.ts;主类声明不能用来证明其类型兼容。
|
||||
|
||||
## README、demo与待处理风险
|
||||
|
||||
README仅简介/demo/许可证。父demo iframe.js使用新类名、url:'/iframe.html',commit创建
|
||||
Artplayer,并由子端static postMessage通知fullscreenWeb;子页面加载两份uncompiled脚本后inject。
|
||||
当前demo注释提示npm安装tool名,但该名本次registry查询404,分发阶段须处理,不能在此宣称
|
||||
已完成真实跨源浏览器或npm安装验收。
|
||||
|
||||
源码显示Date.now ID可能同毫秒冲突;destroy留下pending请求/轮询;重复inject先发握手再注册。
|
||||
当前父/子onMessage均未校验event.source/origin,子端接收可执行函数body,发包目标为'*'。
|
||||
这既是跨窗口信任边界,也是既有协议,须独立设计、取证并记录兼容性决策;不能借TS迁移默默
|
||||
删除commit、改成另一套RPC、或假称已验证来源隔离。上述生命周期/信任风险均保持open。
|
||||
|
||||
运行`node --test refactor/scripts/iframe-contract.test.mjs`:19项覆盖实际主包/legacy、额外helper、
|
||||
工作区main/legacy/ESM、公开字段、消息包、callback receiver、commit body和resolver返回。
|
||||
它是可复现的契约基线;真正浏览器、异常时序、销毁/切源及安装类型矩阵由后续任务完成。
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"packageName": "artplayer-tool-iframe",
|
||||
"registryObservation": {
|
||||
"url": "https://registry.npmjs.org/artplayer-tool-iframe",
|
||||
"status": 404,
|
||||
"observedAt": "2026-09-12T14:45:37.698Z",
|
||||
"body": {
|
||||
"error": "Not found"
|
||||
},
|
||||
"meaning": "Registry returned 404 at observation; this is not proof the name was never published."
|
||||
},
|
||||
"release": {
|
||||
"name": "artplayer-plugin-iframe",
|
||||
"version": "1.0.0",
|
||||
"publishedAt": "2022-11-05T01:42:14.095Z",
|
||||
"tarball": "https://registry.npmjs.org/artplayer-plugin-iframe/-/artplayer-plugin-iframe-1.0.0.tgz",
|
||||
"integrity": "sha512-9eh5NVjZ8/Vxl0ojZxnMY6IlYxCVVfyGH2Rzl7uYDJnGc8NQ4y9vMN/gVh1h8R2m5qaYm7/XDo/45O9a7hDiKg==",
|
||||
"sha256": "16dc92aa84d93bd99bc0a34815dc49f1609fafc1ff255eb353ea893e70b5f91f",
|
||||
"gitHead": "824baca8b408d9f3e0eb2f3d7e0eb2bdee2ff3a8",
|
||||
"manifest": {
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"name": "artplayer-plugin-iframe",
|
||||
"types": "types/artplayer-plugin-iframe.d.ts",
|
||||
"description": "Iframe plugin for ArtPlayer",
|
||||
"main": "dist/artplayer-plugin-iframe.js",
|
||||
"legacy": "dist/artplayer-plugin-iframe.legacy.js",
|
||||
"author": "Harvey Zack <laozhaochaguan@gmail.com>",
|
||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||
"homepage": "https://artplayer.org",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/zhw2590582/ArtPlayer.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/zhw2590582/ArtPlayer/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"html5",
|
||||
"video",
|
||||
"player"
|
||||
]
|
||||
},
|
||||
"files": {
|
||||
"package/README.md": "52c0a1199b6529d60ce8f4414984f9bd1bc3a8daa5bb6cfec8f0aae0c8338e46",
|
||||
"package/dist/artplayer-helper-iframe.js": "47730ed608f0637594549b9762c01191686a4a55b3bd467132304a016d227f4a",
|
||||
"package/dist/artplayer-helper-iframe.legacy.js": "f4c3b11806cad6d984f03a2d9de63deba78cbe538ffe4044a106b0dddc5d4197",
|
||||
"package/dist/artplayer-plugin-iframe.js": "df469c3e4582b127f35798494d0845f1fc4ca2d19bbaf01b918492e035479745",
|
||||
"package/dist/artplayer-plugin-iframe.legacy.js": "15299cb5c848cc61614f7573a802443d69b7c6d0a938a1867835c7cbb4884edd",
|
||||
"package/package.json": "563a6c8407753b92b86319a5e6d9d567525146c32cb010b45b0d88ff481859f6",
|
||||
"package/src/index.js": "2e0b9efeedef0d6c4f2d2b526db35f634cd02af044e4e28dd1a0b26a5b9a02bf",
|
||||
"package/types/artplayer-plugin-iframe.d.ts": "d55d99a2488d60892a0ab82882dc96a0f373dab0c3d2a3cee978bc35aa7c2d0c"
|
||||
},
|
||||
"historicalCore": {
|
||||
"commit": "824baca8b408d9f3e0eb2f3d7e0eb2bdee2ff3a8",
|
||||
"file": "packages/artplayer/package.json",
|
||||
"version": "4.5.9",
|
||||
"sha256LF": "7fe8f4a728c4895d2323a4214f024239f03b985f530529a5d257fa010252c195"
|
||||
}
|
||||
},
|
||||
"sourceCommit": "40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f",
|
||||
"source": {
|
||||
"packages/artplayer-tool-iframe/src/index.js": "5fd5452640ff57fa35cd0f3378505ffc1318e9c114d9a2a6e2f1adbeaec6fd28",
|
||||
"packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.ts": "4f02aa22a6b373393ead533ae3421ad5acf0de7116057f351021c72d3448bd1e",
|
||||
"packages/artplayer-tool-iframe/README.md": "6910a44b767ff80c1d1edc3c90ca7ca77a9b45307fb61d4abd9d52526c7614b3",
|
||||
"packages/artplayer-tool-iframe/package.json": "b28f5156f02a250539f0acaf302ec87fdd506f63b0e73dd308326ec78c691d03",
|
||||
"packages/artplayer-tool-iframe/.npmignore": "46063058e98be55dba5cf5f33d379e73eca048f4126e7af75f8011bab7f09145",
|
||||
"packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.js": "2ec257c8055a9448d12af3fecf1fae8ba58c06f47e85072d02c386cd258b0c86",
|
||||
"packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.legacy.js": "c0755d79f316cda71f045da86749648f95faf47c09180d525fb04bb753576d5f",
|
||||
"packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.mjs": "cbcdb0516dd5f17ca2572a6b3e363ea63b0cfd9733f3e47689c02088f23f29b9",
|
||||
"docs/assets/example/iframe.js": "00a8441d2a9873f7be342a98b58acab1cf6aca2359414f126ffb63f6ee20c218",
|
||||
"docs/iframe.html": "bc262262c66ad32459b7c91ae22a97217414dd5b18cbf68fe105c2269456f879",
|
||||
"docs/assets/ts/artplayer-tool-iframe.d.ts": "fe261fd8fb6a8f8838290a3eef3d624cf7587cec30edaa2644bcf0fb7b44bb5d"
|
||||
},
|
||||
"renameCommit": "1c75731ca9cf2edb7a2f1f26b1bac1de9847b9c3"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# PKG-IFRAME-01 冻结发布名、改名与消息契约
|
||||
|
||||
实际npm包为artplayer-plugin-iframe@1.0.0,含8个成员和两个不同的类协议;新工具名的
|
||||
registry查询返回404,已保存查询时间/状态,不将工作区1.1.0冒充已发布版本。
|
||||
冻结旧包完整性、8成员哈希、发布gitHead关联核心4.5.9及初始工作区11输入。实现地图与
|
||||
公开契约见[契约文档](../baselines/iframe-contract.md)和[来源清单](../baselines/iframe-release.json)。
|
||||
|
||||
19个独立Node契约检查已通过:CJS namespace/default与直接类代际、script全局、ESM、
|
||||
字段/方法、监听先于导航、resove拼写、numeric id、'*'目标、message回调receiver、commit
|
||||
body、静态异步resolver返回及辅助helper的独立destroy协议。第一轮6个失败是误将helper
|
||||
当主类的测试假设;保留日志,按实际artifact建立独立断言,没有把不同协议合成假别名。
|
||||
|
||||
本任务不改生产源码、声明、包版本或demo。记录未验证生命周期和source/origin信任边界,
|
||||
IFRAME-02建立异常/销毁/并发回归,03整理资源,04处理TS及精确/兼容声明,05真实浏览器,
|
||||
06分发和demo。旧helper deep入口与类名迁移的结论必须在04/06明确,不能因main测试通过而遗漏。
|
||||
完整CI1437项与44重复契约、最终来源核验通过,任务标done并立即专用本地提交。
|
||||
|
||||
完整结果见[验证记录](../baselines/iframe-contract-validation.json)。324个现有生产TS通过检查;
|
||||
本包仍为JS,不能把全局TS数量当成本包迁移完成。三项iframe风险保持open,下一项IFRAME-02。
|
||||
+5
-4
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 108 / doing 9 / blocked 0 / done 105 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 107 / doing 9 / blocked 0 / done 106 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -344,12 +344,12 @@
|
||||
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-IFRAME-01 | artplayer-tool-iframe<br>核对包契约与历史用法 | BASE-05 | constructor/commit/message/inject、postMessage 协议与历史公开拼写 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | todo |
|
||||
| PKG-IFRAME-01 | artplayer-tool-iframe<br>核对包契约与历史用法 | BASE-05 | constructor/commit/message/inject、postMessage 协议与历史公开拼写 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
|
||||
| PKG-IFRAME-02 | artplayer-tool-iframe<br>建立特有行为与错误测试 | PKG-IFRAME-01, ENG-03, ENG-05 | 跨窗口消息、ID 匹配、请求失败、重复 inject、销毁中请求 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
|
||||
| PKG-IFRAME-03 | artplayer-tool-iframe<br>整理内部职责与资源 | PKG-IFRAME-02, CORE-02, BASE-07 | 请求注册/响应匹配/监听清理分离;origin/source 安全边界独立决策 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
|
||||
| PKG-IFRAME-04 | artplayer-tool-iframe<br>迁移自有源码和公开类型 | PKG-IFRAME-03, ENG-04, ENG-06, CORE-07 | 消息联合类型、回调/Promise 推导、旧公开字段兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-IFRAME-04 | artplayer-tool-iframe<br>迁移自有源码和公开类型 | PKG-IFRAME-03, ENG-04, ENG-06, CORE-07 | 消息联合类型、回调/Promise 推导、旧公开字段兼容;旧npm export=与实际namespace、额外helper协议分别核验 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-IFRAME-05 | artplayer-tool-iframe<br>验证新旧核心和组合 | PKG-IFRAME-04, CORE-22 | 真实同源/跨源 iframe、既有 commit 协议;安全变化有独立结论 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-IFRAME-06 | artplayer-tool-iframe<br>验证分发并同步文档 | PKG-IFRAME-05, ENG-07 | iframe.js、示例集成和原 script/class 导出验证 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
|
||||
| PKG-IFRAME-06 | artplayer-tool-iframe<br>验证分发并同步文档 | PKG-IFRAME-05, ENG-07 | iframe.js、示例集成和原 script/class 导出验证;旧包名/额外helper深入口与新工具名的迁移结论 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
|
||||
|
||||
## 5 包迁移:artplayer-tool-thumbnail
|
||||
|
||||
@@ -530,4 +530,5 @@
|
||||
- PKG-MB-07: [记录](baselines/mb-hls-source.json) [记录](changes/2026-09-12-PKG-MB-07-hls.md) [记录](baselines/mb-hls-validation.json)
|
||||
- PKG-MB-08: [记录](baselines/mb-entry-source.json) [记录](changes/2026-09-12-PKG-MB-08-entry-checkpoint.md) [记录](baselines/mb-entry-checkpoint.json) [记录](baselines/mb-capability-source.json) [记录](changes/2026-09-12-PKG-MB-08-capability.md) [记录](baselines/mb-capability-validation.json)
|
||||
- PKG-MB-09: [记录](changes/2026-09-12-PKG-MB-09-native-pip-checkpoint.md) [记录](baselines/mb-native-pip-checkpoint.json) [记录](changes/2026-09-12-PKG-MB-09-sustained-playback-checkpoint.md) [记录](baselines/mb-sustained-validation.json) [记录](baselines/mb-sustained-media.json)
|
||||
- PKG-IFRAME-01: [记录](changes/2026-09-12-PKG-IFRAME-01-contract.md) [记录](baselines/iframe-release.json) [记录](baselines/iframe-contract.md) [记录](baselines/iframe-contract-validation.json)
|
||||
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md)
|
||||
@@ -1,5 +1,15 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-IFRAME-01 完成:发布名、辅助类与消息契约
|
||||
|
||||
实际旧包plugin-iframe@1.0.0的8个成员和工作区11个输入已冻结;新tool名本次npm查询404,
|
||||
不将工作区1.1.0冒充发布版本。额外helper有不同destroy协议,主类两代CJS/default、
|
||||
script/ESM、resove、callback receiver和commit序列化由19项实际产物检查保护。
|
||||
完整CI1437与44重复契约通过,324现有生产TS;本包尚未迁移TS。
|
||||
见[契约](baselines/iframe-contract.md)、[变更](changes/2026-09-12-PKG-IFRAME-01-contract.md)
|
||||
与[验证](baselines/iframe-contract-validation.json)。生命周期、消息信任与分发风险保持open。
|
||||
222项:106 done、9 doing、107 todo;立即专用本地提交,下一项IFRAME-02建立并发/销毁/跨窗口回归。
|
||||
|
||||
## PKG-DPIP-05 原生组合检查点与核心文档归属修复(doing)
|
||||
|
||||
真实popup键盘、两轮开关与节点还原覆盖原生video/Canvas/MediaBunny及新旧核心。
|
||||
|
||||
@@ -174,3 +174,6 @@
|
||||
| MB-CAP-01 | open / 已复现 | Current Windows WebKit lacks WebCodecs and Web Audio constructors needed by historical MediaBunny proxies | PKG-MB-02, PKG-MB-09 |
|
||||
| MB-READY-01 | resolved / 已复现 | MediaBunny historical readiness can repeat for trackless input and occur after capability errors | PKG-MB-04, PKG-MB-09 |
|
||||
| DPIP-DISPLAY-01 | resolved / 已复现 | Web fullscreen body placement reclaims a player adopted into another document | CORE-22, PKG-DPIP-05 |
|
||||
| IFRAME-LIFE-01 | open / 源码/产物事实 | Iframe timestamp IDs and unowned pending requests/timers lack terminal cleanup | PKG-IFRAME-02, PKG-IFRAME-03 |
|
||||
| IFRAME-TRUST-01 | open / 源码/产物事实 | Iframe executable commit protocol accepts messages without source/origin validation | PKG-IFRAME-03, PKG-IFRAME-05 |
|
||||
| IFRAME-DIST-01 | open / 已复现 | Iframe tool name is unavailable at registry observation and old archive includes distinct helper exports | PKG-IFRAME-04, PKG-IFRAME-06 |
|
||||
@@ -3765,6 +3765,57 @@
|
||||
"refactor/changes/2026-09-12-PKG-DPIP-05-native-checkpoint.md"
|
||||
],
|
||||
"resolutionRationale": "Original candidate native video reproduction moves the player into the wrong document and fails. Current ownerDocument body placement passes source 84, built main 219 and built legacy 84 related cases, including real native PiP video/Canvas/MediaBunny and iframe placement/focus. Reentry/failure/teardown and keyboard regressions retain their assertions. Full CI and import/SSR checks pass. Native Fullscreen API, background and device gates are separate and not waived."
|
||||
},
|
||||
{
|
||||
"id": "IFRAME-LIFE-01",
|
||||
"title": "Iframe timestamp IDs and unowned pending requests/timers lack terminal cleanup",
|
||||
"owners": [
|
||||
"PKG-IFRAME-02",
|
||||
"PKG-IFRAME-03"
|
||||
],
|
||||
"status": "open",
|
||||
"confirmation": "source-observed",
|
||||
"compatibleResolution": "Reproduce same-tick request collision, pending injection, response errors and destroy behavior before changing internal ownership; preserve public promises[id].resove.",
|
||||
"closureCriteria": "Old behavior and candidate fixes have deterministic race/resource tests plus actual iframe integration.",
|
||||
"evidence": [
|
||||
"refactor/baselines/iframe-contract.md",
|
||||
"refactor/baselines/iframe-release.json",
|
||||
"refactor/changes/2026-09-12-PKG-IFRAME-01-contract.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "IFRAME-TRUST-01",
|
||||
"title": "Iframe executable commit protocol accepts messages without source/origin validation",
|
||||
"owners": [
|
||||
"PKG-IFRAME-03",
|
||||
"PKG-IFRAME-05"
|
||||
],
|
||||
"status": "open",
|
||||
"confirmation": "source-observed",
|
||||
"compatibleResolution": "Treat message origin/source and executable commit as an explicit compatibility and trust-boundary decision; do not silently replace the protocol during TS conversion.",
|
||||
"closureCriteria": "Controlled and real same/cross-origin evidence plus an explicit documented compatibility decision and supported trust model.",
|
||||
"evidence": [
|
||||
"refactor/baselines/iframe-contract.md",
|
||||
"refactor/baselines/iframe-release.json",
|
||||
"refactor/changes/2026-09-12-PKG-IFRAME-01-contract.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "IFRAME-DIST-01",
|
||||
"title": "Iframe tool name is unavailable at registry observation and old archive includes distinct helper exports",
|
||||
"owners": [
|
||||
"PKG-IFRAME-04",
|
||||
"PKG-IFRAME-06"
|
||||
],
|
||||
"status": "open",
|
||||
"confirmation": "reproduced",
|
||||
"compatibleResolution": "Keep actual old plugin/helper exports distinct from workspace tool and document package-name/type/export migration; do not fabricate a tool release or use a renamed implementation as an old archive.",
|
||||
"closureCriteria": "Actual installed entry/declaration tests and documented old-name/helper/new-tool distribution behavior; registry observation remains time scoped.",
|
||||
"evidence": [
|
||||
"refactor/baselines/iframe-contract.md",
|
||||
"refactor/baselines/iframe-release.json",
|
||||
"refactor/changes/2026-09-12-PKG-IFRAME-01-contract.md"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
|
||||
|
||||
export async function verifyIframeContract() {
|
||||
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/iframe-release.json')))
|
||||
assert.equal(baseline.registryObservation.status, 404)
|
||||
assert.equal(baseline.release.name, 'artplayer-plugin-iframe')
|
||||
const archive = await ensureArchive(baseline.release)
|
||||
assert.deepEqual(archiveFiles(archive), Object.keys(baseline.release.files).sort())
|
||||
for (const [file, expected] of Object.entries(baseline.release.files))
|
||||
assert.equal(hash(readMember(archive, file)), expected, file)
|
||||
assert.deepEqual(JSON.parse(readMember(archive, 'package/package.json')), baseline.release.manifest)
|
||||
const core = baseline.release.historicalCore
|
||||
const coreSource = execFileSync('git', ['show', `${core.commit}:${core.file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
|
||||
assert.equal(hash(coreSource), core.sha256LF)
|
||||
assert.equal(JSON.parse(coreSource).version, core.version)
|
||||
const sources = new Map()
|
||||
for (const [file, expected] of Object.entries(baseline.source)) {
|
||||
const source = execFileSync('git', ['show', `${baseline.sourceCommit}:${file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
|
||||
assert.equal(hash(source), expected, file)
|
||||
sources.set(file, source)
|
||||
}
|
||||
return { baseline, archive, sources }
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const { baseline } = await verifyIframeContract()
|
||||
console.log(`Iframe contract: historical ${baseline.release.name}@${baseline.release.version}, ${Object.keys(baseline.release.files).length} archive members, ${Object.keys(baseline.source).length} frozen workspace inputs; tool-name registry 404 remains an observation`)
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Frozen package contract runner.
|
||||
import test from 'node:test'
|
||||
import vm from 'node:vm'
|
||||
import { iframeEnvironment, iframeHistorical } from '../../test/helpers/iframe.js'
|
||||
import { verifyIframeContract } from './iframe-contract.mjs'
|
||||
import { readMember } from './releases.mjs'
|
||||
|
||||
test('Iframe actual old-name archive and renamed workspace remain separate provenance', async () => {
|
||||
const { baseline, archive, sources } = await verifyIframeContract()
|
||||
assert.equal(Object.keys(baseline.release.files).length, 8)
|
||||
assert.equal(Object.keys(baseline.source).length, 11)
|
||||
assert.equal(baseline.release.manifest.module, undefined)
|
||||
assert.equal(baseline.registryObservation.body.error, 'Not found')
|
||||
const old = readMember(archive, 'package/types/artplayer-plugin-iframe.d.ts').toString()
|
||||
const current = sources.get('packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.ts')
|
||||
assert.match(old, /export = ArtplayerPluginIframe/)
|
||||
assert.match(old, /export as namespace ArtplayerPluginIframe/)
|
||||
assert.match(current, /export default ArtplayerToolIframe/)
|
||||
for (const source of [old, current]) {
|
||||
assert.match(source, /resove:/)
|
||||
assert.match(source, /Promise<ReturnType<T>>/)
|
||||
assert.match(source, /static onMessage\(event: MessageEvent & \{ data: Message \}\): void/)
|
||||
}
|
||||
const esm = await import(`data:text/javascript,${encodeURIComponent(sources.get('packages/artplayer-tool-iframe/dist/artplayer-tool-iframe.mjs'))}`)
|
||||
assert.equal(typeof esm.default, 'function')
|
||||
assert.deepEqual(Object.keys(esm), ['default'])
|
||||
})
|
||||
|
||||
const implementations = await iframeHistorical()
|
||||
for (const implementation of implementations.filter(item => item.global !== 'ArtplayerHelperIframe')) {
|
||||
test(`Iframe ${implementation.name}: class, CJS/script exports and constructor fields`, () => {
|
||||
const env = iframeEnvironment(implementation)
|
||||
assert.equal(typeof env.exported, implementation.namespace ? 'object' : 'function')
|
||||
const browser = {}
|
||||
browser.window = browser
|
||||
browser.self = browser
|
||||
vm.runInNewContext(implementation.code, browser)
|
||||
assert.equal(typeof browser[implementation.global], 'function')
|
||||
assert.deepEqual(Object.getOwnPropertyNames(env.Factory.prototype), ['constructor', 'onMessage', 'postMessage', 'commit', 'message', 'destroy'])
|
||||
assert.throws(() => new env.Factory({ iframe: {}, url: '/' }), error => error.name === (implementation.namespace ? 'Error' : 'TypeError') && /needs to be a HTMLIFrameElement/.test(error.message))
|
||||
assert.throws(() => new env.Factory({ iframe: new env.Frame(), url: null }), /needs to be a string/)
|
||||
const frame = new env.Frame()
|
||||
const instance = new env.Factory({ iframe: frame, url: '/iframe.html' })
|
||||
assert.deepEqual(Object.keys(instance), ['url', '$iframe', 'promises', 'injected', 'destroyed', 'messageCallback', 'onMessage'])
|
||||
assert.deepEqual(env.order, ['listen:message', 'src'])
|
||||
assert.equal(instance.$iframe, frame)
|
||||
assert.equal(instance.url, frame.url)
|
||||
assert.equal(instance.injected, false)
|
||||
assert.equal(instance.destroyed, false)
|
||||
assert.equal(instance.messageCallback(), null)
|
||||
assert.throws(() => instance.commit('return 1'), /needs to be a function/)
|
||||
assert.throws(() => instance.message(null), /needs to be a function/)
|
||||
assert.equal(instance.destroy(), undefined)
|
||||
assert.equal(env.handlers.get('message').size, 0)
|
||||
})
|
||||
|
||||
test(`Iframe ${implementation.name}: numeric envelopes, resove spelling and callback receiver`, async () => {
|
||||
const env = iframeEnvironment(implementation)
|
||||
const instance = new env.Factory({ iframe: new env.Frame(), url: '/iframe.html' })
|
||||
const observed = []
|
||||
assert.equal(instance.message(function (message) {
|
||||
observed.push({ receiver: this === instance, ...message })
|
||||
}), undefined)
|
||||
env.dispatch({ type: 'inject' })
|
||||
assert.equal(instance.injected, true)
|
||||
const pending = instance.postMessage({ type: 'query', data: 3, id: 999 })
|
||||
assert.equal(typeof pending.then, 'function')
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(env.sent)), [{ packet: { type: 'query', data: 3, id: 1234 }, origin: '*' }])
|
||||
assert.deepEqual(Object.keys(instance.promises[1234]), ['resove', 'reject'])
|
||||
env.dispatch({ type: 'response', data: 4, id: 1234 })
|
||||
assert.equal(await pending, 4)
|
||||
assert.equal(Object.keys(instance.promises).length, 0)
|
||||
assert.deepEqual(observed, [{ receiver: true, type: 'inject', data: undefined }, { receiver: true, type: 'response', data: 4 }])
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
test(`Iframe ${implementation.name}: child injection and synchronous commit response contract`, async () => {
|
||||
const parent = iframeEnvironment(implementation)
|
||||
assert.equal(parent.Factory.iframe, false)
|
||||
assert.throws(() => parent.Factory.inject(), /can only be used in iframe/)
|
||||
const env = iframeEnvironment(implementation, true)
|
||||
assert.equal(env.Factory.iframe, true)
|
||||
assert.equal(env.Factory.inject(), undefined)
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(env.sent)), [{ packet: { type: 'inject', id: 0 }, origin: '*' }])
|
||||
await env.Factory.onMessage({ data: { type: 'commit', data: 'return 2 + 3', id: 17 } })
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(env.sent.at(-1))), { packet: { type: 'response', data: 5, id: 17 }, origin: '*' })
|
||||
})
|
||||
|
||||
test(`Iframe ${implementation.name}: commit serializes a body and resolver replies remain asynchronous`, async () => {
|
||||
const env = iframeEnvironment(implementation)
|
||||
const instance = new env.Factory({ iframe: new env.Frame(), url: '/' })
|
||||
env.dispatch({ type: 'inject' })
|
||||
const pending = instance.commit(() => {
|
||||
return 7
|
||||
})
|
||||
const packet = env.sent.at(-1).packet
|
||||
assert.equal(packet.type, 'commit')
|
||||
assert.equal(packet.data.trim(), 'return 7')
|
||||
assert.equal(packet.id, 1234)
|
||||
env.dispatch({ type: 'response', data: 7, id: packet.id })
|
||||
assert.equal(await pending, 7)
|
||||
const child = iframeEnvironment(implementation, true)
|
||||
const completion = child.Factory.onMessage({ data: { type: 'commit', data: 'resolve(8)', id: 18 } })
|
||||
assert.equal(typeof completion.then, 'function')
|
||||
await completion
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(child.sent.at(-1))), { packet: { type: 'response', data: 8, id: 18 }, origin: '*' })
|
||||
instance.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
for (const implementation of implementations.filter(item => item.global === 'ArtplayerHelperIframe')) {
|
||||
test(`Iframe ${implementation.name}: extra helper artifact has a distinct static destroy protocol`, async () => {
|
||||
const env = iframeEnvironment(implementation)
|
||||
assert.equal(typeof env.exported, 'object')
|
||||
assert.equal(env.Factory.iframe, undefined)
|
||||
assert.equal(typeof env.Factory.destroy, 'function')
|
||||
const browser = {}
|
||||
browser.window = browser
|
||||
browser.self = browser
|
||||
vm.runInNewContext(implementation.code, browser)
|
||||
assert.equal(typeof browser.ArtplayerHelperIframe, 'function')
|
||||
const instance = new env.Factory({ iframe: new env.Frame(), url: '/' })
|
||||
assert.equal(instance.isInject, false)
|
||||
assert.equal(instance.isDestroy, false)
|
||||
assert.equal(instance.injected, undefined)
|
||||
env.Factory.inject()
|
||||
assert.equal(env.Factory.isInject, true)
|
||||
await env.Factory.onMessage({ data: { type: 'destroy' } })
|
||||
assert.equal(env.Factory.isDestroy, true)
|
||||
assert.equal(env.Factory.isInject, false)
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(env.sent)), [{ packet: { type: 'inject', id: 0 }, origin: '*' }, { packet: { type: 'destroy', id: 0 }, origin: '*' }])
|
||||
instance.destroy()
|
||||
assert.equal(instance.isDestroy, true)
|
||||
})
|
||||
}
|
||||
+9
-4
@@ -3612,11 +3612,16 @@
|
||||
"dependsOn": [
|
||||
"BASE-05"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "constructor/commit/message/inject、postMessage 协议与历史公开拼写",
|
||||
"acceptance": "源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-12-PKG-IFRAME-01-contract.md",
|
||||
"baselines/iframe-release.json",
|
||||
"baselines/iframe-contract.md",
|
||||
"baselines/iframe-contract-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-IFRAME-02",
|
||||
@@ -3669,7 +3674,7 @@
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "H",
|
||||
"deliverable": "消息联合类型、回调/Promise 推导、旧公开字段兼容",
|
||||
"deliverable": "消息联合类型、回调/Promise 推导、旧公开字段兼容;旧npm export=与实际namespace、额外helper协议分别核验",
|
||||
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容",
|
||||
"evidence": []
|
||||
},
|
||||
@@ -3703,7 +3708,7 @@
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "H",
|
||||
"deliverable": "iframe.js、示例集成和原 script/class 导出验证",
|
||||
"deliverable": "iframe.js、示例集成和原 script/class 导出验证;旧包名/额外helper深入口与新工具名的迁移结论",
|
||||
"acceptance": "tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录",
|
||||
"evidence": []
|
||||
},
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import vm from 'node:vm'
|
||||
import { verifyIframeContract } from '../../refactor/scripts/iframe-contract.mjs'
|
||||
import { readMember } from '../../refactor/scripts/releases.mjs'
|
||||
|
||||
export async function iframeHistorical() {
|
||||
const { baseline, archive, sources } = await verifyIframeContract()
|
||||
return [
|
||||
...Object.keys(baseline.release.files).filter(file => file.startsWith('package/dist/')).map(file => ({ name: `published-${file.split('/').at(-1)}`, code: readMember(archive, file).toString(), global: file.includes('helper') ? 'ArtplayerHelperIframe' : 'ArtplayerPluginIframe', namespace: true })),
|
||||
...['.js', '.legacy.js'].map(suffix => ({ name: `workspace${suffix}`, code: sources.get(`packages/artplayer-tool-iframe/dist/artplayer-tool-iframe${suffix}`), global: 'ArtplayerToolIframe', namespace: false })),
|
||||
]
|
||||
}
|
||||
|
||||
export function iframeEnvironment(implementation, child = false) {
|
||||
const handlers = new Map()
|
||||
const timers = new Map()
|
||||
const sent = []
|
||||
const order = []
|
||||
let now = 1234
|
||||
let nextTimer = 0
|
||||
class Frame {
|
||||
contentWindow = { postMessage: (packet, origin) => sent.push({ packet, origin }) }
|
||||
get src() { return this.url }
|
||||
set src(value) {
|
||||
this.url = value
|
||||
order.push('src')
|
||||
}
|
||||
}
|
||||
const module = { exports: {} }
|
||||
const box = {
|
||||
module,
|
||||
exports: module.exports,
|
||||
HTMLIFrameElement: Frame,
|
||||
Date: class extends Date { static now() { return now } },
|
||||
setTimeout(callback, delay) {
|
||||
const id = ++nextTimer
|
||||
timers.set(id, { callback, delay })
|
||||
return id
|
||||
},
|
||||
clearTimeout(id) { timers.delete(id) },
|
||||
addEventListener(name, callback) {
|
||||
const set = handlers.get(name) || new Set()
|
||||
set.add(callback)
|
||||
handlers.set(name, set)
|
||||
order.push(`listen:${name}`)
|
||||
},
|
||||
removeEventListener(name, callback) { handlers.get(name)?.delete(callback) },
|
||||
}
|
||||
box.window = box
|
||||
box.self = box
|
||||
box.top = child ? {} : box
|
||||
box.parent = { postMessage: (packet, origin) => sent.push({ packet, origin }) }
|
||||
vm.runInNewContext(implementation.code, box)
|
||||
const exported = module.exports
|
||||
const Factory = exported.default || exported
|
||||
return { box, Factory, exported, Frame, handlers, timers, sent, order, now(value) {
|
||||
now = value
|
||||
}, dispatch(data, source, origin = 'https://unrelated.invalid') {
|
||||
for (const callback of handlers.get('message') || []) callback({ data, source, origin })
|
||||
} }
|
||||
}
|
||||
Reference in new issue
Block a user