mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
test(ads): [PKG-ADS-02] cover legacy timing and real media contracts
This commit is contained in:
1 parent
c6b82c3392
commit
069ad4aae1
15 files changed
+1519
-17
No files matched your search
+2
-1
@@ -34,6 +34,7 @@
|
||||
"build:i18n": "node ./scripts/build-i18n.js",
|
||||
"build:test": "node ./scripts/build-test.js",
|
||||
"test:playback": "node --test test/playback.test.js",
|
||||
"test:ads": "node --test test/ads.test.js",
|
||||
"trans:docs": "node ./scripts/trans-docs.js",
|
||||
"create:plugin": "node ./scripts/plugin/create.js",
|
||||
"test:dash-control": "node --test test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js",
|
||||
@@ -50,7 +51,7 @@
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
|
||||
"test:unit": "node --test test/ads.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
|
||||
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Ads 回归测试与维护入口
|
||||
|
||||
Ads 的实际发布行为以 [1.0.6 归档](baselines/ads-release.json) 为基线;工作区 2.1.0
|
||||
未发布,其旧 bundle 单独从冻结 Git 提交加载。不要将当前声明里的 source/type 当成
|
||||
发布 JS 支持的输入;正常广告仍使用 html/video/url,图片通过 HTML img 提供。
|
||||
|
||||
## 可执行入口
|
||||
|
||||
使用仓库固定 Node/Yarn 工具链:
|
||||
|
||||
```sh
|
||||
yarn test:ads
|
||||
yarn test:unit
|
||||
yarn test:browser test/browser/ads.spec.js
|
||||
```
|
||||
|
||||
Node 测试已接入 test:unit,因此 ci:check 会执行。浏览器测试独立运行;共享服务器端口
|
||||
8084 和报告目录,每轮运行后先归档 report.json 与完整 results,再启动下一轮。
|
||||
没有新增依赖、锁文件或生产打包规则变更。
|
||||
|
||||
`ARTPLAYER_TEST_ADS` 可以指定额外正式 JS/legacy/mjs 产物,多个文件使用本机路径分隔符。
|
||||
`ARTPLAYER_ADS_ARTIFACT` 给浏览器指定单个 script 产物,默认按仓库 Vite 配置从源码
|
||||
内存构建。路径无效直接失败,不自动回退。实际发布 bundle 总是先校验归档及成员哈希。
|
||||
|
||||
## 边界与文件职责
|
||||
|
||||
- `test/helpers/ads.js`:来源加载、冻结 bundle 的 VM、可控时钟和最小 ArtPlayer 宿主。
|
||||
使用真实 option-validator,记录计时/播放/DOM 操作意图;节点不是 HTML 解析器、布局
|
||||
引擎或媒体解码器。多实例共用一个测试时钟,独立维护监听、节点和归一化选项。
|
||||
- `test/ads.test.js`:正常工厂/配置/优先级、倒计时、按钮阈值、公开方法返回、事件参数
|
||||
身份、独立实例、restart、静音、visibility 与错误顺序;历史缺陷只断言冻结实现。
|
||||
- `test/browser/ads.spec.js`:实际 npm 1.0.6/候选源码与核心 5.4.1/候选四组合,使用
|
||||
本地图片与 MP4,验证解码像素、时间推进、点击、主内容恢复、404 和真实切源。
|
||||
每例附核心来源、插件 SHA、状态、浏览器版本;失败保留截图和 trace。
|
||||
|
||||
播放拒绝用例精确注入 NotAllowedError,只拦截指定广告或广告后的主视频 play。
|
||||
仅对匹配的预期 unhandledrejection 记录并 preventDefault,其他页面异常仍使测试失败。
|
||||
这证明旧插件的 Promise 处理路径,不能当作真实设备自动播放策略验收。
|
||||
零阈值按钮测试初始化后暂停倒计时,以稳定观察首个 tick 前行为;正常视频测试不模拟时间。
|
||||
Windows WebKit 的 videoWidth 可能是布局宽度,解码验证使用本地样本的实际颜色像素。
|
||||
|
||||
## 已复现的历史缺陷与接续
|
||||
|
||||
Node 同时复现 npm 1.0.6 和冻结工作区实现的八类问题:重复 play 建立多条计时链;
|
||||
skip 后残留回调/重复完成;destroy 后仍请求主片播放;零阈值点击首秒无效;初始化前
|
||||
skip 请求播放后抛错;复用工厂使首实例事件参数指向第二实例;重复 metadata 重复启动;
|
||||
初始化前 play 的回调访问不存在的 DOM。
|
||||
|
||||
浏览器另外观察发布版广告 play 被拒后倒计时继续,以及主片恢复被拒后广告隐藏并
|
||||
发出 skip,两条路径均泄漏未处理拒绝。历史观察不是候选验收要求;03 的修复需要
|
||||
对应候选回归,保持正常事件顺序、同步结果与 play/pause 只管理倒计时的既有语义。
|
||||
|
||||
ADS-LIFE-01、ADS-MEDIA-01、ADS-UI-01 继续 open。全屏按钮/外部全屏状态、快速
|
||||
metadata、真实隐藏页媒体、销毁资源回收、旧核心 4.5.5、实际设备策略、隔离打包与
|
||||
完整 demo 验收由 03~06 接续。不能用本轮 5.4.1 组合扩大历史核心支持范围。
|
||||
|
||||
本步输入、原始失败和最终结果见 [执行记录](baselines/ads-validation.json);生产模块
|
||||
地图在 03 实际拆分时写入包内文档,本文件只解释测试维护边界。
|
||||
@@ -63,3 +63,10 @@ bundle-text 导入;私有打包器标识不属于稳定 API,不能把这段
|
||||
上述除实际 export 形状外,运行路径问题先记 source-observed,由 02 复现再在 03 修正。
|
||||
03 按配置/状态与计时/视图/内容恢复拆分;04 严格 TS 与公开类型;05 真实媒体/旧核心/
|
||||
多实例;06 demo、文档和完整分发。保持本地广告素材,不触发真实投放、跳转或计费服务。
|
||||
|
||||
## 02 执行补充
|
||||
|
||||
上述为 01 捕获时的状态。02 已以 58 项 Node 和 66 项三引擎浏览器测试固定正常行为,
|
||||
复现计时/工厂复用/过早调用/销毁、零阈值及两类播放拒绝问题;三个相关风险保持 open。
|
||||
实际结果、原始失败和仍待验证边界见 [Ads 验证说明](../ads-validation.md) 及
|
||||
[02 记录](../changes/2026-09-12-PKG-ADS-02-tests.md)。本步不修改生产接口。
|
||||
@@ -0,0 +1,527 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "PKG-ADS-02",
|
||||
"sourceCommit": "c6b82c33",
|
||||
"inputs": [
|
||||
{
|
||||
"file": "test/helpers/ads.js",
|
||||
"sha256LF": "2301a057612cd752863f1fea9b65860dbb65ba4b029303a784f8759fdadacf6c"
|
||||
},
|
||||
{
|
||||
"file": "test/ads.test.js",
|
||||
"sha256LF": "1aa325c9982aea2de61b68c9688a3640c628a1a47a672080655de729ccd37c64"
|
||||
},
|
||||
{
|
||||
"file": "test/browser/ads.spec.js",
|
||||
"sha256LF": "ab7541748ed89185429e6307bf5caf96221d91fbf29c067d62cd54f5b207f5bd"
|
||||
},
|
||||
{
|
||||
"file": "refactor/baselines/ads-release.json",
|
||||
"sha256LF": "e5b47f7d04fe043aaeb40390da4b4c19d80ff6f8889e8e0f5c806ffa44306b69"
|
||||
},
|
||||
{
|
||||
"file": "package.json",
|
||||
"sha256LF": "ef7248885ff7e6b7d798150ec22ace1c8dc3a4a10fed79fba653cdf09b4baebb"
|
||||
}
|
||||
],
|
||||
"productionChanged": false,
|
||||
"node": {
|
||||
"tests": 58,
|
||||
"passed": 58,
|
||||
"failed": 0,
|
||||
"log": {
|
||||
"file": "refactor/.cache/ads02-node.log",
|
||||
"sha256": "1f8ab521d7c9856239f3bdc55e59ae089f8b4efa50e3319c858fb749d70ea81f",
|
||||
"bytes": 6034
|
||||
},
|
||||
"scope": "14 shared contracts across source/npm1.0.6/frozen-workspace2.1.0 and 8 historical defects across two frozen implementations."
|
||||
},
|
||||
"browser": [
|
||||
{
|
||||
"name": "first",
|
||||
"note": "Fixture error: accidental window prefix in event-name strings; original report retained.",
|
||||
"stats": {
|
||||
"startTime": "2026-09-11T23:17:11.938Z",
|
||||
"duration": 43755.335,
|
||||
"expected": 2,
|
||||
"skipped": 0,
|
||||
"unexpected": 16,
|
||||
"flaky": 0
|
||||
},
|
||||
"report": {
|
||||
"file": "refactor/.cache/ads02-browser-first-report.json",
|
||||
"sha256": "1f75f147f6d9547b024bd4dd567b1117f84e283ea07c8d5aad02680db12be9d5",
|
||||
"bytes": 714132
|
||||
},
|
||||
"log": {
|
||||
"file": "refactor/.cache/ads02-browser-first.log",
|
||||
"sha256": "4a93233ec7bd568a34b4acee199afea67679e049ba5bc4f26c390f9c455b5fc0",
|
||||
"bytes": 51425
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/.last-run.json",
|
||||
"sha256": "277f7af5229ae3845c8f2eebca17747ecab246343884ac63d5e4f8a2224dde85",
|
||||
"bytes": 831
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-4af82-completion-restores-content-chromium/error-context.md",
|
||||
"sha256": "05447494c0ba235ab5126353f802175c8dd1b706d73fa782660f5e99f9443088",
|
||||
"bytes": 11517
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-4af82-completion-restores-content-chromium/test-failed-1.png",
|
||||
"sha256": "6d583742166e610c20655efe045bd91f370069c21570e92d6fd1a1a5309944e7",
|
||||
"bytes": 85742
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-4af82-completion-restores-content-chromium/trace.zip",
|
||||
"sha256": "2e7c69b1142fdf5657231880d3225f291dfb1c6fd0bf4048452ee7b570602364",
|
||||
"bytes": 962815
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-a6f9a-reshold-and-resumed-content-chromium/error-context.md",
|
||||
"sha256": "4242932aef9d82d2e49ae4b0282b67e7446f851e6f919e1f0efb45482f8c363e",
|
||||
"bytes": 11494
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-a6f9a-reshold-and-resumed-content-chromium/test-failed-1.png",
|
||||
"sha256": "67f6d29acf3b6d8a2ce4ff9960bd98438fa5111ef28b6594625e0710e1dd9baa",
|
||||
"bytes": 85451
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-a6f9a-reshold-and-resumed-content-chromium/trace.zip",
|
||||
"sha256": "99def7123d37fdd97922db1fae23c1274428322b39232636f3b11506bf20fa9e",
|
||||
"bytes": 774428
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-bc3ff-ls-open-to-playable-content-chromium/error-context.md",
|
||||
"sha256": "e66b8bee07a7bb6ca3ae0d40ba47e6196e8c040a81eb046e59e97cbd87686262",
|
||||
"bytes": 11359
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-bc3ff-ls-open-to-playable-content-chromium/test-failed-1.png",
|
||||
"sha256": "c7aeb49e2d94bdeca9e7cf96c36c930d8e82013124eac1b63825cbde79de9d51",
|
||||
"bytes": 84845
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-bc3ff-ls-open-to-playable-content-chromium/trace.zip",
|
||||
"sha256": "cfbf508a33445bad979ed0ce731de99ab7282cd5273860e9bc5a293e2297e843",
|
||||
"bytes": 2500977
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-e65a0-acement-does-not-replay-ads-chromium/error-context.md",
|
||||
"sha256": "d63140df98c0bd5a5c1382d6546dafebb2ddf425b99ff2a87f878c15d867663a",
|
||||
"bytes": 11904
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-e65a0-acement-does-not-replay-ads-chromium/test-failed-1.png",
|
||||
"sha256": "9670500dec0dbca163cc3abcf1a55933da5d05553b6495381467f4446c861c28",
|
||||
"bytes": 83113
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-candida-e65a0-acement-does-not-replay-ads-chromium/trace.zip",
|
||||
"sha256": "8218520fe7bd3addeec3483aa111014a7cea77e311444d8c8a952fdefbf9be67",
|
||||
"bytes": 327973
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-1efa0-completion-restores-content-chromium/error-context.md",
|
||||
"sha256": "a6e76cc8c411fa74f68b08354b3898ac6ea76a29dd9438891de59e62d05ee430",
|
||||
"bytes": 11517
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-1efa0-completion-restores-content-chromium/test-failed-1.png",
|
||||
"sha256": "8b2b93410bae5ccb75858fe9e7cee0b13b1c1c5f72ee24ee71887699ffe0911b",
|
||||
"bytes": 85667
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-1efa0-completion-restores-content-chromium/trace.zip",
|
||||
"sha256": "643bd9845f0115ed261e3806c7a1eb724d1baad2beb878f555cce5ab69509774",
|
||||
"bytes": 981934
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-7ce1f-acement-does-not-replay-ads-chromium/error-context.md",
|
||||
"sha256": "b06bd805a8c8592a5b0c10cdf1a20a21333926fad1edccd22c7fe0491d98cb06",
|
||||
"bytes": 10063
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-7ce1f-acement-does-not-replay-ads-chromium/test-failed-1.png",
|
||||
"sha256": "1bc111cc89a2f5182abca3991faac494e7bb08eb17f7e197cc99c360eb9bea2d",
|
||||
"bytes": 85757
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-7ce1f-acement-does-not-replay-ads-chromium/trace.zip",
|
||||
"sha256": "2b9a7b66770c1f8e0be41867c6395920a263fef345f39ab152c44be656b900bb",
|
||||
"bytes": 827835
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-bd6d9-ls-open-to-playable-content-chromium/error-context.md",
|
||||
"sha256": "116b870a3c9a632ea24cafa63c340a9433d32ebb52629cebb95a70d832c09c9c",
|
||||
"bytes": 11359
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-bd6d9-ls-open-to-playable-content-chromium/test-failed-1.png",
|
||||
"sha256": "c7aeb49e2d94bdeca9e7cf96c36c930d8e82013124eac1b63825cbde79de9d51",
|
||||
"bytes": 84845
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-bd6d9-ls-open-to-playable-content-chromium/trace.zip",
|
||||
"sha256": "51cc4158ec08dc7ffaf772650c0504128e7c5988d25590fccd6dca7c0a769cec",
|
||||
"bytes": 2502705
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-d3f59-reshold-and-resumed-content-chromium/error-context.md",
|
||||
"sha256": "849c351ca5e52c1f2d318ea0b52f66a0d545d3a5a83e82eb0227e1341ee11a62",
|
||||
"bytes": 11494
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-d3f59-reshold-and-resumed-content-chromium/test-failed-1.png",
|
||||
"sha256": "67f6d29acf3b6d8a2ce4ff9960bd98438fa5111ef28b6594625e0710e1dd9baa",
|
||||
"bytes": 85451
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-candidate-core-publish-d3f59-reshold-and-resumed-content-chromium/trace.zip",
|
||||
"sha256": "74fdffde8420424a92301d0f561e9257615c83d2d48c66810f8d835934220681",
|
||||
"bytes": 753295
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-038a7-ls-open-to-playable-content-chromium/error-context.md",
|
||||
"sha256": "0d8e511d5322d29807ac64113c1d722951d6651da5455d5602ae5aed8b4bf077",
|
||||
"bytes": 11359
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-038a7-ls-open-to-playable-content-chromium/test-failed-1.png",
|
||||
"sha256": "ce8bb9e88c0963c81b7b57527545e84d9ef28ade0443e521ccddf658439a7bc3",
|
||||
"bytes": 86066
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-038a7-ls-open-to-playable-content-chromium/trace.zip",
|
||||
"sha256": "256cd01936ac9c52f283d5261595c7da272acfc796f5b602cc80a5d599d0fd94",
|
||||
"bytes": 2497213
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-408f9-reshold-and-resumed-content-chromium/error-context.md",
|
||||
"sha256": "1a7d860ee698ae3025f15ce5af11766c1804b0b6bf32bb8e31ac99d2aae7b10d",
|
||||
"bytes": 11494
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-408f9-reshold-and-resumed-content-chromium/test-failed-1.png",
|
||||
"sha256": "190ef0f38dec3e15535cb24669a255da2be7972b2ae2d282255579952d598f1f",
|
||||
"bytes": 85592
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-408f9-reshold-and-resumed-content-chromium/trace.zip",
|
||||
"sha256": "87cfab2ead83790042514d679e81f31e31911f64bc359b1f25dee1f0da2eb26e",
|
||||
"bytes": 786246
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-5a82d-acement-does-not-replay-ads-chromium/error-context.md",
|
||||
"sha256": "346452cd39e825afdee8d912887d8c94c5cd31b6ae4eb474eb7406603c0ebb26",
|
||||
"bytes": 11844
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-5a82d-acement-does-not-replay-ads-chromium/test-failed-1.png",
|
||||
"sha256": "e89961b465d1cab2e2dadd889c1cd7ec2a4445fadc6e5b64a7a2be4d6d349c52",
|
||||
"bytes": 82544
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-5a82d-acement-does-not-replay-ads-chromium/trace.zip",
|
||||
"sha256": "62990cfdd6d6df611258e9f4f81436e08e5deb94715fd931ca97a87845f4eba9",
|
||||
"bytes": 305094
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-e88cb-completion-restores-content-chromium/error-context.md",
|
||||
"sha256": "53649990ecd022be23b799c762d919e2a2557d59082bd99fd3b3b2b4de54a2bc",
|
||||
"bytes": 11517
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-e88cb-completion-restores-content-chromium/test-failed-1.png",
|
||||
"sha256": "a8c4b0f780656c5c22fde95c33844dca73ce234e326c95b2d626f089d42fbab3",
|
||||
"bytes": 85827
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-candida-e88cb-completion-restores-content-chromium/trace.zip",
|
||||
"sha256": "93e4131d8b76e7434c2adff11fc6079d06094d60b6ecd40437ee1b2480d7cf19",
|
||||
"bytes": 994224
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-4059c-ls-open-to-playable-content-chromium/error-context.md",
|
||||
"sha256": "8bc40421d083bd4da4a732e42183d4362e71a3ddc9b9f0445a4e646241019a02",
|
||||
"bytes": 11359
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-4059c-ls-open-to-playable-content-chromium/test-failed-1.png",
|
||||
"sha256": "e84abc16fb03fadc2ff6c0b9014257309d693708983472cba24d56698cdb644d",
|
||||
"bytes": 85019
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-4059c-ls-open-to-playable-content-chromium/trace.zip",
|
||||
"sha256": "2455462a8e7ef451b512870487f31189d5743053701bed59ec108cfb39663002",
|
||||
"bytes": 2517192
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-70f1d-reshold-and-resumed-content-chromium/error-context.md",
|
||||
"sha256": "a3a3e05f78e8aa9387fffcd195e814b8b07d08633dab20ad17763718e796af22",
|
||||
"bytes": 11494
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-70f1d-reshold-and-resumed-content-chromium/test-failed-1.png",
|
||||
"sha256": "190ef0f38dec3e15535cb24669a255da2be7972b2ae2d282255579952d598f1f",
|
||||
"bytes": 85592
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-70f1d-reshold-and-resumed-content-chromium/trace.zip",
|
||||
"sha256": "d18960a99fa8126a57b28c96792c58b750340fbf66b4dba0d00f934103128dcf",
|
||||
"bytes": 801155
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-b0ec8-completion-restores-content-chromium/error-context.md",
|
||||
"sha256": "466a7503fc51d9d91452deea73a6cd260c4063f089ac29e1f0e5345117f68eed",
|
||||
"bytes": 11517
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-b0ec8-completion-restores-content-chromium/test-failed-1.png",
|
||||
"sha256": "8ca355ad820a8fe9eef893f8e902aa31c81cf154ed2493bbe042f6f8148e5122",
|
||||
"bytes": 85885
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-b0ec8-completion-restores-content-chromium/trace.zip",
|
||||
"sha256": "16898ce55027ffb1fe41803cad2e08d618ba46d7b39702032f045b06873867ed",
|
||||
"bytes": 996878
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-cafa4-acement-does-not-replay-ads-chromium/error-context.md",
|
||||
"sha256": "6aa961de1b9a539ac4ed0d198357a4c663dcc41b9642d718fa7355485e8f1036",
|
||||
"bytes": 10063
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-cafa4-acement-does-not-replay-ads-chromium/test-failed-1.png",
|
||||
"sha256": "a813c19361195cf8a21f10ed75c551d91f1b7ac6f2b28e85aaa591956db25103",
|
||||
"bytes": 85938
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-first-results/ads-published-core-publish-cafa4-acement-does-not-replay-ads-chromium/trace.zip",
|
||||
"sha256": "1040ec019bfb2a0596fbbd97d37ac104f5cc955496804925bd87b30e40e566bb",
|
||||
"bytes": 1069405
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "main",
|
||||
"note": "Fixture errors: pause before initialization and Windows WebKit layout-sized videoWidth; no production change.",
|
||||
"stats": {
|
||||
"startTime": "2026-09-11T23:18:17.690Z",
|
||||
"duration": 87437.579,
|
||||
"expected": 45,
|
||||
"skipped": 0,
|
||||
"unexpected": 9,
|
||||
"flaky": 0
|
||||
},
|
||||
"report": {
|
||||
"file": "refactor/.cache/ads02-browser-main-report.json",
|
||||
"sha256": "c8367b7fe7816a43f240e2deeeab5678b2e621faa2d47f328658e81c5fa849fc",
|
||||
"bytes": 4379446
|
||||
},
|
||||
"log": {
|
||||
"file": "refactor/.cache/ads02-browser-main.log",
|
||||
"sha256": "777c8c15c0de7276795b15a0f805645e82381c15b35d72cd7e2fc103072a0fc3",
|
||||
"bytes": 42791
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/.last-run.json",
|
||||
"sha256": "ddff586209b5e0d613af233bcdd6916c98eb6d1cd7d843677a03f501a265d863",
|
||||
"bytes": 488
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-candida-4af82-completion-restores-content-webkit/error-context.md",
|
||||
"sha256": "1ffa2343602c5ec45c07dd286ab36ddbd7c05d046e7023d0f74cb83203f295f6",
|
||||
"bytes": 11609
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-candida-4af82-completion-restores-content-webkit/test-failed-1.png",
|
||||
"sha256": "9661ab1955197d0d14d43e5886b1d915368b6108ebae7822e24f2ce5cc23126a",
|
||||
"bytes": 284210
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-candida-4af82-completion-restores-content-webkit/trace.zip",
|
||||
"sha256": "6fbbb702ec5dd582b083371722cdb36c4bb57ce019b83a133b52ee6ebb2c9678",
|
||||
"bytes": 474156
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-candida-e65a0-acement-does-not-replay-ads-chromium/error-context.md",
|
||||
"sha256": "b397553768ba42ddbcdc11dfb395f9efdd0d8f521d3f0235344befe19b8bbaac",
|
||||
"bytes": 11890
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-candida-e65a0-acement-does-not-replay-ads-chromium/test-failed-1.png",
|
||||
"sha256": "850afa86feea866de96bb79010972202cd50963a1a00d63993c2407f277fc55e",
|
||||
"bytes": 82366
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-candida-e65a0-acement-does-not-replay-ads-chromium/trace.zip",
|
||||
"sha256": "81a6d2908790b8e2b953fdd8fc20d7a444a9ae6fe80b467c9d7aa5f6105d6147",
|
||||
"bytes": 309426
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-publish-1efa0-completion-restores-content-webkit/error-context.md",
|
||||
"sha256": "8f283bdd5ed4524d757f9a6b341d9981c768fe99d3c958cd6124f17819b145d9",
|
||||
"bytes": 11609
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-publish-1efa0-completion-restores-content-webkit/test-failed-1.png",
|
||||
"sha256": "9661ab1955197d0d14d43e5886b1d915368b6108ebae7822e24f2ce5cc23126a",
|
||||
"bytes": 284210
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-publish-1efa0-completion-restores-content-webkit/trace.zip",
|
||||
"sha256": "8a763218a56f680a4153ec45e7a5fba1f09c0f69a504e5adea05eea2d7e4cfdb",
|
||||
"bytes": 482775
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-publish-7ce1f-acement-does-not-replay-ads-chromium/error-context.md",
|
||||
"sha256": "f7978d6f06677fc98a4cd3f2a65c398ea6a71ee6c516f1be5d54bf19144c1ac0",
|
||||
"bytes": 11890
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-publish-7ce1f-acement-does-not-replay-ads-chromium/test-failed-1.png",
|
||||
"sha256": "9670500dec0dbca163cc3abcf1a55933da5d05553b6495381467f4446c861c28",
|
||||
"bytes": 83113
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-publish-7ce1f-acement-does-not-replay-ads-chromium/trace.zip",
|
||||
"sha256": "e88f95d4949c8a742ead3abaeb12daad0907613fe132714aff2c98ee22ec6558",
|
||||
"bytes": 292129
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-publish-7ce1f-acement-does-not-replay-ads-webkit/error-context.md",
|
||||
"sha256": "7dd0f72e6174308fe6ff874a8d7c69c010bee358d4366cc6a999cc60d2109e64",
|
||||
"bytes": 11879
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-publish-7ce1f-acement-does-not-replay-ads-webkit/test-failed-1.png",
|
||||
"sha256": "7aea52a6492913db997af24e7e0549fbb687c6ff0f8eebc62dfe7663ed966686",
|
||||
"bytes": 295773
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-candidate-core-publish-7ce1f-acement-does-not-replay-ads-webkit/trace.zip",
|
||||
"sha256": "37df9e1bddd1332dd0256e84fccda9c48260ef2b70f7d13b01b8f8273c87fde9",
|
||||
"bytes": 439288
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-candida-e88cb-completion-restores-content-webkit/error-context.md",
|
||||
"sha256": "8ab533519aa8d4d6c34683e0d6ab5c18c462f6cff2c81fb0ccc6cda6bed3b0e8",
|
||||
"bytes": 11609
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-candida-e88cb-completion-restores-content-webkit/test-failed-1.png",
|
||||
"sha256": "deb11cedcc6e99a189368adb7884fbbf1fada92f20e546954dd93bfbe63d3f4a",
|
||||
"bytes": 281691
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-candida-e88cb-completion-restores-content-webkit/trace.zip",
|
||||
"sha256": "7e37ab7f87069b038e104b8d4da14801aaae66f44b24cf6195b257917d4f1061",
|
||||
"bytes": 477790
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-publish-b0ec8-completion-restores-content-webkit/error-context.md",
|
||||
"sha256": "a873017516be2c0ce81e3dc0464b557900c9ecbfa33b158b4df5d645d663deed",
|
||||
"bytes": 11609
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-publish-b0ec8-completion-restores-content-webkit/test-failed-1.png",
|
||||
"sha256": "deb11cedcc6e99a189368adb7884fbbf1fada92f20e546954dd93bfbe63d3f4a",
|
||||
"bytes": 281691
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-publish-b0ec8-completion-restores-content-webkit/trace.zip",
|
||||
"sha256": "6a3bc7bee58db58a56d6819b1773980c9ccd4d704f4461936400577c07ddb69d",
|
||||
"bytes": 458980
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-publish-cafa4-acement-does-not-replay-ads-chromium/error-context.md",
|
||||
"sha256": "349c384f17f8be457530f496b7dee004ac73fef1dd3f1f8f0389b26a4fc19479",
|
||||
"bytes": 11830
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-publish-cafa4-acement-does-not-replay-ads-chromium/test-failed-1.png",
|
||||
"sha256": "cc675a6a18ac7f88bc57c8b5465d3e0e50da713ec3bd93aab1af3808f391bfb0",
|
||||
"bytes": 83287
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-publish-cafa4-acement-does-not-replay-ads-chromium/trace.zip",
|
||||
"sha256": "0ed30c8369d6c321a881661a6d0cf5a5ed132bc60ece69522bb570fbdfa44e85",
|
||||
"bytes": 306689
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-publish-cafa4-acement-does-not-replay-ads-webkit/error-context.md",
|
||||
"sha256": "35119c774f59077c08d4fa8560fc659eede6d9385be920915072f00bda08801d",
|
||||
"bytes": 11638
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-publish-cafa4-acement-does-not-replay-ads-webkit/test-failed-1.png",
|
||||
"sha256": "c05abe618a1941b6f4ef394a23488d30b966e9ece26ffab1ab2fa0488e80e3e6",
|
||||
"bytes": 292476
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-main-results/ads-published-core-publish-cafa4-acement-does-not-replay-ads-webkit/trace.zip",
|
||||
"sha256": "bae6016c038d23fc1161b8d6544148c2eb34ef52329328086264a7a1e8606314",
|
||||
"bytes": 430948
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "complete",
|
||||
"note": "Four core/plugin combinations plus exact published rejection and zero-threshold observations; no skips or retries.",
|
||||
"stats": {
|
||||
"startTime": "2026-09-11T23:21:01.439Z",
|
||||
"duration": 83617.205,
|
||||
"expected": 66,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"report": {
|
||||
"file": "refactor/.cache/ads02-browser-complete-report.json",
|
||||
"sha256": "0136934ce8fe090edad7f5aed7111f0895e61ad12d072b5f6e71f5ae5584e76b",
|
||||
"bytes": 5925060
|
||||
},
|
||||
"log": {
|
||||
"file": "refactor/.cache/ads02-browser-complete.log",
|
||||
"sha256": "1d5918f237c457e44227c24047ee2bed941a94a155bca922667290d18ee50413",
|
||||
"bytes": 12396
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"file": "refactor/.cache/ads02-browser-complete-results/.last-run.json",
|
||||
"sha256": "91d1c43004802cd49950d78eb11c8fa7d05da8ffffe219a8b13b2f561bc00903",
|
||||
"bytes": 45
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ci": {
|
||||
"file": "refactor/.cache/ads02-ci.log",
|
||||
"sha256": "3a27de7214c4ecd8344b036f56290eaf89a09b75c3861268fab65e17a4a3c602",
|
||||
"bytes": 84503,
|
||||
"status": "passed",
|
||||
"tests": 806,
|
||||
"unit": 747,
|
||||
"engineering": 14,
|
||||
"baseline": 45,
|
||||
"productionTypeScript": 262,
|
||||
"coreDeclarationsUnchanged": 37,
|
||||
"warning": "Existing generated docs/assets/ts/artplayer.d.ts unused eslint-disable; no errors."
|
||||
},
|
||||
"limits": [
|
||||
"Injected Promise rejection is not device autoplay-policy acceptance.",
|
||||
"Core 4.5.5 still pending.",
|
||||
"Source-built candidate only; complete distribution validation belongs to 04-06.",
|
||||
"Full screen, real hidden-page policy, rapid metadata and teardown media-resource acceptance remain open."
|
||||
],
|
||||
"visualReview": {
|
||||
"file": "refactor/.cache/ads02-video-review.png",
|
||||
"source": "Complete report candidate core / candidate Ads Chromium ads-video-playing attachment",
|
||||
"observed": "Decoded local pattern, countdown, muted icon and fullscreen button visible in ad overlay."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# PKG-ADS-02:Ads 行为、错误和真实媒体基线
|
||||
|
||||
起点 c6b82c33;本步建立测试基线,不修改 Ads 生产源码、公开声明、样式或产物。
|
||||
使用实际 npm 1.0.6、Git 冻结的未发布工作区 2.1.0,以及当前源码构建;不使用虚构的
|
||||
npm 2.1.0。完整来源见 [01 契约](../baselines/ads-contract.md)。
|
||||
|
||||
## 交付与验收
|
||||
|
||||
新增窄范围时钟/宿主 helper 与 58 项 Node 测试:14 项共享行为在三实现运行,8 类
|
||||
历史缺陷仅在两个冻结实现运行。实际 option-validator 校验配置,包含浅 i18n、参数
|
||||
身份、互不干扰的实例、首次播放、倒计时暂停、跳过、restart、媒体错误与 destroy。
|
||||
默认 test:unit 接入,并提供 yarn test:ads;没有新依赖或 lockfile 变动。
|
||||
|
||||
新增 66 项三引擎浏览器测试,全部通过、无重试/跳过。每个引擎 16 项为核心
|
||||
5.4.1/候选 × Ads 1.0.6/候选的四种正常行为组合,另 6 项为两个核心上的三种历史
|
||||
问题观察。正常组合包含 HTML 图片实际加载、视频解码颜色/时间、静音点击、跳过阈值、
|
||||
自动完成、实际 404 恢复、计时暂停和切换主视频。截图中广告画面、倒计时与静音/全屏
|
||||
控件可见;已查看候选核心/候选 Ads 的实际视频截图。
|
||||
|
||||
播放拒绝通过精确的媒体 play 注入复现:广告启动拒绝后计时继续;主片恢复拒绝后广告
|
||||
隐藏并 emit skip,两者均产生未处理拒绝。仅捕获指定错误,其他未处理错误仍导致失败。
|
||||
这不是设备自动播放认证。零阈值用例暂停倒计时观察真实按钮首 tick 前状态。
|
||||
|
||||
完整本地 CI 806 项通过(747 单元、14 工程、45 基线),262 个生产 TS 文件严格检查,
|
||||
核心 37 份声明无漂移。保留一条既有生成编辑器声明的 unused eslint-disable warning,
|
||||
没有新增 lint 错误。执行日志与最终结论记录在 [执行证据](../baselines/ads-validation.json)。
|
||||
生产源码和类型未改,严格检查复用现有项目;完整包分发和原生设备验收仍由后续任务负责。
|
||||
|
||||
## 原始失败与修正
|
||||
|
||||
- 首轮 Chromium 2 通过、16 失败:给浏览器全局加 window 时误改了事件名字符串,
|
||||
监听到错误的名字,修复夹具字符串。原始 trace/报告完整保留。
|
||||
- 第二轮三引擎 45 通过、9 失败:5 个暂停测试在广告初始化前调用方法;补真实 DOM
|
||||
出现条件。4 个 WebKit 视频测试错误要求 videoWidth=320,而 Windows WebKit 报
|
||||
布局宽度 640;改为读取实际解码帧的黄/蓝/青色像素,所有引擎使用相同断言。
|
||||
- 第三轮新增主片拒绝/零阈值观察后 66 项全部通过。没有增加超时、重试或跳过。
|
||||
- 初始 Node 文件的 lint 换行、多语句和正则规范问题已按仓库规则修正。
|
||||
|
||||
## 兼容边界和后续
|
||||
|
||||
API-01/04/05/06/08 的可控行为与真实页面用例已经固定。这里没有改变用户调用、
|
||||
事件顺序、DOM/CSS 或时长规则,也没有把历史缺陷定义为新实现必须保留的行为。
|
||||
ADS-LIFE-01、ADS-MEDIA-01、ADS-UI-01 从源码观察提升到复现,保持 open;其中
|
||||
metadata 竞态、真实隐藏页行为和全屏状态仍待验证,不能因部分复现视为整个风险已解决。
|
||||
|
||||
03 按配置、计时状态、视图、媒体和生命周期所有权拆分,并为历史缺陷增加候选修复断言;
|
||||
04 严格 TS/准确声明/旧类型入口;05 保留旧核心 4.5.5 和实际设备、销毁媒体资源等验证;
|
||||
06 完整产物消费和 demo。正常 play/pause 只管理倒计时的已有语义要保留。
|
||||
测试文件地图、命令与限制见 [Ads 验证说明](../ads-validation.md),包内架构说明随 03
|
||||
实际拆分交付,避免提前记录尚不存在的模块。
|
||||
|
||||
## 提交与回退
|
||||
|
||||
独立本地提交主题:test(ads): [PKG-ADS-02] cover legacy timing and real media contracts。
|
||||
回退该提交移除本步测试、脚本接入、文档和状态变化,生产行为不受影响。
|
||||
没有 push、tag、publish 或 merge;全项目重构和发布复盘目标继续。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 217 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 138 / doing 4 / blocked 0 / done 75 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 137 / doing 4 / blocked 0 / done 76 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -257,7 +257,7 @@
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-ADS-01 | artplayer-plugin-ads<br>核对包契约与历史用法 | BASE-05 | 核对 npm 1.0.0~1.0.6 与未发布工作区 2.1.0 的真实参数、类型、结果、倒计时、分发和旧核心来源 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
|
||||
| PKG-ADS-02 | artplayer-plugin-ads<br>建立特有行为与错误测试 | PKG-ADS-01, ENG-03, ENG-05 | 固定 npm 1.0.6 与工作区契约;HTML/视频/图片 HTML、跳过阈值、计时链、工厂复用、多实例、过早/重复方法、播放拒绝与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
|
||||
| PKG-ADS-02 | artplayer-plugin-ads<br>建立特有行为与错误测试 | PKG-ADS-01, ENG-03, ENG-05 | 固定 npm 1.0.6 与工作区契约;HTML/视频/图片 HTML、跳过阈值、计时链、工厂复用、多实例、过早/重复方法、播放拒绝与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
|
||||
| PKG-ADS-03 | artplayer-plugin-ads<br>整理内部职责与资源 | PKG-ADS-02, CORE-10, CORE-13 | 配置/广告状态/计时/视图/主视频恢复分离,保留产品规则;资源与版本检查改用经验证的旧核心能力边界 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
|
||||
| PKG-ADS-04 | artplayer-plugin-ads<br>迁移自有源码和公开类型 | PKG-ADS-03, ENG-04, ENG-06, CORE-07 | 兼容真实 html/video/url/i18n、旧 export= 与工作区 source/type 类型接受面;时长 string/number 漂移与未实现别名单独处置 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-ADS-05 | artplayer-plugin-ads<br>验证新旧核心和组合 | PKG-ADS-04, CORE-22 | 本地广告 fixture、多实例、正常结束/跳过/销毁;核验历史关联核心 4.5.5、发布 5.4.1 与候选核心的实际归档/能力/媒体组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
@@ -485,3 +485,4 @@
|
||||
- PKG-DASH-04: [记录](changes/2026-09-12-PKG-DASH-04-types.md) [记录](baselines/dash-types-validation.json) [记录](dash-validation.md)
|
||||
- PKG-DASH-05: [记录](changes/2026-09-12-PKG-DASH-05-sdk-checkpoint.md) [记录](baselines/dash-sdk.json) [记录](baselines/dash-sdk-checkpoint.json) [记录](dash-validation.md) [记录](changes/2026-09-12-PKG-DASH-05-sdk-types.md) [记录](baselines/dash-sdk-types-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-sdk-events.md) [记录](baselines/dash-sdk-events-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-seek-diagnosis.md) [记录](baselines/dash-seek-diagnosis.json)
|
||||
- PKG-ADS-01: [记录](baselines/ads-release.json) [记录](baselines/ads-contract.md) [记录](baselines/ads-contract-validation.json) [记录](changes/2026-09-12-PKG-ADS-01-contract.md) [记录](scripts/ads-contract.test.mjs)
|
||||
- PKG-ADS-02: [记录](changes/2026-09-12-PKG-ADS-02-tests.md) [记录](ads-validation.md) [记录](baselines/ads-validation.json)
|
||||
@@ -1,5 +1,25 @@
|
||||
# 进度与证据
|
||||
|
||||
## 最新完成:Ads-02 行为与真实媒体回归
|
||||
|
||||
新增 58 项 Node 测试,覆盖源码、实际 npm 1.0.6 和冻结的未发布工作区 2.1.0;
|
||||
倒计时、阈值、方法返回/事件顺序、独立实例、restart、媒体错误及八类历史缺陷有可重跑证据。
|
||||
Ads 测试接入 test:unit,并提供 yarn test:ads,没有新增依赖或修改生产源码/类型/产物。
|
||||
|
||||
三引擎实际浏览器 66 项全部通过、无跳过/重试:5.4.1/候选核心 × 旧/候选插件,
|
||||
本地图片、视频解码像素、静音/跳过、自动恢复、404、暂停倒计时与切源。额外精确注入
|
||||
两类 play 拒绝,并复现零阈值首 tick 前点击问题。注入拒绝不等于设备自动播放策略验收。
|
||||
首轮事件名夹具错误及第二轮初始化等待/Windows WebKit 尺寸假设失败完整保留。
|
||||
|
||||
完整本地 CI 806 项通过(747 单元、14 工程、45 基线),262 个生产 TS 严格检查,
|
||||
核心声明无漂移。Ads 生命周期/媒体/UI 风险仍 open,旧核心 4.5.5、全屏、隐藏页策略、
|
||||
真实资源销毁、完整分发及设备验收由 03~06 接续;DASH 暂停 seek 等既有风险不变。
|
||||
|
||||
当前 217 项:76 done、4 doing、137 todo。下一项 Ads-03 开始配置/计时/视图/媒体与
|
||||
生命周期拆分及历史缺陷修复。见 [测试说明](ads-validation.md)、
|
||||
[本步记录](changes/2026-09-12-PKG-ADS-02-tests.md) 和 [执行证据](baselines/ads-validation.json)。
|
||||
以下为历史检查点当时状态。
|
||||
|
||||
## 最新完成:Ads-01 实际发布契约核对
|
||||
|
||||
工作区 Ads 2.1.0 未发布;实际 npm 五个 1.0.x 版本、30 个成员已校验冻结,主要行为基线
|
||||
|
||||
@@ -144,6 +144,6 @@
|
||||
| ADS-TYPE-01 | open / 源码/产物事实 | Ads 声明丢失真实 html/video/url,旧 totalDuration 为 string,当前 source/type 未实现 | PKG-ADS-02, PKG-ADS-04 |
|
||||
| ADS-DIST-01 | open / 已复现 | Ads npm 1.0.6 default namespace 与未发布 2.1.0 callable/exports 分发不同 | PKG-ADS-04, PKG-ADS-06, REL-01 |
|
||||
| ADS-CORE-01 | open / 源码/产物事实 | Ads 旧发布关联核心 4.5.5,当前源码新增 >=5 门槛和 document 事件桥依赖 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
|
||||
| ADS-LIFE-01 | open / 源码/产物事实 | Ads 计时链、重复/过早调用、工厂复用与销毁缺乏资源边界 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
|
||||
| ADS-MEDIA-01 | open / 源码/产物事实 | Ads metadata 与 play 拒绝、隐藏页媒体状态和内容恢复仍缺真实验证 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
|
||||
| ADS-UI-01 | open / 源码/产物事实 | Ads 零等待跳过与初始状态不符,全屏图标/外部状态需核验 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
|
||||
| ADS-LIFE-01 | open / 已复现 | Ads 计时链、重复/过早调用、工厂复用与销毁缺乏资源边界 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
|
||||
| ADS-MEDIA-01 | open / 已复现 | Ads metadata 与 play 拒绝、隐藏页媒体状态和内容恢复仍缺真实验证 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
|
||||
| ADS-UI-01 | open / 已复现 | Ads 零等待跳过与初始状态不符,全屏图标/外部状态需核验 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
|
||||
+44
-9
@@ -3015,30 +3015,65 @@
|
||||
{
|
||||
"id": "ADS-LIFE-01",
|
||||
"title": "Ads 计时链、重复/过早调用、工厂复用与销毁缺乏资源边界",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": ["PKG-ADS-02", "PKG-ADS-03", "PKG-ADS-05"],
|
||||
"evidence": ["refactor/baselines/ads-contract.md", "packages/artplayer-plugin-ads/src/index.js"],
|
||||
"owners": [
|
||||
"PKG-ADS-02",
|
||||
"PKG-ADS-03",
|
||||
"PKG-ADS-05"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/baselines/ads-contract.md",
|
||||
"packages/artplayer-plugin-ads/src/index.js",
|
||||
"test/ads.test.js",
|
||||
"test/browser/ads.spec.js",
|
||||
"refactor/ads-validation.md",
|
||||
"refactor/baselines/ads-validation.json"
|
||||
],
|
||||
"compatibleResolution": "先复现重复 play 的计时链、skip 后计时、destroy 后恢复与复用工厂 options;以实例所有权和幂等清理修正,不更改广告时长/跳过规则。",
|
||||
"closureCriteria": "时钟/订阅/DOM 与主片恢复的成功、异常、重入、销毁、复用和多实例回归通过。"
|
||||
},
|
||||
{
|
||||
"id": "ADS-MEDIA-01",
|
||||
"title": "Ads metadata 与 play 拒绝、隐藏页媒体状态和内容恢复仍缺真实验证",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": ["PKG-ADS-02", "PKG-ADS-03", "PKG-ADS-05"],
|
||||
"evidence": ["refactor/baselines/ads-contract.md", "packages/artplayer-plugin-ads/src/index.js", "docs/assets/example/ads.js"],
|
||||
"owners": [
|
||||
"PKG-ADS-02",
|
||||
"PKG-ADS-03",
|
||||
"PKG-ADS-05"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/baselines/ads-contract.md",
|
||||
"packages/artplayer-plugin-ads/src/index.js",
|
||||
"docs/assets/example/ads.js",
|
||||
"test/ads.test.js",
|
||||
"test/browser/ads.spec.js",
|
||||
"refactor/ads-validation.md",
|
||||
"refactor/baselines/ads-validation.json"
|
||||
],
|
||||
"compatibleResolution": "用本地媒体验证事件安装顺序、加载失败与拒绝、倒计时和广告播放的独立语义,恢复主片时不吞公共拒绝或在销毁后播放。",
|
||||
"closureCriteria": "实际广告/主片的加载、暂停/恢复、失败/换源/销毁和隐藏状态有新旧核心组合证据。"
|
||||
},
|
||||
{
|
||||
"id": "ADS-UI-01",
|
||||
"title": "Ads 零等待跳过与初始状态不符,全屏图标/外部状态需核验",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": ["PKG-ADS-02", "PKG-ADS-03", "PKG-ADS-05"],
|
||||
"evidence": ["refactor/baselines/ads-contract.md", "packages/artplayer-plugin-ads/src/index.js", "docs/assets/example/ads.js"],
|
||||
"owners": [
|
||||
"PKG-ADS-02",
|
||||
"PKG-ADS-03",
|
||||
"PKG-ADS-05"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/baselines/ads-contract.md",
|
||||
"packages/artplayer-plugin-ads/src/index.js",
|
||||
"docs/assets/example/ads.js",
|
||||
"test/ads.test.js",
|
||||
"test/browser/ads.spec.js",
|
||||
"refactor/ads-validation.md",
|
||||
"refactor/baselines/ads-validation.json"
|
||||
],
|
||||
"compatibleResolution": "复现 playDuration<=0 文案可关闭但 isCanClose 尚为 false;按既有 demo 规则修复,验证全屏按钮和外部退出状态,保留 CSS/DOM 钩子。",
|
||||
"closureCriteria": "零/正/不可跳阈值、真实点击、倒计时/详情/静音/全屏与外部状态组合均有回归。"
|
||||
}
|
||||
|
||||
+6
-2
@@ -2545,11 +2545,15 @@
|
||||
"ENG-03",
|
||||
"ENG-05"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "固定 npm 1.0.6 与工作区契约;HTML/视频/图片 HTML、跳过阈值、计时链、工厂复用、多实例、过早/重复方法、播放拒绝与销毁",
|
||||
"acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-12-PKG-ADS-02-tests.md",
|
||||
"ads-validation.md",
|
||||
"baselines/ads-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-ADS-03",
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
Use the pinned Node/Yarn toolchain from `../refactor/toolchain-setup.md`.
|
||||
|
||||
`yarn test:ads` runs source, verified npm 1.0.6 and the frozen unpublished 2.1.0
|
||||
Ads bundle against controlled clock/host contracts. It is included in `test:unit`.
|
||||
The helper uses the real option validator but does not simulate media decoding or layout.
|
||||
Historical defect assertions apply only to frozen implementations. See
|
||||
[Ads validation](../refactor/ads-validation.md) for artifact overrides and browser scope.
|
||||
|
||||
| Command | Scope |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `yarn test:unit` | Playback and DASH regressions, public contracts against released/current code, and JS/TS fixture loading |
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Controlled clock/host contracts use Node's runner.
|
||||
import test from 'node:test'
|
||||
import { adsEnvironment, adsImplementations } from './helpers/ads.js'
|
||||
|
||||
const implementations = await adsImplementations()
|
||||
const skips = host => host.calls.filter(call => call[0] === 'emit' && call[1] === 'artplayerPluginAds:skip')
|
||||
const clicks = host => host.calls.filter(call => call[0] === 'emit' && call[1] === 'artplayerPluginAds:click')
|
||||
const contentPlays = host => host.calls.filter(call => call[0] === 'content.play')
|
||||
|
||||
for (const { name, factory } of implementations) {
|
||||
test(`${name}: lazy synchronous factory starts once after ready and either playback event`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
const result = factory({ html: '<b>ad</b>' })(host.art)
|
||||
assert.deepEqual(Object.keys(result), ['name', 'skip', 'pause', 'play'])
|
||||
assert.equal(result.name, 'artplayerPluginAds')
|
||||
assert.equal(host.node(), undefined)
|
||||
assert.equal(env.timers.size, 0)
|
||||
host.art.emit('play')
|
||||
assert.equal(host.node(), undefined)
|
||||
host.art.emit('ready').emit('video:playing')
|
||||
const node = host.node()
|
||||
assert(node)
|
||||
assert.equal(host.art.template.$ads, node)
|
||||
assert.equal(host.node('html').innerHTML, '<b>ad</b>')
|
||||
assert.equal(host.art.playing, false)
|
||||
host.art.emit('play').emit('video:playing')
|
||||
assert.equal(host.node(), node)
|
||||
assert.equal(env.timers.size, 1)
|
||||
assert.equal(host.calls.filter(call => call[0] === 'content.pause').length, 1)
|
||||
})
|
||||
|
||||
test(`${name}: actual validator keeps numeric durations and complete shallow i18n`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
assert.throws(() => factory({ totalDuration: '10' })(env.host().art), /totalDuration/)
|
||||
assert.throws(() => factory({ playDuration: '5' })(env.host().art), /playDuration/)
|
||||
assert.throws(() => factory({ html: 1 })(env.host().art), /html/)
|
||||
assert.throws(() => factory({ i18n: { close: 'close' } })(env.host().art), /i18n/)
|
||||
const host = env.host()
|
||||
factory()(host.art)
|
||||
assert.equal(host.normalized.totalDuration, 10)
|
||||
assert.equal(host.normalized.playDuration, 5)
|
||||
assert.equal(host.normalized.muted, false)
|
||||
assert.equal(host.normalized.html, '')
|
||||
assert.equal(host.normalized.video, '')
|
||||
})
|
||||
|
||||
test(`${name}: video wins over HTML and metadata starts media and countdown`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
factory({ html: '<b>fallback</b>', video: 'ad.mp4', muted: true })(host.art)
|
||||
host.start()
|
||||
assert.equal(host.node('html'), undefined)
|
||||
assert.equal(host.node('video').tagName, 'VIDEO')
|
||||
assert.equal(host.node('video').muted, true)
|
||||
assert.equal(env.timers.size, 0)
|
||||
host.fire('video', 'loadedmetadata')
|
||||
assert.equal(env.timers.size, 1)
|
||||
assert.equal(host.node('video').paused, false)
|
||||
assert.equal(host.node('timer').style.display, 'flex')
|
||||
assert.equal(host.node('control').style.display, 'flex')
|
||||
assert.equal(host.node('loading').style.display, 'none')
|
||||
assert.equal(host.calls.filter(call => call[0] === 'ad.play').length, 1)
|
||||
})
|
||||
|
||||
test(`${name}: each active second counts down and total duration restores content`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
factory({ html: 'ad', playDuration: 2, totalDuration: 3 })(host.art)
|
||||
host.start()
|
||||
assert.equal(host.node('close').innerHTML, '2秒后可关闭广告')
|
||||
assert.equal(host.node('countdown').innerHTML, '3秒')
|
||||
env.tick(999)
|
||||
assert.equal(host.node('countdown').innerHTML, '3秒')
|
||||
env.tick(1)
|
||||
assert.equal(host.node('close').innerHTML, '1秒后可关闭广告')
|
||||
assert.equal(host.node('countdown').innerHTML, '2秒')
|
||||
env.tick(1000)
|
||||
assert.equal(host.node('close').innerHTML, '关闭广告')
|
||||
assert.equal(contentPlays(host).length, 0)
|
||||
env.tick(1000)
|
||||
assert.equal(host.node('countdown').innerHTML, '0秒')
|
||||
assert.equal(host.node().style.display, 'none')
|
||||
assert.equal(skips(host).length, 1)
|
||||
assert.equal(skips(host)[0][2], host.normalized)
|
||||
assert.equal(contentPlays(host).length, 1)
|
||||
assert.equal(env.timers.size, 0)
|
||||
})
|
||||
|
||||
test(`${name}: pause/play control the countdown without changing ad video state`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
const plugin = factory({ video: 'ad.mp4', totalDuration: 4 })(host.art)
|
||||
host.start()
|
||||
host.fire('video', 'loadedmetadata')
|
||||
env.tick(1000)
|
||||
assert.equal(plugin.pause(), undefined)
|
||||
env.tick(5000)
|
||||
assert.equal(host.node('countdown').innerHTML, '3秒')
|
||||
assert.equal(host.node('video').paused, false)
|
||||
assert.equal(plugin.play(), undefined)
|
||||
env.tick(1000)
|
||||
assert.equal(host.node('countdown').innerHTML, '2秒')
|
||||
assert.equal(host.calls.filter(call => call[0] === 'ad.play').length, 1)
|
||||
})
|
||||
|
||||
test(`${name}: button obeys positive threshold while public skip bypasses it`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
const plugin = factory({ html: 'ad', playDuration: 2, totalDuration: 5 })(host.art)
|
||||
host.start()
|
||||
host.fire('close', 'click')
|
||||
env.tick(1000)
|
||||
host.fire('close', 'click')
|
||||
assert.equal(skips(host).length, 0)
|
||||
assert.equal(plugin.skip(), undefined)
|
||||
assert.equal(skips(host).length, 1)
|
||||
const second = env.host()
|
||||
factory({ html: 'second', playDuration: 2, totalDuration: 5 })(second.art)
|
||||
second.start()
|
||||
env.tick(2000)
|
||||
second.fire('close', 'click')
|
||||
assert.equal(skips(second).length, 1)
|
||||
})
|
||||
|
||||
test(`${name}: non-skippable button stays hidden and automatic completion still occurs`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
factory({ html: 'ad', playDuration: 2, totalDuration: 2 })(host.art)
|
||||
host.start()
|
||||
assert.equal(host.node('close').style.display, 'none')
|
||||
env.tick(2000)
|
||||
assert.equal(skips(host).length, 1)
|
||||
})
|
||||
|
||||
test(`${name}: translations preserve first-placeholder substitution and normalized payload`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
const i18n = { close: 'skip', countdown: '%s seconds %s', detail: 'visit', canBeClosed: 'wait %s' }
|
||||
factory({ html: 'ad', playDuration: 1, totalDuration: 3, i18n })(host.art)
|
||||
host.start()
|
||||
assert.equal(host.node('countdown').innerHTML, '3 seconds %s')
|
||||
assert.equal(host.node('close').innerHTML, 'wait 1')
|
||||
env.tick(1000)
|
||||
assert.equal(host.node('close').innerHTML, 'skip')
|
||||
host.fire('close', 'click')
|
||||
assert.equal(skips(host)[0][2].i18n, i18n)
|
||||
})
|
||||
|
||||
test(`${name}: ad/detail clicks preserve URL and option payload; unlinked ad still emits`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
factory({ html: '<img src="poster.png">', url: 'https://example.invalid/ad' })(host.art)
|
||||
host.start()
|
||||
host.fire('html', 'click')
|
||||
host.fire('detail', 'click')
|
||||
assert.deepEqual(env.opened, ['https://example.invalid/ad', 'https://example.invalid/ad'])
|
||||
assert.equal(clicks(host).length, 2)
|
||||
assert(clicks(host).every(call => call[2] === host.normalized))
|
||||
const second = env.host()
|
||||
factory({ html: 'unlinked' })(second.art)
|
||||
second.start()
|
||||
assert.equal(second.node('detail').style.display, 'none')
|
||||
second.fire('html', 'click')
|
||||
assert.equal(clicks(second).length, 1)
|
||||
assert.equal(env.opened.length, 2)
|
||||
})
|
||||
|
||||
test(`${name}: mute applies only to the ad and toggles existing icons`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
factory({ video: 'ad.mp4', muted: true })(host.art)
|
||||
host.start()
|
||||
assert.equal(host.nodes.get('volume').style.display, 'none')
|
||||
assert.equal(host.nodes.get('volumeClose').style.display, 'inline-flex')
|
||||
host.fire('muted', 'click')
|
||||
assert.equal(host.node('video').muted, false)
|
||||
assert.equal(host.nodes.get('volume').style.display, 'inline-flex')
|
||||
assert.equal(host.nodes.get('volumeClose').style.display, 'none')
|
||||
host.fire('muted', 'click')
|
||||
assert.equal(host.node('video').muted, true)
|
||||
})
|
||||
|
||||
test(`${name}: document visibility pauses and resumes only the countdown`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
factory({ html: 'ad', totalDuration: 4 })(host.art)
|
||||
host.start()
|
||||
env.tick(1000)
|
||||
host.visibility(true)
|
||||
env.tick(5000)
|
||||
assert.equal(host.node('countdown').innerHTML, '3秒')
|
||||
host.visibility(false)
|
||||
env.tick(1000)
|
||||
assert.equal(host.node('countdown').innerHTML, '2秒')
|
||||
})
|
||||
|
||||
test(`${name}: media error before metadata restores content and retains skip order`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
factory({ video: 'missing.mp4' })(host.art)
|
||||
host.start()
|
||||
host.calls.length = 0
|
||||
host.fire('video', 'error')
|
||||
assert.deepEqual(host.calls.map(call => call.slice(0, 2)), [
|
||||
['content.play', false],
|
||||
['emit', 'play'],
|
||||
['ad.pause'],
|
||||
['style', 'artplayer-plugin-ads'],
|
||||
['emit', 'artplayerPluginAds:skip'],
|
||||
])
|
||||
assert.equal(host.node().style.display, 'none')
|
||||
assert.equal(env.timers.size, 0)
|
||||
})
|
||||
|
||||
test(`${name}: separate factories keep instance timers and event options independent`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const first = env.host()
|
||||
const second = env.host()
|
||||
const firstPlugin = factory({ html: 'first', totalDuration: 2 })(first.art)
|
||||
factory({ html: 'second', totalDuration: 3 })(second.art)
|
||||
first.start()
|
||||
second.start()
|
||||
firstPlugin.pause()
|
||||
env.tick(3000)
|
||||
assert.equal(skips(first).length, 0)
|
||||
assert.equal(first.node('countdown').innerHTML, '2秒')
|
||||
assert.equal(skips(second).length, 1)
|
||||
assert.equal(skips(second)[0][2], second.normalized)
|
||||
firstPlugin.play()
|
||||
env.tick(2000)
|
||||
assert.equal(skips(first).length, 1)
|
||||
assert.equal(skips(first)[0][2], first.normalized)
|
||||
})
|
||||
|
||||
test(`${name}: initial input is read at attach and completed preroll stays hidden after restart`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
const option = { html: 'before', totalDuration: 1 }
|
||||
const attach = factory(option)
|
||||
option.html = 'at attach'
|
||||
attach(host.art)
|
||||
option.html = 'after attach'
|
||||
host.start()
|
||||
assert.equal(host.node('html').innerHTML, 'at attach')
|
||||
env.tick(1000)
|
||||
const root = host.node()
|
||||
host.art.emit('restart', 'second.mp4').emit('ready').emit('play').emit('video:playing')
|
||||
assert.equal(host.node(), root)
|
||||
assert.equal(root.style.display, 'none')
|
||||
assert.equal(skips(host).length, 1)
|
||||
assert.equal(env.timers.size, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Freeze legacy defects separately; migrated candidates must not inherit these assertions.
|
||||
for (const { name, factory } of implementations.filter(item => item.historical)) {
|
||||
test(`${name} historical: duplicate play starts parallel countdown chains`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
const plugin = factory({ html: 'ad', totalDuration: 10 })(host.art)
|
||||
host.start()
|
||||
plugin.play()
|
||||
assert.equal(env.timers.size, 2)
|
||||
env.tick(1000)
|
||||
assert.equal(host.node('countdown').innerHTML, '8秒')
|
||||
plugin.pause()
|
||||
assert.equal(env.timers.size, 1)
|
||||
})
|
||||
|
||||
test(`${name} historical: skip leaves a queued callback and emits completion again`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
const plugin = factory({ html: 'ad', totalDuration: 1 })(host.art)
|
||||
host.start()
|
||||
plugin.skip()
|
||||
assert.equal(env.timers.size, 1)
|
||||
env.tick(1000)
|
||||
assert.equal(skips(host).length, 2)
|
||||
plugin.skip()
|
||||
assert.equal(skips(host).length, 3)
|
||||
})
|
||||
|
||||
test(`${name} historical: destroy leaves countdown intent that requests content play`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
factory({ html: 'ad', totalDuration: 1 })(host.art)
|
||||
host.start()
|
||||
host.art.destroy()
|
||||
assert.equal(env.timers.size, 1)
|
||||
env.tick(1000)
|
||||
assert.deepEqual(contentPlays(host), [['content.play', true]])
|
||||
})
|
||||
|
||||
test(`${name} historical: zero wait displays close immediately but rejects the first click`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
factory({ html: 'ad', playDuration: 0, totalDuration: 3 })(host.art)
|
||||
host.start()
|
||||
assert.equal(host.node('close').innerHTML, '关闭广告')
|
||||
host.fire('close', 'click')
|
||||
assert.equal(skips(host).length, 0)
|
||||
env.tick(1000)
|
||||
host.fire('close', 'click')
|
||||
assert.equal(skips(host).length, 1)
|
||||
})
|
||||
|
||||
test(`${name} historical: skip before initialization plays content then throws`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
const plugin = factory({ html: 'ad' })(host.art)
|
||||
assert.throws(() => plugin.skip(), /undefined/)
|
||||
assert.equal(contentPlays(host).length, 1)
|
||||
})
|
||||
|
||||
test(`${name} historical: reusing one factory changes the first instance event payload identity`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const attach = factory({ html: 'shared' })
|
||||
const first = env.host()
|
||||
const firstPlugin = attach(first.art)
|
||||
first.start()
|
||||
const firstOption = first.normalized
|
||||
const second = env.host()
|
||||
attach(second.art)
|
||||
second.start()
|
||||
firstPlugin.skip()
|
||||
assert.notEqual(firstOption, second.normalized)
|
||||
assert.equal(skips(first)[0][2], second.normalized)
|
||||
})
|
||||
|
||||
test(`${name} historical: repeated metadata schedules and starts the ad twice`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
factory({ video: 'ad.mp4' })(host.art)
|
||||
host.start()
|
||||
host.fire('video', 'loadedmetadata')
|
||||
host.fire('video', 'loadedmetadata')
|
||||
assert.equal(env.timers.size, 2)
|
||||
assert.equal(host.calls.filter(call => call[0] === 'ad.play').length, 2)
|
||||
})
|
||||
|
||||
test(`${name} historical: play before initialization schedules a callback against missing DOM`, (t) => {
|
||||
const env = adsEnvironment(t)
|
||||
const host = env.host()
|
||||
const plugin = factory({ html: 'ad' })(host.art)
|
||||
plugin.play()
|
||||
assert.equal(env.timers.size, 1)
|
||||
assert.throws(() => env.tick(1000), /null|undefined/)
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
# Browser regression entry
|
||||
|
||||
`yarn test:browser test/browser/ads.spec.js` tests actual Ads 1.0.6 and candidate code
|
||||
with published 5.4.1 and candidate cores using local image/video media. It covers actual
|
||||
decoded pixels, countdown, skipping, content restoration, 404 and source replacement.
|
||||
Published-only rejection cases inject an exact play failure; they do not certify device
|
||||
autoplay policies. See [Ads validation](../../refactor/ads-validation.md).
|
||||
|
||||
Audio Track tests use the source and frozen 1.1.0 plugin on both cores. The local AAC fixture
|
||||
has a pinned hash and generation command in media/audio-tone.json and uses the existing Range
|
||||
server. The suite retains published cleanup/native-pause defects as explicit observations.
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { compilePackage } from '../helpers/load.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
let published
|
||||
let candidate
|
||||
let release
|
||||
|
||||
test.beforeAll(async () => {
|
||||
release = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/ads-release.json', import.meta.url))).release
|
||||
const member = `package/${release.manifest.main}`
|
||||
const bytes = readMember(await ensureArchive(release), member)
|
||||
assert.equal(hash(bytes), release.files[member])
|
||||
published = bytes.toString()
|
||||
candidate = process.env.ARTPLAYER_ADS_ARTIFACT
|
||||
? fs.readFileSync(process.env.ARTPLAYER_ADS_ARTIFACT, 'utf8')
|
||||
: await compilePackage('artplayer-plugin-ads', 'umd')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
const state = await page.evaluate(() => {
|
||||
const ad = document.querySelector('.artplayer-plugin-ads-video')
|
||||
return {
|
||||
events: window.adsEvents,
|
||||
rejected: window.adsRejected,
|
||||
ad: ad && { time: ad.currentTime, paused: ad.paused, width: ad.videoWidth, height: ad.videoHeight, error: ad.error?.code },
|
||||
content: window.art && { time: window.art.currentTime, paused: window.art.video.paused, destroyed: window.art.isDestroy },
|
||||
}
|
||||
})
|
||||
await testInfo.attach('ads-state', { contentType: 'application/json', body: JSON.stringify(state) })
|
||||
await page.evaluate(() => {
|
||||
// Historical skip/destroy do not cancel their countdown. Explicit pause prevents
|
||||
// fixture teardown from masking the behavior asserted by a different test.
|
||||
window.art?.plugins.artplayerPluginAds?.pause()
|
||||
if (window.art && !window.art.isDestroy)
|
||||
window.art.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
async function openAds(page, core, plugin, option, testInfo) {
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
const code = plugin === 'published' ? published : candidate
|
||||
await page.addScriptTag({ content: code })
|
||||
await testInfo.attach('ads-inputs', { contentType: 'application/json', body: JSON.stringify({ core, plugin, release: plugin === 'published' ? release : undefined, sha256: hash(code), artifact: plugin === 'candidate' ? process.env.ARTPLAYER_ADS_ARTIFACT || 'workspace source build' : undefined }) })
|
||||
await page.evaluate((option) => {
|
||||
window.adsEvents = []
|
||||
window.art = new window.Artplayer({
|
||||
container: '.player',
|
||||
url: '/test/pattern.mp4',
|
||||
muted: true,
|
||||
plugins: [window.artplayerPluginAds(option)],
|
||||
})
|
||||
window.art.on('artplayerPluginAds:skip', value => window.adsEvents.push({ name: 'skip', option: value }))
|
||||
window.art.on('artplayerPluginAds:click', value => window.adsEvents.push({ name: 'click', option: value }))
|
||||
document.querySelector('#play').onclick = () => window.art.play()
|
||||
}, option)
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toHaveCount(0)
|
||||
await page.locator('#play').click()
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
for (const plugin of ['published', 'candidate']) {
|
||||
const label = `${core} core / ${plugin} Ads`
|
||||
|
||||
test(`${label}: real image HTML, skip threshold and resumed content`, async ({ page }, testInfo) => {
|
||||
await openAds(page, core, plugin, { html: '<img alt="Ad artwork" src="/test/thumbnail-grid.svg">', playDuration: 2, totalDuration: 8 }, testInfo)
|
||||
const root = page.locator('.artplayer-plugin-ads')
|
||||
await expect(root).toBeVisible()
|
||||
await expect.poll(() => page.locator('img[alt="Ad artwork"]').evaluate(image => image.complete && image.naturalWidth > 0)).toBe(true)
|
||||
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
|
||||
await page.locator('.artplayer-plugin-ads-close').click()
|
||||
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(0)
|
||||
await expect(page.locator('.artplayer-plugin-ads-close')).toHaveText('关闭广告')
|
||||
await page.locator('.artplayer-plugin-ads-close').click()
|
||||
await expect(root).toBeHidden()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
|
||||
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
|
||||
await testInfo.attach('ads-image-completed', { contentType: 'image/png', body: await page.screenshot() })
|
||||
})
|
||||
|
||||
test(`${label}: real video decodes, mute toggles and countdown completion restores content`, async ({ page }, testInfo) => {
|
||||
await openAds(page, core, plugin, { video: '/test/pattern.mp4', html: 'unused', muted: true, playDuration: 5, totalDuration: 3 }, testInfo)
|
||||
const ad = page.locator('.artplayer-plugin-ads-video')
|
||||
await expect.poll(() => ad.evaluate(video => video.currentTime)).toBeGreaterThan(0.2)
|
||||
expect(await ad.evaluate(video => ({ paused: video.paused, muted: video.muted }))).toEqual({ paused: false, muted: true })
|
||||
// Windows WebKit reports layout-sized videoWidth; sample actual decoded colors.
|
||||
const pixels = await ad.evaluate((video) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 80
|
||||
canvas.height = 45
|
||||
const context = canvas.getContext('2d')
|
||||
context.drawImage(video, 0, 0, 80, 45)
|
||||
return [30, 45, 75].map(x => [...context.getImageData(x, 2, 1, 1).data])
|
||||
})
|
||||
expect(pixels[0][0]).toBeGreaterThan(180)
|
||||
expect(pixels[0][1]).toBeGreaterThan(180)
|
||||
expect(pixels[0][2]).toBeLessThan(80)
|
||||
expect(pixels[1][0]).toBeLessThan(80)
|
||||
expect(pixels[1][2]).toBeGreaterThan(180)
|
||||
expect(pixels[2][1]).toBeGreaterThan(180)
|
||||
expect(pixels[2][2]).toBeGreaterThan(180)
|
||||
await testInfo.attach('ads-decoded-pixels', { contentType: 'application/json', body: JSON.stringify(pixels) })
|
||||
await expect(page.locator('.artplayer-plugin-ads-html')).toHaveCount(0)
|
||||
await expect(page.locator('.artplayer-plugin-ads-close')).toBeHidden()
|
||||
await page.locator('.artplayer-plugin-ads-muted').click()
|
||||
expect(await ad.evaluate(video => video.muted)).toBe(false)
|
||||
await page.locator('.artplayer-plugin-ads-muted').click()
|
||||
await testInfo.attach('ads-video-playing', { contentType: 'image/png', body: await page.screenshot() })
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
|
||||
expect(await ad.evaluate(video => video.paused)).toBe(true)
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
|
||||
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
|
||||
})
|
||||
|
||||
test(`${label}: actual missing ad media fails open to playable content`, async ({ page }, testInfo) => {
|
||||
await page.route('**/missing-ad.mp4', route => route.fulfill({ status: 404, body: '' }))
|
||||
await openAds(page, core, plugin, { video: '/missing-ad.mp4', muted: true }, testInfo)
|
||||
await expect.poll(() => page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
|
||||
expect(await page.locator('.artplayer-plugin-ads-video').evaluate(video => video.error?.code)).toBe(4)
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
|
||||
})
|
||||
|
||||
test(`${label}: countdown pause resumes once and source replacement does not replay ads`, async ({ page }, testInfo) => {
|
||||
await openAds(page, core, plugin, { html: 'one preroll', playDuration: 1, totalDuration: 8 }, testInfo)
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toBeVisible()
|
||||
await page.evaluate(() => window.art.plugins.artplayerPluginAds.pause())
|
||||
const count = await page.locator('.artplayer-plugin-ads-countdown').textContent()
|
||||
// A real clock observation specifically checks that pause stops countdown.
|
||||
await page.waitForTimeout(1200)
|
||||
await expect(page.locator('.artplayer-plugin-ads-countdown')).toHaveText(count)
|
||||
await page.evaluate(() => window.art.plugins.artplayerPluginAds.play())
|
||||
await expect(page.locator('.artplayer-plugin-ads-countdown')).not.toHaveText(count)
|
||||
await page.evaluate(() => {
|
||||
window.art.plugins.artplayerPluginAds.pause()
|
||||
window.art.plugins.artplayerPluginAds.skip()
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
|
||||
await page.evaluate(() => window.art.switchUrl('/test/pattern.mp4?replacement=1'))
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toHaveCount(1)
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
|
||||
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
|
||||
})
|
||||
}
|
||||
|
||||
test(`${core} core / published Ads historical: rejected ad play leaks a rejection while timer continues`, async ({ page }, testInfo) => {
|
||||
await page.addInitScript(() => {
|
||||
window.adsRejected = []
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
if (event.reason?.message === 'Ads test controlled NotAllowedError') {
|
||||
window.adsRejected.push({ name: event.reason.name, message: event.reason.message })
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
const play = HTMLMediaElement.prototype.play
|
||||
HTMLMediaElement.prototype.play = function () {
|
||||
if (this.classList.contains('artplayer-plugin-ads-video'))
|
||||
return Promise.reject(new DOMException('Ads test controlled NotAllowedError', 'NotAllowedError'))
|
||||
return play.call(this)
|
||||
}
|
||||
})
|
||||
await openAds(page, core, 'published', { video: '/test/pattern.mp4', muted: true, totalDuration: 2 }, testInfo)
|
||||
await expect.poll(() => page.evaluate(() => window.adsRejected)).toEqual([{ name: 'NotAllowedError', message: 'Ads test controlled NotAllowedError' }])
|
||||
expect(await page.locator('.artplayer-plugin-ads-video').evaluate(video => video.paused)).toBe(true)
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
|
||||
})
|
||||
|
||||
test(`${core} core / published Ads historical: rejected content restoration hides ad and leaks rejection`, async ({ page }, testInfo) => {
|
||||
await page.addInitScript(() => {
|
||||
window.adsRejected = []
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
if (event.reason?.message === 'Ads test controlled content rejection') {
|
||||
window.adsRejected.push({ name: event.reason.name, message: event.reason.message })
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
const play = HTMLMediaElement.prototype.play
|
||||
HTMLMediaElement.prototype.play = function () {
|
||||
if (this === window.art?.video && document.querySelector('.artplayer-plugin-ads'))
|
||||
return Promise.reject(new DOMException('Ads test controlled content rejection', 'NotAllowedError'))
|
||||
return play.call(this)
|
||||
}
|
||||
})
|
||||
await openAds(page, core, 'published', { html: 'ad', totalDuration: 1 }, testInfo)
|
||||
await expect.poll(() => page.evaluate(() => window.adsRejected)).toEqual([{ name: 'NotAllowedError', message: 'Ads test controlled content rejection' }])
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
|
||||
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
|
||||
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
|
||||
})
|
||||
|
||||
test(`${core} core / published Ads historical: zero threshold advertises skip before accepting click`, async ({ page }, testInfo) => {
|
||||
await openAds(page, core, 'published', { html: 'ad', playDuration: 0, totalDuration: 5 }, testInfo)
|
||||
const close = page.locator('.artplayer-plugin-ads-close')
|
||||
await expect(close).toBeVisible()
|
||||
// Freeze only the countdown after real initialization; this is a button contract.
|
||||
await page.evaluate(() => window.art.plugins.artplayerPluginAds.pause())
|
||||
await expect(close).toHaveText('关闭广告')
|
||||
await close.click()
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toBeVisible()
|
||||
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(0)
|
||||
await page.evaluate(() => window.art.plugins.artplayerPluginAds.play())
|
||||
await expect(page.locator('.artplayer-plugin-ads-countdown')).not.toHaveText('5秒')
|
||||
await close.click()
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
|
||||
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
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 vm from 'node:vm'
|
||||
import validator from 'option-validator'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { loadPackage } from './load.js'
|
||||
|
||||
function commonJS(bytes) {
|
||||
const module = { exports: {} }
|
||||
vm.runInNewContext(bytes.toString(), {
|
||||
module,
|
||||
exports: module.exports,
|
||||
get document() { return globalThis.document },
|
||||
get window() { return globalThis.window },
|
||||
setTimeout: (...args) => globalThis.setTimeout(...args),
|
||||
clearTimeout: id => globalThis.clearTimeout(id),
|
||||
console,
|
||||
}, { timeout: 5000 })
|
||||
return module.exports.default || module.exports
|
||||
}
|
||||
|
||||
export async function adsImplementations() {
|
||||
const baseline = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/ads-release.json', import.meta.url)))
|
||||
const archive = await ensureArchive(baseline.release)
|
||||
const member = `package/${baseline.release.manifest.main}`
|
||||
const bytes = readMember(archive, member)
|
||||
assert.equal(hash(bytes), baseline.release.files[member])
|
||||
const file = 'packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.js'
|
||||
const historical = execFileSync('git', ['show', `${baseline.sourceCommit}:${file}`])
|
||||
assert.equal(hash(historical.toString().replaceAll('\r\n', '\n')), baseline.source[file])
|
||||
const implementations = [
|
||||
{ name: 'source', factory: (await loadPackage('artplayer-plugin-ads')).default },
|
||||
{ name: 'published-1.0.6', historical: true, factory: commonJS(bytes) },
|
||||
{ name: 'frozen-workspace-2.1.0', historical: true, factory: commonJS(historical) },
|
||||
]
|
||||
for (const file of (process.env.ARTPLAYER_TEST_ADS || '').split(path.delimiter).filter(Boolean)) {
|
||||
const bytes = fs.readFileSync(file)
|
||||
const factory = file.endsWith('.mjs') ? (await import(`data:text/javascript;base64,${bytes.toString('base64')}`)).default : commonJS(bytes)
|
||||
implementations.push({ name: `artifact-${path.basename(file)}`, factory })
|
||||
}
|
||||
return implementations
|
||||
}
|
||||
|
||||
// Intent fixture only: these nodes do not model layout, HTML parsing or media decoding.
|
||||
export function adsEnvironment(t) {
|
||||
const document = Object.assign(new EventTarget(), { hidden: false })
|
||||
const opened = []
|
||||
for (const [name, value] of Object.entries({ document, window: { open: url => opened.push(url) } })) {
|
||||
const previous = Object.getOwnPropertyDescriptor(globalThis, name)
|
||||
Object.defineProperty(globalThis, name, { configurable: true, value })
|
||||
t.after(() => previous ? Object.defineProperty(globalThis, name, previous) : Reflect.deleteProperty(globalThis, name))
|
||||
}
|
||||
const timers = new Map()
|
||||
let nextId = 0
|
||||
let now = 0
|
||||
t.mock.method(globalThis, 'setTimeout', (callback, delay) => {
|
||||
const id = ++nextId
|
||||
timers.set(id, { callback, at: now + Number(delay) })
|
||||
return id
|
||||
})
|
||||
t.mock.method(globalThis, 'clearTimeout', id => timers.delete(id))
|
||||
t.mock.method(Date, 'now', () => now)
|
||||
function tick(milliseconds) {
|
||||
const end = now + milliseconds
|
||||
let steps = 0
|
||||
while (true) {
|
||||
const entry = [...timers].sort((a, b) => a[1].at - b[1].at)[0]
|
||||
if (!entry || entry[1].at > end)
|
||||
break
|
||||
assert(++steps < 10000, 'Unexpected endless timer chain')
|
||||
const [id, timer] = entry
|
||||
timers.delete(id)
|
||||
now = timer.at
|
||||
timer.callback()
|
||||
}
|
||||
now = end
|
||||
}
|
||||
function host() {
|
||||
const calls = []
|
||||
const listeners = new Map()
|
||||
const proxies = []
|
||||
const nodes = new Map()
|
||||
let normalized
|
||||
class Node extends EventTarget {
|
||||
constructor(tag = 'div', className = '', html = '') {
|
||||
super()
|
||||
this.tagName = tag.toUpperCase()
|
||||
this.className = className
|
||||
this.innerHTML = html
|
||||
this.style = {}
|
||||
this.children = []
|
||||
this.muted = false
|
||||
this.paused = true
|
||||
this.ownerDocument = document
|
||||
if (className)
|
||||
nodes.set(className, this)
|
||||
}
|
||||
|
||||
play() {
|
||||
calls.push(['ad.play'])
|
||||
this.paused = false
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
pause() {
|
||||
calls.push(['ad.pause'])
|
||||
this.paused = true
|
||||
}
|
||||
}
|
||||
const prefix = 'artplayer-plugin-ads'
|
||||
function append(parent, value) {
|
||||
if (typeof value !== 'string') {
|
||||
parent.children.push(value)
|
||||
return value
|
||||
}
|
||||
const match = value.match(/^<(\w+)\s[^>]*class="([^"]*)"[^>]*>([\s\S]*)<\/\w+>$/)
|
||||
assert(match, 'Extend the fixture for a new wrapper; do not claim a real HTML parser')
|
||||
const [, tag, className, inner] = match
|
||||
const node = new Node(tag, className, inner)
|
||||
parent.children.push(node)
|
||||
for (const child of inner.matchAll(/<div class="([^"]+)"[^>]*>([\s\S]*?)<\/div>/g))
|
||||
node.children.push(new Node('div', child[1], child[2]))
|
||||
return node
|
||||
}
|
||||
const art = {
|
||||
template: { $player: new Node() },
|
||||
icons: Object.fromEntries(['volume', 'volumeClose', 'fullscreenOn', 'fullscreenOff', 'loading'].map(name => [name, new Node('span', name)])),
|
||||
constructor: {
|
||||
version: '5.4.1',
|
||||
validator(value, schema) {
|
||||
normalized = validator(value, schema)
|
||||
return normalized
|
||||
},
|
||||
utils: {
|
||||
append,
|
||||
query: (selector, parent) => parent.children.find(child => child.className === selector.slice(1)),
|
||||
setStyle(node, name, value) {
|
||||
calls.push(['style', node?.className, name, value])
|
||||
node.style[name] = value
|
||||
},
|
||||
errorHandle(condition, message) {
|
||||
if (!condition)
|
||||
throw new Error(message)
|
||||
},
|
||||
},
|
||||
},
|
||||
playing: false,
|
||||
fullscreen: false,
|
||||
isDestroy: false,
|
||||
on(name, callback) {
|
||||
if (!listeners.has(name))
|
||||
listeners.set(name, new Set())
|
||||
listeners.get(name).add(callback)
|
||||
return art
|
||||
},
|
||||
off(name, callback) {
|
||||
listeners.get(name)?.delete(callback)
|
||||
return art
|
||||
},
|
||||
once(name, callback) {
|
||||
const once = (...args) => {
|
||||
art.off(name, once)
|
||||
callback(...args)
|
||||
}
|
||||
return art.on(name, once)
|
||||
},
|
||||
emit(name, ...args) {
|
||||
calls.push(['emit', name, ...args])
|
||||
for (const callback of [...(listeners.get(name) || [])]) callback(...args)
|
||||
return art
|
||||
},
|
||||
proxy(target, name, callback) {
|
||||
target.addEventListener(name, callback)
|
||||
const off = () => target.removeEventListener(name, callback)
|
||||
proxies.push(off)
|
||||
return off
|
||||
},
|
||||
play() {
|
||||
calls.push(['content.play', art.isDestroy])
|
||||
art.playing = true
|
||||
art.emit('play')
|
||||
return Promise.resolve()
|
||||
},
|
||||
pause() {
|
||||
calls.push(['content.pause'])
|
||||
art.playing = false
|
||||
art.emit('pause')
|
||||
},
|
||||
destroy() {
|
||||
art.isDestroy = true
|
||||
art.emit('destroy')
|
||||
proxies.splice(0).forEach(off => off())
|
||||
listeners.clear()
|
||||
},
|
||||
}
|
||||
return {
|
||||
art,
|
||||
nodes,
|
||||
calls,
|
||||
listeners,
|
||||
get normalized() { return normalized },
|
||||
node: suffix => nodes.get(suffix ? `${prefix}-${suffix}` : prefix),
|
||||
start() { art.emit('ready').emit('play') },
|
||||
fire(suffix, name) { nodes.get(`${prefix}-${suffix}`).dispatchEvent(new Event(name)) },
|
||||
visibility(hidden) {
|
||||
document.hidden = hidden
|
||||
document.dispatchEvent(new Event('visibilitychange'))
|
||||
art.emit('document:visibilitychange')
|
||||
},
|
||||
}
|
||||
}
|
||||
return { host, tick, timers, document, opened }
|
||||
}
|
||||
Reference in new issue
Block a user