test(canvas): [PKG-CANVAS-05] checkpoint installed native picture-in-picture

This commit is contained in:
Harvey Zhao committed 2026-09-14 19:08:22 +08:00
1 parent c3967bcbf4
commit 37ebb7a034
21 files changed
+2552 -16

No files matched your search

+1 -1
View File
@@ -140,7 +140,7 @@ jobs:
# These extra installed packages are required by the browser helpers.
# Other ecosystem installation/SDK gates remain tracked by CI-01.
run: |
yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas 2>&1 | tee refactor/.cache/ci/browser-packages.log
yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas,artplayer-plugin-document-pip 2>&1 | tee refactor/.cache/ci/browser-packages.log
node --input-type=module -e 'import fs from "node:fs"; const { output } = JSON.parse(fs.readFileSync("refactor/.cache/packages/latest.json")); fs.appendFileSync(process.env.GITHUB_ENV, `ARTPLAYER_BROWSER_ARTIFACTS=${output}/browser-artifacts.json\n`);'
- name: Run all source browser checks
if: ${{ !cancelled() }}
@@ -89,3 +89,9 @@ plugin 1.1.0; candidate core's event-document focus checks fix this independentl
Candidate web fullscreen stays in the current ownerDocument body and restores
its captured placement. This does not prove native fullscreen availability inside
PiP, background throttling or physical-device support; PKG-DPIP-05 stays open.
`canvas-dpip.spec.js` additionally checks native Canvas video/track adoption,
subtitle seeking and all three close/destroy endings. Its installed mode requires
the verified five-package map, including Document PiP; the helper rejects missing
maps, stale inputs and conflicting source/artifact overrides. This is integration
evidence, not full package release acceptance or physical-device evidence.
@@ -106,3 +106,10 @@ single-track ownership, ordinary Canvas child insertion/removal and escaped inse
after destroy on old/new cores. It also belongs to the installed browser scope;
keep its bytes and native engine provenance with the lifecycle report.
See `refactor/canvas-validation.md` and the task evidence for actual outcomes and limitations.
`canvas-dpip.spec.js` follows the same Canvas, backing video and native subtitle
track into actual Document PiP windows. It covers playback/draw advancement,
paused subtitle seeking and close/native-close/destroy resource ownership. The
installed scope requires Document PiP's own verified package bytes as well; a
source helper may not fill a missing installed package. Unsupported native APIs
are recorded separately and never treated as native playback evidence.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,42 @@
# PKG-CANVAS-05 Canvas + 原生 Document PiP 检查点
进行中。新增 canvas-dpip.spec.js 验证实际 requestWindow、Canvas/底层 video/字幕
轨道跨文档归属、持续绘制与时间推进、暂停寻址字幕、插件关闭/原生关闭/销毁。
使用真实本地媒体和 VTT,RAF 观察器转发原生调度,不替换 PiP API 或媒体状态。
不支持的引擎只记录能力缺口,不计作原生窗口播放通过。
首轮为源码组合,18 项成功退出包含 Chromium/Firefox 12 项真实窗口播放与
WebKit 6 项 API 不可用记录。初版明确拒绝安装 map,避免把源码 Document PiP
混称为已安装包;报告保留在 canvas-dpip-source-report。
## 安装与 CI 范围扩展
package-check 通过已有 dpip-contract 核对发布文件,将 Document PiP 加入显式
可选包注册表。现在五包经过源码快照、三格式构建、pack、仓库外离线安装和冻结
重装,再校验全部文件摘要。core/chapter 的36运行时、5旧类型+8精确类型模式
仍是原来的两包范围;不把它们当作 Document PiP 的完整消费者验收。
dpipCandidate 现在优先核验显式安装 map,缺文件/缺包/旧输入报错,不再回退到
源码;冻结 workspace 与独立 ARTPLAYER_DPIP_ARTIFACT 覆盖也不能混用。这个
错误已有旧红新绿:此前不存在的安装map被静默忽略,Node测试3通过1失败;
修正后包/runner/CI检查共46通过,包含缺 Document PiP 安装步骤的工作流反例。
installed scope 和 CI 准备步骤增加第五包及 canvas-dpip.spec.js,仍保留完整
source suite、不吞失败、两份独立报告及原消费者执行顺序。main/legacy 的实际
浏览器结果与输入摘要见[证据](../baselines/canvas-dpip-validation.json):每种格式
均为51项成功退出,其中12项原生PiP、6项API不可用、33项Canvas字幕/生命周期与
Ambilight组合。原生窗口覆盖六种 core/ending 组合各在 Chromium/Firefox 执行,
不把 WebKit 的能力分支计为原生播放。这里不代表远端 CI 已运行。
严格工具 TS、专项/根 lint(0错误/1既有warning)、actionlint 和固定工具链通过。
未新增依赖,也没有更改浏览器重试、超时或媒体断言;新测试初版样式诊断已修复。
## 维护与限制
Canvas、Document PiP 的架构说明和测试/CI入口已同步。本次没有生产运行时修改,
不修改已有全局源码/安装性能门槛。重跑时先提供含五包的安装map,再运行 README
所列命令;source 对照仍独立存在。报告中的 API 不可用分支不是设备支持通过。
PKG-CANVAS-05 仍 doing:实际Safari/移动设备、后台节流和完整生态组合没有闭环。
DPIP-05/06、Canvas-06 及全量远端 CI/发布任务仍开放。作为本任务检查点单独本地
提交,提交审计通过后接续;没有推送、部署或npm发布。回退需一并还原五包CI配置、
加载器、测试与文档,不能只删除安装准备步骤而保留依赖它的新scope。
+1 -1
View File
@@ -2,7 +2,7 @@
浏览器CI现在分别执行 `yarn test:browser:source` 和
`yarn test:browser:installed`。前者清除继承的安装map并保留全部spec;后者要求已
核验的core/chapter/Ambilight/Canvas安装,执行明确列出的installed子集。两步不
核验的core/chapter/Ambilight/Canvas/Document PiP安装,执行明确列出的installed子集。两步不
吞失败,普通失败后仍保存另一范围的证据,分别上传browser-source/browser-installed。
入口与报告规则见[维护说明](../scripts/browser-validation/README.md)。source全量
耗时/60分钟预算、其余插件安装矩阵与远端运行仍待CI-01/CI-04,不计为已通过。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 259 项,范围 22 个包及工作区/示例。
状态:todo 50 / doing 19 / blocked 0 / done 190 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 49 / doing 20 / blocked 0 / done 190 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -347,7 +347,7 @@
| PKG-CANVAS-02 | artplayer-proxy-canvas<br>建立特有行为与错误测试 | PKG-CANVAS-01, ENG-03, ENG-05 | ready/loadeddata/canplay、play/seek、回调、resize 与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-CANVAS-03 | artplayer-proxy-canvas<br>整理内部职责与资源 | PKG-CANVAS-02, CORE-06, CORE-11, CORE-16 | video adapter/原 canvas 方法/RAF 绘制/事件订阅分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-CANVAS-04 | artplayer-proxy-canvas<br>迁移自有源码和公开类型 | PKG-CANVAS-03, ENG-04, ENG-06, CORE-07 | canvas 与媒体能力的精确组合类型,保持 Result | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
| PKG-CANVAS-05 | artplayer-proxy-canvas<br>验证新旧核心和组合 | PKG-CANVAS-04, CORE-22, PKG-DPIP-04, PKG-FACTORY-01, PKG-CANVAS-SUBTITLE-01 | 真实 video/canvas 绘制、字幕和 document PiP 恢复 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-CANVAS-05 | artplayer-proxy-canvas<br>验证新旧核心和组合 | PKG-CANVAS-04, CORE-22, PKG-DPIP-04, PKG-FACTORY-01, PKG-CANVAS-SUBTITLE-01 | 真实 video/canvas 绘制、字幕和 document PiP 恢复 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | doing |
| PKG-CANVAS-06 | artplayer-proxy-canvas<br>验证分发并同步文档 | PKG-CANVAS-05, ENG-07 | canvas.js、三种产物、调用兼容与资源证据 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
| PKG-CANVAS-SUBTITLE-01 | artplayer-proxy-canvas<br>修复 Canvas 代理原生字幕轨道挂载 | PKG-CANVAS-04 | 初始字幕能力、原生轨道挂载与资源生命周期适配及新旧核心回归 | 复现旧字幕 cues 为空;新旧核心加载、寻址、切换、销毁通过;普通 Canvas 节点行为和分发入口保持兼容 | M | done |
@@ -611,6 +611,7 @@
- PKG-CANVAS-02: [记录](changes/2026-09-12-PKG-CANVAS-02-tests.md) [记录](baselines/canvas-behavior-validation.json) [记录](canvas-validation.md)
- PKG-CANVAS-03: [记录](changes/2026-09-12-PKG-CANVAS-03-lifecycle.md) [记录](baselines/canvas-lifecycle-validation.json) [记录](canvas-validation.md)
- PKG-CANVAS-04: [记录](changes/2026-09-12-PKG-CANVAS-04-types.md) [记录](baselines/canvas-types-validation.json) [记录](canvas-validation.md)
- PKG-CANVAS-05: [记录](changes/2026-09-14-PKG-CANVAS-05-native-pip.md) [记录](baselines/canvas-dpip-validation.json) [记录](baselines/canvas-subtitles-validation.json) [记录](baselines/ambilight-installed-validation.json)
- PKG-MB-01: [记录](baselines/mb-release.json) [记录](baselines/mb-surface.json) [记录](baselines/mb-contract.md) [记录](baselines/mb-contract-validation.json) [记录](mb-validation.md) [记录](changes/2026-09-12-PKG-MB-01-contract.md)
- PKG-MB-02: [记录](changes/2026-09-12-PKG-MB-02-checkpoint.md) [记录](baselines/mb-media-checkpoint.json) [记录](mb-validation.md) [记录](changes/2026-09-12-PKG-MB-02-baseline.md) [记录](baselines/mb-behavior-validation.json)
- PKG-MB-03: [记录](changes/2026-09-12-PKG-MB-03-input.md) [记录](baselines/mb-input-validation.json)
+15
View File
@@ -1,5 +1,20 @@
# 进度与证据
## PKG-CANVAS-05 原生 PiP 与五包安装检查点(仍 doing)
源码首轮18项:Chromium/Firefox实际原生窗口12通过,WebKit仅6项API不可用记录。
Canvas、底层video与track一起移入窗口,播放/绘制继续,暂停seek字幕正确;插件
关闭、原生关闭和销毁均恢复/释放节点、窗口、占位与RAF。不是iframe模拟。
进一步把Document PiP加入已核验安装包注册与CI:五包离线安装/冻结重装和指纹检查
通过,main/legacy各51项成功退出(各12原生PiP+6不可用+33其他组合)。
修复dpip加载器忽略安装map回退源码的问题:旧Node测试3通过1失败,修正后工程/
工作流46通过;DPIP-INSTALLED-PROVENANCE-01按该证据关闭。缺map/缺包/陈旧输入及
冲突覆盖均不允许源码替代。严格工具TS、lint(1既有warning)、actionlint/工具链通过。
见[检查点](changes/2026-09-14-PKG-CANVAS-05-native-pip.md)和
[证据](baselines/canvas-dpip-validation.json)。259项:190 done、20 doing、49 todo。
Canvas真机/后台节流、DPIP完整消费者/设备、完整远端CI和发布仍开放;没有生产
运行时修改、依赖新增、push、部署或发布。该检查点独立提交后审计再继续下一步。
## PKG-CANVAS-SUBTITLE-01 原生字幕轨道修复完成
新旧核心三引擎复现 Canvas 字幕为空。先前说明需修正:初始失败中的 $track
+1
View File
@@ -273,3 +273,4 @@
| VCONSOLE-LIFECYCLE-01 | resolved / 已复现 | vConsole delayed log, panel and scroller work outlives its owner | SITE-07, SITE-05, SITE-VCONSOLE-01 |
| CHAPTER-FULLSCREEN-GEOMETRY-01 | resolved / 已复现 | Chromium 原生全屏后章节 hover 标题曾出现后又隐藏 | PKG-CHAPTER-05, PKG-CHAPTER-HOVER-01 |
| CANVAS-SUBTITLE-01 | resolved / 已复现 | Canvas底层video缺初始字幕轨道且普通Canvas挂载不能加载原生字幕 | PKG-CANVAS-SUBTITLE-01, PKG-CANVAS-05 |
| DPIP-INSTALLED-PROVENANCE-01 | resolved / 已复现 | Document PiP测试加载器忽略显式安装map并回退到源码 | PKG-CANVAS-05, CI-01 |
+24
View File
@@ -6241,6 +6241,30 @@
"test/browser/canvas-subtitles.spec.js"
],
"resolutionRationale": "Initial candidate and routing-only controls each fail all six subtitle cases. Bootstrap + insertion adaptation passes; npm 1.1.0 independently fails all six. Expanded native source tests pass 24; installed main and legacy pass 33 each; final six pass after adding baseline selection. Captions also visibly render in the local demo. This fixes subtitle capability/insertion only; broader Canvas integration remains open."
},
{
"id": "DPIP-INSTALLED-PROVENANCE-01",
"title": "Document PiP测试加载器忽略显式安装map并回退到源码",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"PKG-CANVAS-05",
"CI-01"
],
"evidence": [
"test/package-check.test.js",
"refactor/baselines/canvas-dpip-validation.json"
],
"compatibleResolution": "使用已安装文件及source/build指纹,缺输入拒绝执行;保留独立source模式。",
"closureCriteria": "缺安装map旧红新绿,实际五包main/legacy原生组合与工作流配置验证。",
"workspaceState": "Explicit map takes precedence; DPIP frozen/artifact overrides conflict instead of overriding installed provenance. Current CI prepares five packages before browser scopes.",
"resolutionEvidence": [
"refactor/changes/2026-09-14-PKG-CANVAS-05-native-pip.md",
"refactor/baselines/canvas-dpip-validation.json",
"test/helpers/dpip.js",
"refactor/scripts/ci-workflow.test.mjs"
],
"resolutionRationale": "Old loader test reports missing expected rejection; updated loader rejects missing/stale maps and conflicting modes. Five-package isolated build/pack/install supplies main and legacy native PiP tests with package hashes. Local workflow mutation protects the required DPIP preparation; remote runs remain unverified."
}
]
}
+2 -2
View File
@@ -90,10 +90,10 @@ export function validateCIWorkflow(source) {
const vueIndex = browser.steps.findIndex(step => step.run === 'yarn test:vue-consumer 2>&1 | tee refactor/.cache/ci/vue-consumer.log')
assert(vueIndex > consumerIndex && !Object.hasOwn(browser.steps[vueIndex], 'if'), 'Run Vue installed consumers after restoring canonical Node')
assert(browser.steps.some(step => step.uses?.startsWith('actions/upload-artifact@') && step.if === 'always()' && step.with.path.split('\n').includes('refactor/.cache/vue-consumer-*/')), 'Retain Vue consumer failure evidence')
const extraIndex = browser.steps.findIndex(step => step.run?.startsWith('yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas '))
const extraIndex = browser.steps.findIndex(step => step.run?.startsWith('yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas,artplayer-plugin-document-pip '))
const sourceEngineIndex = browser.steps.findIndex(step => step.run === 'yarn test:browser:source 2>&1 | tee refactor/.cache/ci/browser-source.log')
const engineIndex = browser.steps.findIndex(step => step.run === 'yarn test:browser:installed 2>&1 | tee refactor/.cache/ci/browser-installed.log')
assert(extraIndex > Math.max(consumerIndex, reactIndex, vueIndex) && extraIndex < engineIndex, 'Prepare installed Ambilight/Canvas after consumer probes and before browser checks')
assert(extraIndex > Math.max(consumerIndex, reactIndex, vueIndex) && extraIndex < engineIndex, 'Prepare installed Ambilight/Canvas/Document PiP after consumer probes and before browser checks')
assert(!Object.hasOwn(browser.steps[extraIndex], 'if') && browser.steps[extraIndex].run.includes('GITHUB_ENV') && browser.steps[extraIndex].run.includes('ARTPLAYER_BROWSER_ARTIFACTS='), 'Always select the additional installed browser artifact map')
assert(sourceEngineIndex > extraIndex && sourceEngineIndex < engineIndex, 'Retain complete source checks before the additional installed suite')
for (const index of [sourceEngineIndex, engineIndex]) {
+4
View File
@@ -34,6 +34,10 @@ for (const [name, mutate] of [
['wrong consumer Node version', w => w.jobs['browser-smoke'].steps.find(s => s.id === 'consumer-node-20').with['node-version'] = '24'],
['skipped installed consumer', w => w.jobs['browser-smoke'].steps.find(s => s.run?.includes('--expected-node 22.12.0')).if = 'false'],
['missing browser plugin installation', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:package --include=')).run = 'echo omitted'],
['missing Document PiP browser installation', (w) => {
const step = w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:package --include='))
step.run = step.run.replace(',artplayer-plugin-document-pip', '')
}],
['skipped browser plugin installation', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:package --include=')).if = 'false'],
['missing source browser checks', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:browser:source ')).run = 'echo omitted'],
['filtered source browser checks', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:browser:source ')).run += ' --grep subset'],
+7 -2
View File
@@ -3638,11 +3638,16 @@
"PKG-FACTORY-01",
"PKG-CANVAS-SUBTITLE-01"
],
"status": "todo",
"status": "doing",
"risk": "H",
"deliverable": "真实 video/canvas 绘制、字幕和 document PiP 恢复",
"acceptance": "最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成",
"evidence": []
"evidence": [
"changes/2026-09-14-PKG-CANVAS-05-native-pip.md",
"baselines/canvas-dpip-validation.json",
"baselines/canvas-subtitles-validation.json",
"baselines/ambilight-installed-validation.json"
]
},
{
"id": "PKG-CANVAS-06",
+2 -2
View File
@@ -7,8 +7,8 @@ mislabel source/explicit-vendor SDK tests. The caller's environment is untouched
Other deliberate fixture switches retain their existing meaning.
`yarn test:browser:installed` requires ARTPLAYER_BROWSER_ARTIFACTS from
`yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas`.
Both launcher and config validate the selected four packages' installed hashes
`yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas,artplayer-plugin-document-pip`.
Both launcher and config validate the selected five packages' installed hashes
and source/build freshness. `scope.ts` lists the currently supported installed
test files. This is a growing installed subset, not full ecosystem acceptance.
Source checks remain complete when the installed list grows.
+2 -1
View File
@@ -2,7 +2,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'
import assert from 'node:assert/strict'
export type BrowserScope = 'source' | 'installed'
export const installedPackages = ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight', 'artplayer-proxy-canvas']
export const installedPackages = ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight', 'artplayer-proxy-canvas', 'artplayer-plugin-document-pip']
export const installedTests = [
'playback.spec.js',
'lifecycle.spec.js',
@@ -14,6 +14,7 @@ export const installedTests = [
'ambilight-proxy.spec.js',
'canvas-lifecycle.spec.js',
'canvas-subtitles.spec.js',
'canvas-dpip.spec.js',
]
export function browserInvocation(scope: string, args: string[], environment: NodeJS.ProcessEnv) {
+1
View File
@@ -75,6 +75,7 @@ export async function checkPackages({ release = false, include = [] } = {}) {
const supported = {
'artplayer-plugin-ambilight': async () => (await import('../refactor/scripts/ambilight-contract.mjs')).verifyAmbilightContract(),
'artplayer-proxy-canvas': async () => (await import('../refactor/scripts/canvas-contract.mjs')).verifyCanvasContract(),
'artplayer-plugin-document-pip': async () => (await import('../refactor/scripts/dpip-contract.mjs')).verifyDpipContract(),
}
assert(new Set(include).size === include.length, 'Duplicate additional package')
const historicalFiles = new Map()
+6
View File
@@ -1,5 +1,11 @@
# Browser regression entry
`canvas-dpip.spec.js` tests actual native windows with the Canvas video and subtitle
track, continued playback, paused cue seeking and close/native-close/destroy.
Chromium/Firefox native outcomes and WebKit API-unavailable outcomes must be
counted separately. The installed scope requires five verified packages, including
Document PiP; `dpipCandidate` cannot substitute a source build for a missing map.
`canvas-subtitles.spec.js` covers native track bootstrap/load/seek/replacement and
cleanup on old/new cores, including ordinary Canvas child operations. It is part
of both browser scopes. `ARTPLAYER_CANVAS_SUBTITLE_BASELINE=1.1.0` runs the exact
+126
View File
@@ -0,0 +1,126 @@
import assert from 'node:assert/strict'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { canvasCandidate } from '../helpers/canvas.js'
import { dpipCandidate } from '../helpers/dpip.js'
import { expect, test } from './fixtures.js'
const canvas = await canvasCandidate()
const dpip = await dpipCandidate()
if (process.env.ARTPLAYER_BROWSER_ARTIFACTS)
assert(canvas.name === 'candidate-installed' && dpip.name === 'candidate-installed', 'Installed Canvas PiP requires both verified packages')
for (const core of ['published', 'candidate']) {
for (const ending of ['close', 'native-close', 'destroy']) {
test(`${core}: canvas and subtitles survive native Document PiP ${ending}`, async ({ page, context, browserName }, testInfo) => {
await page.route('**/canvas-pip.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:00:00.000 --> 00:00:03.000\nOpening caption\n\n00:00:03.000 --> 00:00:08.000\nPiP caption\n' }))
await page.goto(`/test/player.html?core=${core}`)
const supported = await page.evaluate(() => typeof window.documentPictureInPicture?.requestWindow === 'function')
const evidence = { core, ending, browserName, supported, canvas: { name: canvas.name, sha256: hash(canvas.source) }, dpip: { name: dpip.name, sha256: hash(dpip.code) }, outcome: 'incomplete' }
if (!supported) {
evidence.outcome = 'native-document-pip-unavailable'
await testInfo.attach('canvas-dpip', { contentType: 'application/json', body: JSON.stringify(evidence) })
return
}
await page.addScriptTag({ content: `(() => {
const pending = new Set();
const requestAnimationFrame = callback => { const id = window.requestAnimationFrame(time => { pending.delete(id); callback(time); }); pending.add(id); return id; };
const cancelAnimationFrame = id => { pending.delete(id); window.cancelAnimationFrame(id); };
const module = { exports: {} }; const exports = module.exports;
${canvas.source}; window.canvasFactory = module.exports.default || module.exports;
window.canvasPending = pending;
})();` })
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${dpip.code}; window.dpipFactory = module.exports.default || module.exports; })();` })
await page.evaluate(() => {
window.canvasDraws = 0
window.canvasErrors = []
window.canvasPipEvents = []
window.art = new window.Artplayer({
container: '.player',
url: '/test/pattern.mp4',
muted: true,
loop: true,
subtitle: { url: '/canvas-pip.vtt' },
proxy: window.canvasFactory((context, video) => {
window.canvasDraws++
window.canvasContext = context
window.canvasVideo = video
}),
plugins: [window.dpipFactory({ fallbackToVideoPiP: false })],
})
window.originalCanvas = window.art.video
window.originalPlayer = window.art.template.$player
window.originalParent = window.originalPlayer.parentNode
window.pip = window.art.plugins.artplayerPluginDocumentPip
window.art.on('document-pip', active => window.canvasPipEvents.push(active))
window.art.on('artplayerProxyCanvas:error', error => window.canvasErrors.push(String(error)))
document.querySelector('#play').onclick = () => window.art.play()
const open = document.createElement('button')
open.id = 'canvas-open-pip'
open.textContent = 'Open Canvas PiP'
open.onclick = () => {
window.openingCanvasPip = window.pip.open()
}
document.body.append(open)
})
try {
await expect.poll(() => page.evaluate(() => window.art.isReady && window.art.subtitle.cues.length === 2)).toBe(true)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.canvasDraws)).toBeGreaterThan(1)
const popupPromise = context.waitForEvent('page', { timeout: 8000 })
await page.click('#canvas-open-pip')
const popup = await popupPromise
await page.evaluate(() => window.openingCanvasPip)
await expect(popup.locator('canvas.art-video')).toBeVisible()
evidence.entered = await page.evaluate(() => ({
active: window.pip.isActive,
sameCanvas: window.art.video === window.originalCanvas,
playerAdopted: window.originalPlayer.ownerDocument === window.documentPictureInPicture.window.document,
videoAdopted: window.canvasVideo.ownerDocument === window.originalPlayer.ownerDocument,
trackAdopted: window.art.template.$track.ownerDocument === window.originalPlayer.ownerDocument,
time: window.art.currentTime,
draws: window.canvasDraws,
}))
expect(evidence.entered.active && evidence.entered.sameCanvas && evidence.entered.playerAdopted && evidence.entered.videoAdopted && evidence.entered.trackAdopted).toBe(true)
await expect.poll(() => page.evaluate(() => window.canvasDraws)).toBeGreaterThan(evidence.entered.draws + 2)
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(evidence.entered.time + 0.1)
await page.evaluate(() => {
window.art.pause()
window.art.currentTime = 4
})
await expect(popup.locator('.art-subtitle')).toHaveText('PiP caption')
await expect.poll(() => page.evaluate(() => window.canvasPending.size)).toBe(0)
expect(await page.evaluate(() => window.canvasContext.getImageData(0, 0, 1, 1).data[3])).toBe(255)
if (ending === 'destroy') {
await page.evaluate(() => window.art.destroy(false))
}
else {
if (ending === 'native-close')
await page.evaluate(() => window.documentPictureInPicture.window.close())
else await page.evaluate(() => window.pip.close())
await expect.poll(() => page.evaluate(() => window.pip.isActive)).toBe(false)
expect(await page.evaluate(() => window.originalPlayer.parentNode === window.originalParent && window.canvasVideo.ownerDocument === document && window.originalCanvas === window.art.video)).toBe(true)
await expect(page.locator('.art-subtitle')).toHaveText('PiP caption')
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(4.1)
await page.evaluate(() => window.art.destroy(false))
}
await expect.poll(() => page.evaluate(() => !window.documentPictureInPicture.window || window.documentPictureInPicture.window.closed)).toBe(true)
const draws = await page.evaluate(() => window.canvasDraws)
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))))
evidence.final = await page.evaluate(() => ({ active: window.pip.isActive, connected: window.canvasVideo.isConnected, paused: window.canvasVideo.paused, src: window.canvasVideo.getAttribute('src'), pending: window.canvasPending.size, draws: window.canvasDraws, placeholders: document.querySelectorAll('.artplayer-document-pip-placeholder').length, events: window.canvasPipEvents, errors: window.canvasErrors }))
expect(evidence.final).toEqual({ active: false, connected: false, paused: true, src: null, pending: 0, draws, placeholders: 0, events: ending === 'destroy' ? [true] : [true, false], errors: [] })
evidence.outcome = 'native-window-playback-subtitles-and-cleanup'
}
finally {
evidence.observed = await page.evaluate(() => ({ active: window.pip?.isActive, paused: window.canvasVideo?.paused, time: window.canvasVideo?.currentTime, draws: window.canvasDraws, subtitle: window.art?.template.$subtitle?.textContent, errors: window.canvasErrors, events: window.canvasPipEvents }))
await page.evaluate(() => {
if (!window.art.isDestroy)
window.art.destroy(false)
window.documentPictureInPicture.window?.close()
})
await testInfo.attach('canvas-dpip', { contentType: 'application/json', body: JSON.stringify(evidence) })
}
})
}
}
+9 -1
View File
@@ -8,14 +8,22 @@ import { build, transform } from 'esbuild'
import less from 'less'
import { verifyDpipContract } from '../../refactor/scripts/dpip-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
import { verifyInstalledArtifacts } from '../../scripts/installed-artifacts.mjs'
import { getEntryFile } from '../../scripts/projects.js'
export async function dpipCandidate() {
const root = fileURLToPath(new URL('../../', import.meta.url))
if (process.env.ARTPLAYER_BROWSER_ARTIFACTS) {
assert.notEqual(process.env.ARTPLAYER_DPIP_BASELINE, '1', 'Installed candidate cannot use the frozen workspace')
assert(!process.env.ARTPLAYER_DPIP_ARTIFACT, 'Installed candidate cannot override Document PiP artifact')
const { inputs } = verifyInstalledArtifacts(root, process.env.ARTPLAYER_BROWSER_ARTIFACTS, ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-document-pip'])
const input = inputs.find(item => item.name === 'artplayer-plugin-document-pip')
return { name: 'candidate-installed', code: await fs.readFile(input.file, 'utf8'), provenance: input }
}
if (process.env.ARTPLAYER_DPIP_BASELINE === '1')
return (await dpipHistorical()).find(item => item.name === 'frozen-workspace')
if (process.env.ARTPLAYER_DPIP_ARTIFACT)
return { name: 'candidate-artifact', code: await fs.readFile(process.env.ARTPLAYER_DPIP_ARTIFACT, 'utf8') }
const root = fileURLToPath(new URL('../../', import.meta.url))
const output = await build({ entryPoints: [getEntryFile(path.join(root, 'packages/artplayer-plugin-document-pip'))], bundle: true, write: false, format: 'cjs', target: 'es2020', plugins: [{
name: 'dpip-less-inline',
setup(build) {
+10 -1
View File
@@ -9,25 +9,34 @@ import { removeConsumer, runtimeConsumer } from '../scripts/package-consumer.mjs
import { emitterContracts } from './contracts/emitter.js'
import { ambilightCandidate } from './helpers/ambilight.js'
import { canvasCandidate } from './helpers/canvas.js'
import { dpipCandidate } from './helpers/dpip.js'
test('Additional browser packages cannot be mistaken for full release consumer acceptance', async () => {
await assert.rejects(checkPackages({ release: true, include: ['artplayer-plugin-ambilight'] }), /not full release acceptance/)
})
test('Explicit installed plugin maps never fall back to source or frozen workspace', async () => {
const keys = ['ARTPLAYER_BROWSER_ARTIFACTS', 'ARTPLAYER_AMBILIGHT_BASELINE', 'ARTPLAYER_CANVAS_BASELINE']
const keys = ['ARTPLAYER_BROWSER_ARTIFACTS', 'ARTPLAYER_AMBILIGHT_BASELINE', 'ARTPLAYER_CANVAS_BASELINE', 'ARTPLAYER_DPIP_BASELINE', 'ARTPLAYER_DPIP_ARTIFACT']
const previous = Object.fromEntries(keys.map(key => [key, process.env[key]]))
try {
process.env.ARTPLAYER_BROWSER_ARTIFACTS = path.resolve('refactor/.cache/absent-installed-plugin-map.json')
assert(!fs.existsSync(process.env.ARTPLAYER_BROWSER_ARTIFACTS))
delete process.env.ARTPLAYER_AMBILIGHT_BASELINE
delete process.env.ARTPLAYER_CANVAS_BASELINE
delete process.env.ARTPLAYER_DPIP_BASELINE
delete process.env.ARTPLAYER_DPIP_ARTIFACT
await assert.rejects(ambilightCandidate(), { code: 'ENOENT' })
await assert.rejects(canvasCandidate(), { code: 'ENOENT' })
await assert.rejects(dpipCandidate(), { code: 'ENOENT' })
process.env.ARTPLAYER_AMBILIGHT_BASELINE = '1'
process.env.ARTPLAYER_CANVAS_BASELINE = '1'
process.env.ARTPLAYER_DPIP_BASELINE = '1'
await assert.rejects(ambilightCandidate(), /cannot use the frozen workspace/)
await assert.rejects(canvasCandidate(), /cannot use the frozen workspace/)
await assert.rejects(dpipCandidate(), /cannot use the frozen workspace/)
delete process.env.ARTPLAYER_DPIP_BASELINE
process.env.ARTPLAYER_DPIP_ARTIFACT = 'override.js'
await assert.rejects(dpipCandidate(), /cannot override Document PiP artifact/)
}
finally {
for (const key of keys) {
+3 -3
View File
@@ -1,9 +1,9 @@
# Installed package checks
Run `yarn test:package` with the pinned Node and Yarn after a frozen install. The
default consumer scope is core and chapter. For Ambilight/Canvas browser work,
run `yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas`.
These two additional packages have reviewed published contracts and use the same
default consumer scope is core and chapter. For Ambilight/Canvas/Document PiP browser work,
run `yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas,artplayer-plugin-document-pip`.
These three additional packages have reviewed published contracts and use the same
source snapshot/build/pack/offline-install/frozen-reinstall pipeline. Installed
files are copied only after archive hash checks. The report explicitly separates
the core/chapter runtime/type scope from the additional browser package scope;