test: [ENG-08] add verified TS coverage and paired performance reports

This commit is contained in:
Harvey Zhao committed 2026-09-11 22:01:48 +08:00
1 parent 8ed0a5228c
commit 4a9a26d4fc
38 files changed
+24335 -61

No files matched your search

+43 -1
View File
@@ -20,10 +20,49 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
jobs:
coverage:
name: TS coverage (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
defaults:
run:
shell: bash
env:
CI: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
package-manager-cache: false
- name: Install the pinned toolchain and dependencies
run: |
npm install --global --force yarn@1.22.22
yarn install --frozen-lockfile --non-interactive
yarn check:toolchain --strict
- name: Verify source maps and critical lifecycle coverage
run: yarn test:coverage
- name: Upload coverage and source-map evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
refactor/.cache/coverage/latest.json
refactor/.cache/coverage/run-*/
include-hidden-files: true
retention-days: 14
if-no-files-found: warn
browser-smoke:
name: Browser playback smoke
runs-on: macos-latest
timeout-minutes: 20
timeout-minutes: 30
env:
CI: true
steps:
@@ -46,6 +85,8 @@ jobs:
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 three engines
run: yarn test:browser
- name: Compare installed performance and verify resource cleanup
run: yarn test:performance
- name: Upload browser evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -53,6 +94,7 @@ jobs:
name: browser-${{ github.run_id }}-${{ github.run_attempt }}
path: |
refactor/.cache/browser/
refactor/.cache/performance/
refactor/.cache/packages/latest.json
refactor/.cache/packages/run-*/*.json
refactor/.cache/packages/run-*/*.log
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),c=r(t);if("object"===s){if("object"!==c)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(c,"'"));Object.keys(e).forEach(function(r){var s=t[r],c=e[r],l=n.slice();l.push(r),i(s,c,l),a(s,c,l),o(s,c,l)})}if("array"===s){if("array"!==c)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(c,"'"));t.forEach(function(r,s){var c=t[s],l=e[s]||e[0],d=n.slice();d.push(s),i(c,l,d),a(c,l,d),o(c,l,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1";function a(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(r)}):(document.head||document.documentElement).appendChild(r)),r.textContent=e}const s=globalThis.CUSTOM_USER_AGENT??("undefined"===typeof navigator?"":navigator.userAgent),c=/^(?:(?!chrome|android).)*safari/i.test(s),l=/iPad|iPhone|iPod/i.test(s)&&("undefined"===typeof window||!window.MSStream),d=l||s.includes("Macintosh")&&"undefined"!==typeof navigator&&navigator.maxTouchPoints>=1,u=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(s)||d,p="undefined"!==typeof window&&"undefined"!==typeof document;const f={properties:["audioTracks","autoplay","buffered","controller","controlLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),c=r(t);if("object"===s){if("object"!==c)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(c,"'"));Object.keys(e).forEach(function(r){var s=t[r],c=e[r],l=n.slice();l.push(r),i(s,c,l),a(s,c,l),o(s,c,l)})}if("array"===s){if("array"!==c)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(c,"'"));t.forEach(function(r,s){var c=t[s],l=e[s]||e[0],d=n.slice();d.push(s),i(c,l,d),a(c,l,d),o(c,l,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1";function a(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(r)}):(document.head||document.documentElement).appendChild(r)),r.textContent=e}const s=globalThis.CUSTOM_USER_AGENT??("undefined"===typeof navigator?"":navigator.userAgent),c=/^(?:(?!chrome|android).)*safari/i.test(s),l=/iPad|iPhone|iPod/i.test(s)&&("undefined"===typeof window||!window.MSStream),d=l||s.includes("Macintosh")&&"undefined"!==typeof navigator&&navigator.maxTouchPoints>=1,u=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(s)||d,p="undefined"!==typeof window&&"undefined"!==typeof document;const f={properties:["audioTracks","autoplay","buffered","controller","controlLine truncated
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,r=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,a=(e,r,n)=>r in e?t(e,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[r]=n,s=(t,e)=>{for(var r in e||(e={}))o.call(e,r)&&a(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&a(t,r,e[r]);return t},l=(t,n)=>e(t,r(n)),c=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function d(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var u,p={exports:{}};var f=(u||(u=1,p.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),p.exports);const h=d(f),m="5.4.1";function v(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.apLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,r=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,a=(e,r,n)=>r in e?t(e,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[r]=n,s=(t,e)=>{for(var r in e||(e={}))o.call(e,r)&&a(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&a(t,r,e[r]);return t},l=(t,n)=>e(t,r(n)),c=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function d(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var u,p={exports:{}};var f=(u||(u=1,p.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),p.exports);const h=d(f),m="5.4.1";function v(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.apLine truncated
+9 -14
View File
@@ -1031,13 +1031,13 @@ const utils = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePropert
userAgent,
vttToBlob
}, Symbol.toStringTag, { value: "Module" }));
function noop() {
}
function listen(scope, target, name, callback, options = {}) {
const { capture = false, once = false, signal } = options;
if (scope.closed || signal?.aborted)
return () => {
};
let release = () => {
};
return noop;
let release = noop;
function listener(event) {
if (once)
release();
@@ -1061,11 +1061,9 @@ function listen(scope, target, name, callback, options = {}) {
}
function timeout(scope, callback, delay) {
if (scope.closed)
return () => {
};
return noop;
let pending2 = true;
let release = () => {
};
let release = noop;
const timer = setTimeout(() => {
if (!pending2)
return;
@@ -1081,11 +1079,9 @@ function timeout(scope, callback, delay) {
}
function animationFrame(scope, callback) {
if (scope.closed)
return () => {
};
return noop;
let pending2 = true;
let release = () => {
};
let release = noop;
const frame = requestAnimationFrame((time2) => {
if (!pending2)
return;
@@ -1113,8 +1109,7 @@ function wait(scope, delay = 0) {
return Promise.resolve(false);
return new Promise((resolve) => {
let completed = false;
let release = () => {
};
let release = noop;
const timer = setTimeout(() => {
completed = true;
release();
+6 -3
View File
@@ -39,20 +39,22 @@
"test:dash-control": "node --test test/dash-control.test.js",
"dev": "npx cross-env NODE_ENV=development node ./scripts/dev.js",
"build": "npx cross-env NODE_ENV=production node ./scripts/build.js",
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" playwright.config.js --no-fix",
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix",
"build:all": "yarn ci:build && yarn lint",
"check:toolchain": "node scripts/check-toolchain.mjs",
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" playwright.config.js --fix",
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix",
"check:plan": "node refactor/scripts/plan.mjs --check",
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js",
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js",
"test:baseline": "node --test refactor/scripts/*.test.mjs",
"ci:check": "yarn check:toolchain --strict && yarn check:plan && yarn lint && yarn check:types && yarn typecheck && yarn test",
"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/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",
"test:performance": "playwright test --config playwright.performance.config.js",
"test:browser:install": "playwright install chromium firefox webkit",
"test:browser:report": "playwright show-report refactor/.cache/browser/html",
"test:package": "node scripts/package-check.mjs",
@@ -66,6 +68,7 @@
"@playwright/test": "1.63.0",
"@vue/compiler-sfc": "3.5.28",
"@yarnpkg/lockfile": "1.1.0",
"c8": "12.0.0",
"cpy": "13.2.3",
"cross-env": "10.1.0",
"cross-spawn": "7.0.6",
+17
View File
@@ -4,6 +4,23 @@ ArtPlayer keeps its existing constructor, player mixins, plugins and DOM/CSS hoo
The production entry is `src/index.ts`. All owned core production modules use
TypeScript; libs/screenfull.js remains an audited third-party source file.
## Coverage maintenance
Run `yarn test:coverage` for the shared Node unit suite with TS source maps.
`scripts/coverage-policy.json` enrolls core and chapter, with separate gates for
scope disposal, resource adapters, instance teardown and source operations.
The report includes never-imported runtime files; type-only and vendored
exclusions are enumerated with source hashes. Browser-only paths may have low
Node coverage and still require their real browser scenarios.
`test/coverage.test.js` verifies both loaders against an unexecuted TS branch,
an unimported file and missing/stale maps. Do not bypass it or replace missing
function/branch counters with 100%. See `refactor/coverage-performance.md`.
`lifecycle/resources.ts` shares one inert cleanup for closed registrations and
pre-registration placeholders. Active releases still retain independent scope
ownership. Closed listener/timer/frame handles are safe to invoke repeatedly;
unit tests verify they cannot schedule work or call the original callbacks.
## Public declaration sources
`public/` owns the TypeScript sources for the historical package declarations.
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),c=r(t);if("object"===s){if("object"!==c)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(c,"'"));Object.keys(e).forEach(function(r){var s=t[r],c=e[r],l=n.slice();l.push(r),i(s,c,l),a(s,c,l),o(s,c,l)})}if("array"===s){if("array"!==c)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(c,"'"));t.forEach(function(r,s){var c=t[s],l=e[s]||e[0],d=n.slice();d.push(s),i(c,l,d),a(c,l,d),o(c,l,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1";function a(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(r)}):(document.head||document.documentElement).appendChild(r)),r.textContent=e}const s=globalThis.CUSTOM_USER_AGENT??("undefined"===typeof navigator?"":navigator.userAgent),c=/^(?:(?!chrome|android).)*safari/i.test(s),l=/iPad|iPhone|iPod/i.test(s)&&("undefined"===typeof window||!window.MSStream),d=l||s.includes("Macintosh")&&"undefined"!==typeof navigator&&navigator.maxTouchPoints>=1,u=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(s)||d,p="undefined"!==typeof window&&"undefined"!==typeof document;const f={properties:["audioTracks","autoplay","buffered","controller","controlLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),c=r(t);if("object"===s){if("object"!==c)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(c,"'"));Object.keys(e).forEach(function(r){var s=t[r],c=e[r],l=n.slice();l.push(r),i(s,c,l),a(s,c,l),o(s,c,l)})}if("array"===s){if("array"!==c)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(c,"'"));t.forEach(function(r,s){var c=t[s],l=e[s]||e[0],d=n.slice();d.push(s),i(c,l,d),a(c,l,d),o(c,l,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1";function a(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(r)}):(document.head||document.documentElement).appendChild(r)),r.textContent=e}const s=globalThis.CUSTOM_USER_AGENT??("undefined"===typeof navigator?"":navigator.userAgent),c=/^(?:(?!chrome|android).)*safari/i.test(s),l=/iPad|iPhone|iPod/i.test(s)&&("undefined"===typeof window||!window.MSStream),d=l||s.includes("Macintosh")&&"undefined"!==typeof navigator&&navigator.maxTouchPoints>=1,u=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(s)||d,p="undefined"!==typeof window&&"undefined"!==typeof document;const f={properties:["audioTracks","autoplay","buffered","controller","controlLine truncated
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,r=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,a=(e,r,n)=>r in e?t(e,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[r]=n,s=(t,e)=>{for(var r in e||(e={}))o.call(e,r)&&a(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&a(t,r,e[r]);return t},l=(t,n)=>e(t,r(n)),c=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function d(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var u,p={exports:{}};var f=(u||(u=1,p.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),p.exports);const h=d(f),m="5.4.1";function v(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.apLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,r=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,a=(e,r,n)=>r in e?t(e,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[r]=n,s=(t,e)=>{for(var r in e||(e={}))o.call(e,r)&&a(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&a(t,r,e[r]);return t},l=(t,n)=>e(t,r(n)),c=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function d(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var u,p={exports:{}};var f=(u||(u=1,p.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),p.exports);const h=d(f),m="5.4.1";function v(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.apLine truncated
+9 -14
View File
@@ -1031,13 +1031,13 @@ const utils = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePropert
userAgent,
vttToBlob
}, Symbol.toStringTag, { value: "Module" }));
function noop() {
}
function listen(scope, target, name, callback, options = {}) {
const { capture = false, once = false, signal } = options;
if (scope.closed || signal?.aborted)
return () => {
};
let release = () => {
};
return noop;
let release = noop;
function listener(event) {
if (once)
release();
@@ -1061,11 +1061,9 @@ function listen(scope, target, name, callback, options = {}) {
}
function timeout(scope, callback, delay) {
if (scope.closed)
return () => {
};
return noop;
let pending2 = true;
let release = () => {
};
let release = noop;
const timer = setTimeout(() => {
if (!pending2)
return;
@@ -1081,11 +1079,9 @@ function timeout(scope, callback, delay) {
}
function animationFrame(scope, callback) {
if (scope.closed)
return () => {
};
return noop;
let pending2 = true;
let release = () => {
};
let release = noop;
const frame = requestAnimationFrame((time2) => {
if (!pending2)
return;
@@ -1113,8 +1109,7 @@ function wait(scope, delay = 0) {
return Promise.resolve(false);
return new Promise((resolve) => {
let completed = false;
let release = () => {
};
let release = noop;
const timer = setTimeout(() => {
completed = true;
release();
@@ -1,10 +1,12 @@
import type ResourceScope from './scope'
function noop() {}
export function listen(scope: ResourceScope, target: EventTarget, name: string, callback: EventListenerOrEventListenerObject, options: AddEventListenerOptions = {}): () => void {
const { capture = false, once = false, signal } = options
if (scope.closed || signal?.aborted)
return () => {}
let release = () => {}
return noop
let release = noop
function listener(this: EventTarget, event: Event) {
if (once)
release()
@@ -30,9 +32,9 @@ export function listen(scope: ResourceScope, target: EventTarget, name: string,
export function timeout(scope: ResourceScope, callback: () => void, delay: number): () => void {
if (scope.closed)
return () => {}
return noop
let pending = true
let release = () => {}
let release = noop
const timer = setTimeout(() => {
if (!pending)
return
@@ -49,9 +51,9 @@ export function timeout(scope: ResourceScope, callback: () => void, delay: numbe
export function animationFrame(scope: ResourceScope, callback: FrameRequestCallback): () => void {
if (scope.closed)
return () => {}
return noop
let pending = true
let release = () => {}
let release = noop
const frame = requestAnimationFrame((time) => {
if (!pending)
return
@@ -89,7 +91,7 @@ export function wait(scope: ResourceScope, delay = 0): Promise<boolean> {
return Promise.resolve(false)
return new Promise((resolve) => {
let completed = false
let release = () => {}
let release = noop
const timer = setTimeout(() => {
completed = true
release()
+25
View File
@@ -0,0 +1,25 @@
import assert from 'node:assert/strict'
import process from 'node:process'
import { defineConfig } from '@playwright/test'
import base from './playwright.config.js'
assert(process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Performance requires ARTPLAYER_BROWSER_ARTIFACTS from yarn test:package')
export default defineConfig({
...base,
testDir: './test/performance',
timeout: 180000,
fullyParallel: false,
workers: 1,
outputDir: 'refactor/.cache/performance/browser/results',
reporter: [
['list'],
['json', { outputFile: 'refactor/.cache/performance/browser/report.json' }],
],
use: {
...base.use,
trace: 'off',
video: 'off',
screenshot: 'only-on-failure',
},
})
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,46 @@
# ENG-08 覆盖率、资源与性能报告
本任务接在 CORE-23(8ed0a522)之后。目标是让真实覆盖缺口、资源清理失败和性能/体积审查信号可重复检查,不能用工具生成成功代替核心验收。
## 实现与兼容边界
- 新增开发依赖 c8 12.0.0,固定 Yarn 1.22.22;frozen install 已通过。没有新增包运行时依赖、版本变更或公开导出。
- test:coverage 复用 test:unit 的明确用例列表。覆盖率模式生成带完整映射的文件模块,正常测试加载方式保留;按进程隔离临时模块和每次报告,禁用测试构建 tree shaking。原始 V8 收集和归一化映射分别保存,不修改 node_modules。
- sourceInventory 自动纳入未加载源码并明确排除类型文件及已审计第三方文件。关键文件门槛检查实际计数、拒绝零分母、缺失文件、忽略计数与未登记 suppression。类型排除通过 TS 编译后的空模块判断,非简单按文件名猜测。
- 资源适配器四处初始化空 cleanup 和关闭后的空释放统一为无状态内部函数,减少重复闭包分配。实际注册后的闭包和资源所有权仍独立,取消句柄重复调用安全;没有改变公开方法的返回值、事件顺序或 callback this。
- 新增容器预约/失败回滚、直接 scope 释放和聚合清理异常测试。针对新增统计规则,不降低已有门槛,也不执行不可到达回调来增加数字。
- 性能执行、输入/压缩、夹具适配、验证和摘要分离。复用 BASE-06 的同一测量体,仅适配产物 URL 与报告传输;旧冻结 fixture 和既有发布来源验证保留。新旧包相同场景各一次预热、五次正式样本,三组交替配对,资源探针在计时之后独立运行。
- 性能必须使用隔离安装包,校验分发哈希、构建 Node/依赖/脚本、当前包源码和声明来源,拒绝陈旧构建。体积比较三个相同格式入口的实际发布/候选字节,gzip9/Brotli6 沿用原设置。
- Ubuntu/Windows 覆盖率 job 和 macOS 浏览器后顺序配对均写入 Node CI,保留失败证据与摘要,SHA 固定 action、只读权限不变。本次未推送,远端 CI 结果不能由本地结果代替。
## 失败证据与修正
首轮原始 c8 报告声称生命周期行覆盖率 100%,却缺少函数/分支。实际 V8 数据与单独转换均有有效记录;问题在于 Node source-map-cache 中的 file URL 被 c8 本地路径过滤器排除。v8-to-istanbul 默认行计数仍为一,形成虚假高值。保留原始 JSON,独立转换目录归一化路径后,未执行代码和真实分支恢复统计。映射源码还必须匹配本轮源码哈希,拒绝过期/缺失映射。
真实 esbuild/Vite canary 同时验证:完整执行通过、未执行 return 行仍为零、未导入文件留在分母、未执行分支触发门槛。另有空分母、跳过计数和错误百分比检查,不能只检查命令退出码。原失败日志和首轮数据保留在本地缓存。
完整 CI 476 项通过(440 单元、11 工程、25 基线),249 个生产 TS 文件严格检查、37 个公共声明无漂移。覆盖率命令自身还先运行两项真实映射/门槛测试。当前 224 个运行时文件、26 个明确排除,行 60.94%、分支 86.55%、函数 66.71%;四个关键文件的三类指标均为 100%。这是 c8 对 Node 用例的统计,不能等同全部浏览器路径或全生态覆盖率。
## 性能信号与交接
首轮三引擎配对检查全部执行完成,54 次候选资源探针通过;现代核心 gzip 从 35486 增至 58374 字节,核心三个分发格式均超过体积审查阈值。首轮 Firefox/WebKit 个别组构造耗时也出现信号,尚未确认跨组稳定回归。ENG-PERF-01/02 保持 open,交 CORE-22/REVIEW-01 归因、优化及复核。
计时和体积状态 review-required 与资源硬失败分开保存。测试退出零只证明报告有效、配对完整和候选资源要求通过;不能据此发布或关闭风险。没有修改阈值、覆盖历史基线或将旧版定时器残留作为候选允许值。物理设备、屏幕阅读器和浏览器/GPU 原生内存仍有独立发布验收范围。
## 最终验收与提交
| 项目 | 结果 |
| --- | --- |
| 源码和类型 | 原定四个生命周期门槛通过,249 个生产 TS 严格检查,37 个声明无漂移;476 项 CI 通过 |
| 安装包兼容 | run-LF1aXT 的 34 项运行时、5 组旧类型/8 组精确类型通过,零类型阻塞 |
| 生成产物 | 按正式 build/build:i18n 重建;113 个包分发/声明及 docs 编译文件与隔离安装指纹一致;三个导入检查通过 |
| 真实浏览器 | modern 与 legacy 各 114 项资源、实例、字幕生命周期回归通过,三引擎各 38 项,没有失败、重试或跳过 |
| 配对报告 | 最终三引擎各三组配对,54 次候选资源探针通过;Chromium/Firefox 无计时信号,WebKit 第一组 core/chapter 各 +3 ms,其余两组不触发 |
| 性能审查 | 核心三格式体积增长与间歇计时信号保持 review-required;ENG-PERF-01/02 open,不作为发布放行 |
| 工程与文档 | frozen install、actionlint 和 diff 检查通过;源映射、报告规则、运行命令、CI 限制及模块职责已同步 |
[冻结证据](../baselines/quality-validation.json) 保留源码/工具/测试指纹、原始与归一化 V8 哈希、有效覆盖率、配对完整数据、隔离安装类型/运行时检查及两种格式浏览器结果。首轮失败和间歇性能信号的日志指纹单独保留,不覆盖历史。
提交前最后审查为覆盖率 job 明确 bash shell,使 Windows 的多行安装步骤也在中途原生命令失败时停止,避免仅以末条命令状态误判。只调整工作流执行约定,重新通过 actionlint;此前未提交的证据副本保存在本地缓存 quality-before-ci-review.json,最终冻结证据绑定修正后的工作流哈希,生产代码和浏览器产物未变。
ENG-08 done,59 done、155 todo。本任务独立本地 commit 包含实现、测试、文档、任务状态和生成产物;提交核实后开始 CORE-22。没有 push/publish/tag/merge。回退时整体回退本任务实现、开发依赖/锁文件、CI、测试、生成产物和文档,保留前置 CORE-23 的接口与声明成果。
+62
View File
@@ -0,0 +1,62 @@
# 覆盖率、资源与性能报告
本文由 ENG-08 维护。覆盖率和资源/性能配对链路已通过工程验收,见 [冻结证据](baselines/quality-validation.json)。报告工具通过不等于核心或发布验收通过。
## Node 单元覆盖率
使用固定 Node 24.21.0、Yarn 1.22.22,运行 `yarn test:coverage`。新增开发依赖 c8 12.0.0 仅在仓库测试中使用,不进入发布包的运行时依赖。版本由根 package.json 和 yarn.lock 固定。
执行顺序:真实映射回归测试 → package.json 中同一份 test:unit 用例列表 → Node V8 原始收集 → 源码映射检查与路径归一化 → c8 报告 → 关键文件门槛。正常 test:unit 仍使用原加载路径;覆盖率模式为每个进程生成带 inline source map 的模块,并关闭测试构建的 tree shaking,防止未使用代码从分母消失。此模式不是发布构建配置。
| 文件 | 职责 |
| --- | --- |
| scripts/coverage.mjs | 编排收集、报告和门槛,使用全新的 run-* 目录 |
| scripts/coverage-maps.mjs | 保留原始 V8 JSON;核对映射内源码与本轮源码 SHA-256;在独立目录把 file URL 转为本地路径 |
| scripts/coverage-report.mjs | 枚举真实源码,检查报告文件清单和计数,按有效计数重新计算百分比 |
| scripts/coverage-policy.json | 已纳入的包、明确排除理由及关键模块门槛 |
| test/coverage.test.js | 实际运行 esbuild/Vite 两种加载器;验证未执行分支/未导入文件、空计数、缺失/过期映射和跳过计数不能伪装通过 |
当前纳入 artplayer 与 artplayer-plugin-chapter。224 个运行时文件进入分母;25 个仅类型文件和 1 个第三方 screenfull 文件单独列出,保留内容哈希和排除理由。后续迁移生态包时扩展 packages 与对应风险门槛,不得声称已经覆盖全部 22 个包。
| 关键文件 | 行最低值 | 分支最低值 | 函数最低值 |
| --- | --- | --- | --- |
| lifecycle/scope.ts | 95% | 90% | 95% |
| lifecycle/resources.ts | 95% | 90% | 95% |
| lifecycle/instance.ts | 95% | 90% | 95% |
| source/operation.ts | 90% | 85% | 90% |
以上是原始门槛,未为通过检查降低。关键指标分母为零直接失败。总覆盖率用于显示缺口,不替代功能验证;未执行的浏览器 DOM 路径仍显示为 Node 覆盖率缺口。不能将 Node 和浏览器的用例数相加后声称覆盖率提高。
`refactor/.cache/coverage/latest.json` 指向本次报告目录。run-* 中保存 inventory.json、c8.json、report.json、原始 raw/、归一化 normalized/、测试模块 modules/ 和 HTML/JSON/LCOV 报告。原始收集文件保持不变;report.json 保存归一化前后 SHA-256。失败记录保持 failed,不自动更新冻结基线。
### 映射问题与生命周期补测
首轮报告中,生命周期文件出现 100% 行覆盖率且函数/分支分母为零。原始 V8 包含有效分支,直接转换也有有效分支;c8 在 applyCoverage 阶段按文件路径过滤,而 Node 缓存中的 sources 是 file URL,两者不一致导致有效范围被过滤,默认行计数却保留。先保留原始数据,再将 sources 归一化即可恢复。回归用例同时检查未执行 return 行仍为零,不能只断言报告生成成功。没有修改 node_modules 或绕过 c8 过滤器。
补充了容器竞争与失败回滚、直接 scope 释放、多个清理异常仍释放预约的单元用例。资源适配器重复创建的无状态空 cleanup 合并为一个内部函数;实际注册的释放句柄仍独立。封闭 scope 返回的监听器/定时器/RAF 句柄重复调用不产生晚到回调。此处不是公开方法或导出调整。
### CI
Node CI 新增 Ubuntu/Windows 覆盖率 job,验证固定工具链并运行完整覆盖率命令,失败时也上传报告及原始映射证据。权限仍为 contents:read,未引入第三方覆盖率服务。仓库尚未推送,远端两个系统的执行结果尚待验证;本机 Windows 通过不能冒充 Linux CI 结果。
## 资源与性能验收
沿用 BASE-06:至少三组同环境旧版/候选配对,交替顺序;每种配置一次预热和五次计时样本。构造/就绪/播放 Promise/销毁阈值为 max(25%, 对应绝对值 2/50/10/2 ms)。压缩采用原始字节、gzip 9、Brotli 6,匹配相同格式与入口,增长超过 max(5%, 1024 bytes) 触发审查。
计时和体积触发审查不等于资源泄漏。候选资源检查严格要求销毁后无实例、DOM、proxy 监听器、RAF、剩余定时器和晚到回调,不以旧版 BASE-PERF-01 的残留作为容许值。计时阶段不装资源探针,资源探针清理自身计时器发生在记录之后。物理设备、GPU/浏览器原生内存仍不能靠这些计数证明无泄漏。
先运行 `yarn test:package`,将 ARTPLAYER_BROWSER_ARTIFACTS 设为生成的 browser-artifacts.json,再执行 `yarn test:performance`。单独的 Playwright 配置按单 worker 运行三种引擎,关闭 trace/video,禁止与其他浏览器或重型构建任务同时计时。测试复用冻结 BASE-06 测量体,仅通过适配器修改脚本 URL 和报告提交方式,原始 fixture 不变。加载前检查包源码/声明来源、构建脚本、依赖和分发哈希,旧产物不能代替当前源码。
scripts/performance-artifacts.mjs 管输入和压缩,performance-fixture.mjs 管冻结 fixture 适配,performance-report.mjs 管配对与资源门槛,performance-summary.mjs 输出审查表;test/performance/paired.spec.js 管真实浏览器执行,test/performance-report.test.js 覆盖不完整配对、换环境、假播放、资源残留、过期源码和改动产物等失败。共享的测量验证从原 performance.mjs 提取为函数,旧发布基线仍保留完整来源校验。
报告保存于 refactor/.cache/performance/run-<engine>-*/report.json 和 summary.md;失败或未完成状态保留,Playwright JSON 与附件另存 browser/。Node CI 在安装包浏览器功能测试后顺序运行配对,追加 GitHub 摘要并上传报告。远端 macOS 执行尚未发生,本地 Windows 不替代该项环境验证。
退出零表示测量完整性、配对规则及候选资源检查通过;review-required 明确表示仍有计时或体积审查信号。该信号不自动失败工程报告生成,也绝不作为发布放行:CORE-22/REVIEW-01 必须处理风险台账。不得把报告改为 passed、抬高阈值或覆盖历史基线来隐藏增长。
### 首轮观测与待审查项
新安装包 run-LF1aXT 在三引擎各三组配对、共 54 次候选资源探针中通过严格清理检查。核心现代版 raw 134037 → 207875、gzip9 35486 → 58374、Brotli6 32533 → 53326 字节,核心 legacy/ESM 同样超过审查阈值;chapter 三格式均未超过组合阈值。已登记 ENG-PERF-01,由 CORE-22/REVIEW-01 归因、优化和审查剩余开销。
Firefox 第二组 core/chapter 构造耗时分别增加 3/5 ms,WebKit 第三组 chapter 增加 3 ms,其他组未触发。这些是首轮间歇信号,尚不能确认稳定回归;ENG-PERF-02 要求保留样本并继续配对验证,不将单组当作优化或退化结论。最终结果以本任务后续冻结证据为准。
最终收尾运行仍为三引擎各三组配对、54 次候选资源探针通过。Chromium/Firefox 没有计时信号;WebKit 第一组 core/chapter 构造各增加 3 ms,另两组未触发。体积结果与首轮相同,两项风险均保持 open。源码和测试、覆盖率计数、安装包指纹、配对原始记录、两个格式各 114 项资源/生命周期浏览器结果及验证日志哈希一并写入冻结证据,未覆盖首轮报告。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。
状态:todo 156 / doing 0 / blocked 0 / done 58 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 155 / doing 0 / blocked 0 / done 59 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -81,7 +81,7 @@
| ENG-05 | workspace<br>建立真实浏览器测试服务 | ENG-03, BASE-04, BASE-08 | Playwright projects、本地 Range/失败媒体服务;复用 docs 页面/样本的状态隔离、错误采集与候选资源映射 | Chromium/Firefox/WebKit 的基础播放 smoke 和报告可执行;以媒体状态断言判定通过,区分轻量用例与真实编辑器交互 | M | done |
| ENG-06 | workspace<br>支持按包非交互与 JS/TS 构建 | ENG-02, BASE-05 | build/dev 入口解析、指定包参数、原交互保留 | 三种产物、Less/SVG/worker 和本地 8082 demo 正常;保持 BASE-05 的 AMD 同时写入全局行为及 i18n/legacy 入口 | H | done |
| ENG-07 | workspace<br>建立 tarball 消费与产物检查 | ENG-04, ENG-06 | 隔离 npm 消费 fixtures、API/声明/入口差分 | 不借 workspace 源码通过,能识别缺文件与默认导出变化 | H | done |
| ENG-08 | workspace<br>增加覆盖率、资源与性能报告 | ENG-03, ENG-05, BASE-06 | 覆盖率基线、资源清理断言、性能报告与阈值 | 关键生命周期分支有门槛,报告不靠无意义断言堆数量;将 BASE-06 的同环境多组配对、计时/压缩审查阈值与资源异常分开接入候选,不把历史现象冻结成正常要求 | M | todo |
| ENG-08 | workspace<br>增加覆盖率、资源与性能报告 | ENG-03, ENG-05, BASE-06 | 覆盖率基线、资源清理断言、性能报告与阈值 | 关键生命周期分支有门槛,报告不靠无意义断言堆数量;将 BASE-06 的同环境多组配对、计时/压缩审查阈值与资源异常分开接入候选,不把历史现象冻结成正常要求 | M | done |
| ENG-09 | workspace<br>建立全包依赖影响和文档检查 | ENG-07, DOC-04 | 共享核心/构建影响映射、文档及每任务完成提交的 Git 审计接入;契约-支持版本-测试 ID-命令-候选/报告-任务的覆盖索引 | 核心变化触发必需生态检查;原有 DOC-01 至 04 基线例外明确,后续 done 任务不能缺失独立 commit;计划/已执行/缺证据分开,识别缺少验证归属的公开契约 | M | todo |
| ENG-10 | workspace<br>建立历史失败分级和测试可靠性规则 | ENG-03, ENG-04, ENG-05, ENG-07, BASE-07 | 历史失败 ID/环境/旧版复现/负责修复任务、逐模块门槛、受控等待与 trace/retry 规则 | 不靠全局忽略或无理由 skip 隐藏问题,新增回归阻止交付,设备缺口和偶发失败单独可见;以 risks.json 为统一差异索引;关闭必须有 resolutionEvidence/rationale,已复现、源码事实、未验证分开,登记不等于豁免 | M | done |
@@ -433,6 +433,7 @@
- ENG-05: [记录](changes/2026-09-10-ENG-05-browser-tests.md) [记录](baselines/browser-validation.json)
- ENG-06: [记录](changes/2026-09-10-ENG-06-build-development.md) [记录](build-development.md) [记录](baselines/build-validation.json)
- ENG-07: [记录](changes/2026-09-10-ENG-07-package-consumers.md) [记录](baselines/package-validation.json)
- ENG-08: [记录](coverage-performance.md) [记录](baselines/quality-validation.json) [记录](changes/2026-09-11-ENG-08-quality-reports.md)
- ENG-10: [记录](changes/2026-09-10-ENG-10-test-reliability.md) [记录](test-reliability.md)
- PILOT-01: [记录](changes/2026-09-10-PILOT-01-chapter.md) [记录](baselines/pilot-validation.json)
- CORE-01: [记录](changes/2026-09-10-CORE-01-typed-utils.md) [记录](baselines/core-utils-validation.json)
+10
View File
@@ -1,5 +1,15 @@
# 进度与证据
## 最新完成:ENG-08 覆盖率、资源与性能报告
覆盖率统计核心/chapter 的 224 个运行时文件,明确列出 26 个类型或第三方排除项。首轮发现 file URL 与路径过滤不一致,产生虚假 100% 行覆盖率/零函数分支;已通过原始 V8 数据定位并修复到独立归一化阶段,保留原始文件。真实 esbuild/Vite 映射回归覆盖未执行分支、未导入文件、缺失/过期源码和无效计数。
完整 CI 476 项、249 个生产 TS 严格检查通过;覆盖率四个关键生命周期文件通过原定门槛。新安装包 34 项运行时、5 组旧类型及 8 组精确类型通过,113 个分发/声明/示例产物哈希一致,三个导入检查通过。modern/legacy 的资源、实例和字幕生命周期组合各 114 项通过(每引擎 38 项),没有失败、重试或跳过。
最终三引擎各完成三组配对,54 次候选资源探针严格清理通过;计时/体积仍为 review-required。核心 gzip 增长和间歇构造耗时信号登记为 ENG-PERF-01/02,由 CORE-22/REVIEW-01 处理。新增覆盖率与性能 CI 配置通过 actionlint,但远端尚未运行。见 [冻结证据](baselines/quality-validation.json)、[实施记录](changes/2026-09-11-ENG-08-quality-reports.md) 和 [维护说明](coverage-performance.md)。
ENG-08 done;59 done、155 todo。本任务以独立本地 commit 交付,包含实现、测试、依赖/锁文件、CI、生成产物及文档。下一步 CORE-22 核心总验收,包含性能/体积归因与复核;报告工具通过不是发布放行。没有 push/publish/tag/merge。
## 最新完成:CORE-23 键盘、焦点与可访问名称
默认菜单、原生模式/SSR/字幕检查已完成阶段验证;最终审查另复现并修正设置入口被删除/隐藏/禁用后留下隐藏焦点,以及导航进入隐藏/inert 设置项。四项旧产物回归全部失败,修正后三浏览器设置/集成组合 45 项通过。
+2
View File
@@ -4,6 +4,8 @@
| ID | 状态 / 证据等级 | 条目 | 后续任务 |
| --- | --- | --- | --- |
| ENG-PERF-01 | open / 源码/产物事实 | Candidate core bundles exceed the BASE-06 size review threshold in all three formats | CORE-22, REVIEW-01 |
| ENG-PERF-02 | open / 待取证 | Intermittent constructor timing signals require follow-up paired measurement | CORE-22, REVIEW-01 |
| BASE-LIFE-06 | resolved / 已复现 | Published switch leaves an unhandled rejection and an unsettled promise when resume fails | CORE-09 |
| BASE-LIFE-01 | resolved / 已复现 | Superseded switch resolves along with the latest source | CORE-09 |
| BASE-LIFE-02 | resolved / 已复现 | Switch remains unsettled during the bounded observation after destroy | CORE-09 |
+36
View File
@@ -5,6 +5,42 @@
"updatedAt": "2026-09-11",
"note": "Open means closure evidence is outstanding; source-observed and unverified are not reproduced runtime bugs. A source issue already mitigated in the workspace still requires candidate regression evidence. No accepted-with-scope item is implied by baseline task completion.",
"items": [
{
"id": "ENG-PERF-01",
"title": "Candidate core bundles exceed the BASE-06 size review threshold in all three formats",
"confirmation": "source-observed",
"status": "open",
"owners": [
"CORE-22",
"REVIEW-01"
],
"evidence": [
"refactor/coverage-performance.md",
"refactor/baselines/quality-validation.json",
"refactor/changes/2026-09-11-ENG-08-quality-reports.md"
],
"compatibleResolution": "Attribute the growth to actual modules and build output, remove avoidable overhead while preserving APIs, and document any justified remaining cost against matching published entries.",
"closureCriteria": "Fresh isolated artifacts have reproducible raw/gzip9/Brotli6 results and an explicit size review; do not raise or overwrite the historical threshold to pass.",
"workspaceState": "First ENG-08 modern core measurement: 134037 to 207875 raw bytes, 35486 to 58374 gzip9, 32533 to 53326 Brotli6. Chapter remains below the combined relative/absolute review threshold. This is a review requirement, not a release approval."
},
{
"id": "ENG-PERF-02",
"title": "Intermittent constructor timing signals require follow-up paired measurement",
"confirmation": "unverified",
"status": "open",
"owners": [
"CORE-22",
"REVIEW-01"
],
"evidence": [
"refactor/coverage-performance.md",
"refactor/baselines/quality-validation.json",
"refactor/changes/2026-09-11-ENG-08-quality-reports.md"
],
"compatibleResolution": "Repeat paired measurements on the same environment, distinguish initialization work from host jitter, and optimize only after identifying a reproducible cost.",
"closureCriteria": "Explain repeat-group timing signals with retained samples and source fingerprints; do not infer a regression or improvement from a single group or expand timing allowances.",
"workspaceState": "First ENG-08 run: Firefox group 2 had core/chapter constructor delta 3/5 ms, WebKit group 3 chapter delta 3 ms, versus 2 ms absolute allowance. Other groups did not trigger. Candidate resource checks passed independently. Final run: Chromium/Firefox had no timing signals; WebKit group 1 core/chapter each increased 3 ms, with no signals in the other two groups. The intermittent review remains open."
},
{
"id": "BASE-LIFE-06",
"title": "Published switch leaves an unhandled rejection and an unsettled promise when resume fails",
+5 -2
View File
@@ -12,14 +12,17 @@ export const timingThresholds = {
destroyMs: { relative: 0.25, absoluteMs: 2 },
}
export function validatePerformance(report) {
assert.equal(report.kind, 'performance')
validatePublishedCapture(report, 'performance')
assert.equal(report.capture.media['video.mp4'], hash(fs.readFileSync(path.resolve(refactorDir, '../docs/assets/sample/video.mp4'))))
validatePerformanceMeasurements(report)
}
export function validatePerformanceMeasurements(report) {
assert.equal(report.kind, 'performance')
assert.deepEqual(report.errors, [])
assert.deepEqual(report.unhandled, [])
assert.equal(report.environment.visibility, 'visible')
assert.deepEqual(report.visibilityChanges, [], 'Background timing is not comparable')
assert.equal(report.remainingInstances, 0)
assert.equal(report.capture.media['video.mp4'], hash(fs.readFileSync(path.resolve(refactorDir, '../docs/assets/sample/video.mp4'))))
assert.deepEqual(report.samples.map(({ configuration, round, warmup }) => ({ configuration, round, warmup })), Array.from({ length: 6 }, (_, round) => ['core', 'chapter'].map(configuration => ({ configuration, round, warmup: round === 0 }))).flat())
for (const sample of report.samples) {
for (const key of [...Object.keys(timingThresholds), 'playbackObservedMs']) assert(Number.isFinite(sample[key]) && sample[key] >= 0, `Invalid ${key}`)
+6 -2
View File
@@ -594,11 +594,15 @@
"ENG-05",
"BASE-06"
],
"status": "todo",
"status": "done",
"risk": "M",
"deliverable": "覆盖率基线、资源清理断言、性能报告与阈值",
"acceptance": "关键生命周期分支有门槛,报告不靠无意义断言堆数量;将 BASE-06 的同环境多组配对、计时/压缩审查阈值与资源异常分开接入候选,不把历史现象冻结成正常要求",
"evidence": []
"evidence": [
"coverage-performance.md",
"baselines/quality-validation.json",
"changes/2026-09-11-ENG-08-quality-reports.md"
]
},
{
"id": "ENG-09",
+2
View File
@@ -1,5 +1,7 @@
# 测试与验收
核心/chapter 的 Node TS 覆盖率使用 `yarn test:coverage`。源映射防伪验证、关键生命周期门槛、报告位置及资源/性能实施状态见 [覆盖率与性能维护说明](coverage-performance.md)。
## 测试层次
浏览器验收采用连接的 @Chrome 交互检查与仓库自动回归相结合;具体操作断言、证据范围和真机边界见 [多轮复盘与 npm 准入](release-reviews.md)。浏览器已连接不代表播放器用例已通过。
+59
View File
@@ -0,0 +1,59 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const hash = value => createHash('sha256').update(value).digest('hex')
function inside(directory, file) {
const relative = path.relative(directory, file)
return relative !== '' && !relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)
}
// Keep the original V8 files intact. c8's remap filter expects filesystem paths,
// while Node's source-map cache contains file URLs (including on Windows).
export function normalizeCoverageReports(rawDirectory, mappedDirectory, { root, moduleDirectory, inventory }) {
assert(!fs.existsSync(mappedDirectory), 'Normalized coverage must use a fresh directory')
fs.mkdirSync(mappedDirectory)
const sources = new Map([...inventory.files, ...inventory.excluded].map(item => [path.resolve(root, item.file), item]))
const reports = []
let moduleCount = 0
for (const file of fs.readdirSync(rawDirectory).filter(file => file.endsWith('.json')).sort()) {
const raw = fs.readFileSync(path.join(rawDirectory, file))
const report = JSON.parse(raw)
assert(Array.isArray(report.result), `Invalid V8 report: ${file}`)
const cache = report['source-map-cache'] || {}
const modules = report.result.filter(item => item.url.startsWith('file:') && inside(moduleDirectory, fileURLToPath(item.url)))
for (const module of modules) {
const map = cache[module.url]?.data
assert(map?.version === 3 && typeof map.mappings === 'string' && map.mappings.length > 0 && Array.isArray(map.sources) && map.sources.length > 0, `Missing source map: ${module.url}`)
assert.equal(map.sourcesContent?.length, map.sources.length, `Missing source contents: ${module.url}`)
for (const [index, source] of map.sources.entries()) {
const absolute = source.startsWith('file:') ? fileURLToPath(source) : source
assert(path.isAbsolute(absolute), `Unresolved source map path: ${source}`)
const enrolled = sources.get(path.normalize(absolute))
if (enrolled) {
assert.equal(typeof map.sourcesContent[index], 'string', `Missing mapped source: ${source}`)
assert.equal(hash(map.sourcesContent[index].replaceAll('\r\n', '\n')), enrolled.sha256, `Stale mapped source: ${source}`)
}
}
moduleCount++
}
let normalizedSources = 0
for (const entry of Object.values(cache)) {
if (!entry.data?.sources)
continue
entry.data.sources = entry.data.sources.map((source) => {
if (!source.startsWith('file:'))
return source
normalizedSources++
return fileURLToPath(source)
})
}
const normalized = `${JSON.stringify(report)}\n`
fs.writeFileSync(path.join(mappedDirectory, file), normalized)
reports.push({ file, rawSha256: hash(raw), normalizedSha256: hash(normalized), modules: modules.length, normalizedSources })
}
assert(moduleCount > 0, 'No source-mapped test modules were executed')
return { modules: moduleCount, reports }
}
+13
View File
@@ -0,0 +1,13 @@
{
"schemaVersion": 1,
"packages": ["artplayer", "artplayer-plugin-chapter"],
"exclude": {
"packages/artplayer/src/libs/screenfull.js": "Vendored dependency with separate provenance and browser compatibility tests."
},
"minimum": {
"packages/artplayer/src/lifecycle/scope.ts": { "lines": 95, "branches": 90, "functions": 95 },
"packages/artplayer/src/lifecycle/resources.ts": { "lines": 95, "branches": 90, "functions": 95 },
"packages/artplayer/src/lifecycle/instance.ts": { "lines": 95, "branches": 90, "functions": 95 },
"packages/artplayer/src/source/operation.ts": { "lines": 90, "branches": 85, "functions": 90 }
}
}
+58
View File
@@ -0,0 +1,58 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import ts from 'typescript'
const walk = directory => fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => entry.isDirectory() ? walk(path.join(directory, entry.name)) : [path.join(directory, entry.name)])
const relative = (root, file) => path.relative(root, file).split(path.sep).join('/')
export function sourceInventory(root, policy) {
assert.equal(policy.schemaVersion, 1)
assert(policy.packages.length > 0)
const files = []
const excluded = []
for (const name of policy.packages) {
assert(/^artplayer(?:-[a-z-]+)?$/.test(name), `Invalid coverage package: ${name}`)
for (const file of walk(path.join(root, 'packages', name, 'src')).filter(file => /\.[jt]s$/.test(file))) {
const name = relative(root, file)
const source = fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n')
const sha256 = createHash('sha256').update(source).digest('hex')
const emitted = file.endsWith('.d.ts') ? '' : ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext } }).outputText.trim()
const reason = policy.exclude[name] || (/^(?:export\s*\{\s*\}\s*;?)?$/.test(emitted) ? 'Type-only module; no emitted runtime code.' : undefined)
if (reason) {
excluded.push({ file: name, sha256, reason })
}
else {
assert(!/\b(?:c8|v8|istanbul)\s+ignore\b/i.test(source), `Undocumented coverage suppression: ${name}`)
files.push({ file: name, sha256 })
}
}
}
for (const name of Object.keys(policy.exclude))
assert(excluded.some(item => item.file === name), `Stale coverage exclusion: ${name}`)
for (const name of Object.keys(policy.minimum))
assert(files.some(item => item.file === name), `Critical coverage file is absent or excluded: ${name}`)
return { files: files.sort((a, b) => a.file.localeCompare(b.file)), excluded }
}
export function analyzeCoverage(root, summary, inventory, policy) {
const normalized = new Map(Object.entries(summary).filter(([file]) => file !== 'total').map(([file, value]) => [relative(root, file), value]))
assert.deepEqual([...normalized.keys()].sort(), inventory.files.map(item => item.file).sort(), 'Coverage source inventory differs; a source map or zero-coverage file is missing')
const violations = []
const files = inventory.files.map((source) => {
const metrics = normalized.get(source.file)
const values = {}
for (const metric of ['lines', 'branches', 'functions', 'statements']) {
const { covered, total, skipped } = metrics[metric]
assert(Number.isInteger(total) && total >= 0 && Number.isInteger(covered) && covered >= 0 && covered <= total && skipped === 0, `Invalid or ignored ${metric}: ${source.file}`)
const percent = total ? covered * 100 / total : 100
values[metric] = { covered, total, percent }
const minimum = policy.minimum[source.file]?.[metric]
if (minimum !== undefined && (total === 0 || percent < minimum))
violations.push({ file: source.file, metric, minimum, actual: percent, total })
}
return { ...source, metrics: values }
})
return { files, excluded: inventory.excluded, minimum: policy.minimum, violations }
}
+76
View File
@@ -0,0 +1,76 @@
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { normalizeCoverageReports } from './coverage-maps.mjs'
import { analyzeCoverage, sourceInventory } from './coverage-report.mjs'
const root = fileURLToPath(new URL('../', import.meta.url))
const require = createRequire(import.meta.url)
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
const write = (file, data) => fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`)
export function runCoverage() {
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Run yarn test:coverage with the pinned Yarn')
const policy = read(path.join(root, 'scripts/coverage-policy.json'))
const inventory = sourceInventory(root, policy)
const unit = read(path.join(root, 'package.json')).scripts['test:unit']
assert(unit.startsWith('node --test '), 'Coverage expects the shared explicit Node unit test list')
const tests = unit.slice('node --test '.length).split(' ')
assert(tests.length > 0 && tests.every(file => /^test\/[a-z-]+\.test\.js$/.test(file)), 'Unsupported unit test command; update the coverage runner with it')
const cache = path.join(root, 'refactor/.cache/coverage')
fs.mkdirSync(cache, { recursive: true })
const output = fs.mkdtempSync(path.join(cache, 'run-'))
const configFile = path.join(output, 'c8.json')
const config = {
'all': true,
'exclude-after-remap': true,
'include': policy.packages.map(name => `packages/${name}/src/**`),
'exclude': inventory.excluded.map(item => item.file),
'src': policy.packages.map(name => `packages/${name}/src`),
'extension': ['.js', '.ts'],
'reporter': ['json', 'json-summary', 'text-summary', 'html', 'lcovonly'],
'reports-dir': path.join(output, 'report'),
'temp-directory': path.join(output, 'normalized'),
}
write(configFile, config)
write(path.join(output, 'inventory.json'), inventory)
const started = new Date().toISOString()
const moduleDirectory = path.join(output, 'modules')
const rawDirectory = path.join(output, 'raw')
const result = spawnSync(process.execPath, ['--test', ...tests], {
cwd: root,
stdio: 'inherit',
env: { ...process.env, NODE_V8_COVERAGE: rawDirectory, ARTPLAYER_COVERAGE_DIR: moduleDirectory },
})
const report = { task: 'ENG-08', started, node: process.version, c8: read(require.resolve('c8/package.json')).version, output: path.relative(root, output).replaceAll('\\', '/'), tests, testExitCode: result.status, status: 'failed' }
write(path.join(cache, 'latest.json'), { output: report.output })
write(path.join(output, 'report.json'), report)
if (result.error || result.status !== 0) {
throw result.error || new Error(`Coverage unit tests failed: ${result.status}`)
}
report.maps = normalizeCoverageReports(rawDirectory, config['temp-directory'], { root, moduleDirectory, inventory })
write(path.join(output, 'report.json'), report)
const rendered = spawnSync(process.execPath, [require.resolve('c8/bin/c8.js'), 'report', '--config', configFile], { cwd: root, stdio: 'inherit' })
assert(!rendered.error && rendered.status === 0, 'Coverage report generation failed')
Object.assign(report, analyzeCoverage(root, read(path.join(output, 'report/coverage-summary.json')), inventory, policy))
report.status = report.violations.length ? 'failed' : 'passed'
write(path.join(output, 'report.json'), report)
console.log(`Coverage: ${report.files.length} runtime files, ${report.excluded.length} documented/type-only exclusions; ${report.violations.length} gate violations. Report: ${report.output}/report/index.html`)
assert.deepEqual(report.violations, [], 'Critical lifecycle coverage is below the configured gate')
return report
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
try {
assert.equal(process.argv.length, 2, 'Use yarn test:coverage')
runCoverage()
}
catch (error) {
console.error(error)
process.exitCode = 1
}
}
+88
View File
@@ -0,0 +1,88 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../refactor/scripts/releases.mjs'
import { measureBytes } from '../refactor/scripts/sizes.mjs'
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
const walk = directory => fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => entry.isDirectory() ? walk(path.join(directory, entry.name)) : [path.join(directory, entry.name)])
function digest(file) {
const bytes = fs.readFileSync(file)
return hash(/\.(?:[cm]?[jt]s|json|css|less|html|svg|txt)$/.test(file) ? bytes.toString('utf8').replaceAll('\r\n', '\n') : bytes)
}
export function verifyPerformanceArtifacts(root, mapFile) {
assert(mapFile, 'Run yarn test:package and set ARTPLAYER_BROWSER_ARTIFACTS to its browser-artifacts.json')
const directory = path.dirname(path.resolve(mapFile))
const artifacts = read(mapFile)
const report = read(path.join(directory, 'report.json'))
assert.equal(report.task, 'ENG-07', 'Performance requires isolated installation evidence')
assert.equal(report.knownTypeBlockers, 0)
assert.equal(report.node, process.versions.node, 'Build and benchmark Node versions differ')
const toolchain = {}
const currentManifest = read(path.join(root, 'package.json'))
const buildManifest = read(path.join(directory, 'build/package.json'))
for (const key of ['packageManager', 'devDependencies', 'resolutions'])
assert.deepEqual(buildManifest[key], currentManifest[key], `Build toolchain changed: ${key}`)
for (const file of ['yarn.lock', 'scripts/build.js', 'scripts/utils.js', 'scripts/projects.js']) {
toolchain[file] = digest(path.join(root, file))
assert.equal(digest(path.join(directory, 'build', file)), toolchain[file], `Stale build input: ${file}`)
}
const inputs = []
for (const name of ['artplayer', 'artplayer-plugin-chapter']) {
const pkg = report.packages.find(pkg => pkg.name === name)
assert(pkg && artifacts[name], `Missing installed package: ${name}`)
const file = path.resolve(directory, artifacts[name])
const member = `package/dist/${path.basename(file)}`
assert.equal(hash(fs.readFileSync(file)), pkg.files[member], `Installed performance artifact changed: ${name}`)
const sourceHashes = {}
for (const folder of ['src', 'public']) {
const current = path.join(root, 'packages', name, folder)
const snapshot = path.join(directory, 'build/packages', name, folder)
if (!fs.existsSync(current)) {
assert(!fs.existsSync(snapshot), `Source directory was removed: ${name}/${folder}`)
continue
}
const inventory = location => Object.fromEntries(walk(location).map(file => [path.relative(location, file).replaceAll('\\', '/'), digest(file)]))
const expected = inventory(current)
assert.deepEqual(inventory(snapshot), expected, `Stale package source: ${name}/${folder}; rerun yarn test:package`)
Object.assign(sourceHashes, Object.fromEntries(Object.entries(expected).map(([file, digest]) => [`${folder}/${file}`, digest])))
}
for (const file of ['package.json', 'THIRD_PARTY_NOTICES', 'tsconfig.json']) {
const current = path.join(root, 'packages', name, file)
const snapshot = path.join(directory, 'build/packages', name, file)
assert.equal(fs.existsSync(current), fs.existsSync(snapshot), `Package input removed or added: ${name}/${file}`)
if (fs.existsSync(current)) {
sourceHashes[file] = digest(current)
assert.equal(digest(snapshot), sourceHashes[file], `Stale package input: ${name}/${file}`)
}
}
inputs.push({ name, file, member, sha256: pkg.files[member], archiveSha256: pkg.sha256, sourceHashes })
}
return { directory, inputs, toolchain, packages: report.packages }
}
export async function compareArtifactSizes(root, artifacts) {
const releases = read(path.join(root, 'refactor/baselines/releases.json')).releases
const comparisons = []
for (const pkg of artifacts.packages) {
const release = releases.find(release => release.name === pkg.name)
const archive = await ensureArchive(release)
for (const extension of ['js', 'legacy.js', 'mjs']) {
const member = `package/dist/${pkg.name}.${extension}`
assert(release.files[member] && pkg.files[member], `Missing matching size entry: ${member}`)
const before = measureBytes(readMember(archive, member))
const after = measureBytes(fs.readFileSync(path.join(artifacts.directory, 'artifacts', pkg.name, member.slice('package/'.length))))
assert.equal(before.sha256, release.files[member])
assert.equal(after.sha256, pkg.files[member])
const reviewSignals = ['raw', 'gzip9', 'brotli6'].flatMap((metric) => {
const deltaBytes = after[metric] - before[metric]
const allowanceBytes = Math.max(before[metric] * 0.05, 1024)
return deltaBytes > allowanceBytes ? [{ metric, deltaBytes, allowanceBytes }] : []
})
comparisons.push({ name: pkg.name, member, before, after, reviewSignals })
}
}
return comparisons
}
+24
View File
@@ -0,0 +1,24 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
// Reuse the frozen BASE-06 measurement body; only adapt transport and bundle URLs.
const directory = fileURLToPath(new URL('../refactor/fixtures/', import.meta.url))
function replaceOnce(source, before, after) {
assert.equal(source.split(before).length, 2, `Performance fixture adapter drift: ${before}`)
return source.replace(before, after)
}
export function performanceScript() {
const source = fs.readFileSync(`${directory}/performance.js`, 'utf8')
return replaceOnce(source, 'const response = await fetch(\'/reports/performance\', { method: \'POST\', headers: { \'Content-Type\': \'application/json\' }, body: JSON.stringify(report) })', 'window.artplayerPerformanceReport = report; const response = { ok: true }')
}
export function performanceHtml(variant) {
assert(['published', 'candidate'].includes(variant), 'Invalid performance variant')
let source = fs.readFileSync(`${directory}/performance.html`, 'utf8')
source = replaceOnce(source, '/releases/artplayer/dist/artplayer.js', `/${variant}/artplayer.js`)
source = replaceOnce(source, '/releases/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.js', `/${variant}/artplayer-plugin-chapter.js`)
source = replaceOnce(source, '/fixtures/performance.js', '/test/performance.js')
return source.replaceAll('published performance baseline', `${variant} performance comparison`).replaceAll('Published performance and resource baseline', `${variant}: performance and resource comparison`)
}
+28
View File
@@ -0,0 +1,28 @@
import assert from 'node:assert/strict'
import { comparePerformance, validatePerformanceMeasurements } from '../refactor/scripts/performance.mjs'
export function validatePairedPerformance(report) {
assert.equal(report.schemaVersion, 1)
assert(report.environment.browser && report.environment.version && report.environment.platform, 'Missing benchmark environment')
assert.deepEqual(report.runs.map(({ group, variant }) => ({ group, variant })), Array.from({ length: 3 }, (_, group) => (group % 2 ? ['candidate', 'published'] : ['published', 'candidate']).map(variant => ({ group, variant }))).flat(), 'Require three alternating paired groups')
for (const run of report.runs) {
validatePerformanceMeasurements(run.measurements)
assert.deepEqual(run.measurements.environment, report.runs[0].measurements.environment, 'Different browser/viewport/hardware cohort')
assert.deepEqual(run.measurements.scripts, [`/${run.variant}/artplayer.js`, `/${run.variant}/artplayer-plugin-chapter.js`, '/test/performance.js'], 'Unexpected benchmark scripts')
if (run.variant === 'candidate') {
for (const resource of run.measurements.resources) {
assert.deepEqual(resource.immediatelyAfter, [], 'Candidate left active timers at destroy')
assert.deepEqual(resource.after.timers, [], 'Candidate retained timers after destroy')
assert.deepEqual(resource.after.lateCallbacks, [], 'Candidate executed callbacks after destroy')
assert.equal(resource.after.lateResizeEvents, 0, 'Candidate dispatched late resize')
}
}
}
const groups = Array.from({ length: 3 }, (_, group) => {
const pair = report.runs.filter(run => run.group === group)
const before = pair.find(run => run.variant === 'published').measurements
const after = pair.find(run => run.variant === 'candidate').measurements
return { group, ...comparePerformance(before, after) }
})
return { groups, reviewRequired: groups.some(group => group.reviewSignals.length > 0) }
}
+27
View File
@@ -0,0 +1,27 @@
import { summarizePerformance } from '../refactor/scripts/performance.mjs'
export function performanceSummary(report) {
const lines = [
`## ArtPlayer performance: ${report.environment.browser} ${report.environment.version}`,
'',
`Status: **${report.status}**. Timing and size signals require review; lifecycle checks must pass independently.`,
'',
'| Group | Configuration | Metric | Published median (ms) | Candidate median (ms) | Review |',
'| --- | --- | --- | ---: | ---: | --- |',
]
for (const group of report.groups) {
const before = summarizePerformance(report.runs.find(run => run.group === group.group && run.variant === 'published').measurements)
for (const [configuration, metrics] of Object.entries(group.summary)) {
for (const [metric, result] of Object.entries(metrics)) {
const review = group.reviewSignals.some(signal => signal.configuration === configuration && signal.metric === metric)
lines.push(`| ${group.group + 1} | ${configuration} | ${metric} | ${before[configuration][metric].median.toFixed(2)} | ${result.median.toFixed(2)} | ${review ? 'Required' : '-'} |`)
}
}
}
lines.push('', '| Package entry | Raw bytes (old → new) | gzip 9 | Brotli 6 | Review |', '| --- | ---: | ---: | ---: | --- |')
for (const size of report.sizes)
lines.push(`| ${size.member} | ${size.before.raw} → ${size.after.raw} | ${size.before.gzip9} → ${size.after.gzip9} | ${size.before.brotli6} → ${size.after.brotli6} | ${size.reviewSignals.length ? 'Required' : '-'} |`)
const resources = report.runs.filter(run => run.variant === 'candidate').reduce((count, run) => count + run.measurements.resources.length, 0)
lines.push('', `${resources} candidate resource probes passed: no retained timers, late callbacks, RAF, proxy entries, instances or DOM after destruction. This does not measure native heap or GPU memory.`, '', 'One warm-up and five measured samples per configuration, per group; three alternating paired groups. Compression is per file, not HTTP transfer size.', '')
return lines.join('\n')
}
+6
View File
@@ -8,6 +8,7 @@ import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { build } from 'vite'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { performanceHtml, performanceScript } from '../../scripts/performance-fixture.mjs'
import { getEntryFile } from '../../scripts/projects.js'
import { getGlobalName, getViteBuildConfig } from '../../scripts/utils.js'
@@ -84,6 +85,7 @@ function send(req, res, bytes, filename) {
}
add('/assets/sample/video.mp4', fs.readFileSync(path.join(workspace, 'docs/assets/sample/video.mp4')), { kind: 'docs-media', file: 'docs/assets/sample/video.mp4' })
add('/test/performance.js', Buffer.from(performanceScript()), { kind: 'adapted-baseline-fixture', file: 'refactor/fixtures/performance.js' })
const server = http.createServer((req, res) => {
try {
@@ -127,6 +129,10 @@ const server = http.createServer((req, res) => {
send(req, res, Buffer.from(html), 'player.html')
return
}
if (url.pathname === '/test/performance.html') {
send(req, res, Buffer.from(performanceHtml(url.searchParams.get('variant'))), 'performance.html')
return
}
assert(!/^\/(?:test|candidate|published|compiled|uncompiled)\//.test(url.pathname), 'Unmapped test artifact')
const root = fs.realpathSync(path.join(workspace, 'docs'))
let filename = path.resolve(root, `.${decodeURIComponent(url.pathname)}`)
+126
View File
@@ -0,0 +1,126 @@
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- Exercise the real V8 collection and remapping pipeline.
import { test } from 'node:test'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { normalizeCoverageReports } from '../scripts/coverage-maps.mjs'
import { analyzeCoverage, sourceInventory } from '../scripts/coverage-report.mjs'
const root = fileURLToPath(new URL('../', import.meta.url))
const require = createRequire(import.meta.url)
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
const write = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value)}\n`)
test('real TS source maps retain unexecuted branches and files across both test loaders', () => {
const cache = path.join(root, 'refactor/.cache/coverage')
fs.mkdirSync(cache, { recursive: true })
const fixture = fs.mkdtempSync(path.join(cache, 'canary-'))
const source = path.join(fixture, 'packages/artplayer/src')
fs.mkdirSync(source, { recursive: true })
fs.writeFileSync(path.join(source, 'probe.ts'), `export default function probe(value: boolean): string {
if (value) {
return 'taken'
}
return 'missed'
}
`)
fs.writeFileSync(path.join(source, 'support.ts'), 'export default function support(): number { return 42 }\n')
fs.writeFileSync(path.join(source, 'index.ts'), 'export { default } from \'./probe\'\n')
fs.writeFileSync(path.join(source, 'never.ts'), 'export default function never(): number { return 99 }\n')
fs.writeFileSync(path.join(source, 'types.ts'), 'export interface Example { value: boolean }\n')
const entry = path.join(fixture, 'run.mjs')
const loader = pathToFileURL(path.join(root, 'test/helpers/load.js')).href
fs.writeFileSync(entry, `import assert from 'node:assert/strict'
import { loadModules, loadPackage } from ${JSON.stringify(loader)}
const { probe, support } = await loadModules({ probe: 'packages/artplayer/src/probe', support: 'packages/artplayer/src/support' }, ${JSON.stringify(fixture)})
assert.equal(support(), 42)
const packaged = (await loadPackage('artplayer', ${JSON.stringify(fixture)})).default
for (const fn of [probe, packaged]) {
assert.equal(fn(true), 'taken')
if (process.argv[2] === 'complete') assert.equal(fn(false), 'missed')
}
`)
const policy = { schemaVersion: 1, packages: ['artplayer'], exclude: {}, minimum: { 'packages/artplayer/src/probe.ts': { lines: 100, branches: 100, functions: 100 } } }
const inventory = sourceInventory(fixture, policy)
assert.deepEqual(inventory.excluded.map(item => item.file), ['packages/artplayer/src/types.ts'])
for (const mode of ['incomplete', 'complete']) {
const output = path.join(fixture, mode)
const rawDirectory = path.join(output, 'raw')
const moduleDirectory = path.join(output, 'modules')
const mappedDirectory = path.join(output, 'normalized')
const collected = spawnSync(process.execPath, [entry, mode], {
cwd: fixture,
encoding: 'utf8',
env: { ...process.env, NODE_V8_COVERAGE: rawDirectory, ARTPLAYER_COVERAGE_DIR: moduleDirectory },
})
assert.equal(collected.status, 0, collected.stderr)
const maps = normalizeCoverageReports(rawDirectory, mappedDirectory, { root: fixture, moduleDirectory, inventory })
assert(maps.modules >= 2 && maps.reports.some(item => item.normalizedSources > 0))
const config = path.join(output, 'c8.json')
write(config, {
'all': true,
'exclude-after-remap': true,
'include': ['packages/artplayer/src/**'],
'exclude': inventory.excluded.map(item => item.file),
'src': ['packages/artplayer/src'],
'extension': ['.ts'],
'reporter': ['json', 'json-summary'],
'temp-directory': mappedDirectory,
'reports-dir': path.join(output, 'report'),
})
const rendered = spawnSync(process.execPath, [require.resolve('c8/bin/c8.js'), 'report', '--config', config], { cwd: fixture, encoding: 'utf8' })
assert.equal(rendered.status, 0, rendered.stderr)
const summary = read(path.join(output, 'report/coverage-summary.json'))
const analyzed = analyzeCoverage(fixture, summary, inventory, policy)
const probe = analyzed.files.find(item => item.file.endsWith('/probe.ts'))
assert(probe.metrics.functions.total > 0 && probe.metrics.branches.total > 0)
assert.equal(analyzed.files.find(item => item.file.endsWith('/never.ts')).metrics.lines.covered, 0, 'Never imported file must remain in the denominator with zero coverage')
if (mode === 'complete') {
assert.deepEqual(analyzed.violations, [])
}
else {
assert(analyzed.violations.some(item => item.metric === 'branches'))
const full = read(path.join(output, 'report/coverage-final.json'))[path.join(source, 'probe.ts')]
const missed = Object.entries(full.statementMap).filter(([, location]) => location.start.line === 5)
assert(missed.length > 0 && missed.every(([id]) => full.s[id] === 0), 'Unexecuted TS return must not become 100% covered')
const reportFile = maps.reports.find(item => item.modules > 0).file
const original = read(path.join(rawDirectory, reportFile))
const module = original.result.find(item => item.url.startsWith(pathToFileURL(moduleDirectory).href))
const mutated = structuredClone(original)
const mapped = mutated['source-map-cache'][module.url].data
const index = mapped.sources.findIndex(item => item.endsWith('/probe.ts'))
assert(index >= 0)
mapped.sourcesContent[index] += '\n// stale source'
const corrupt = path.join(output, 'corrupt')
fs.mkdirSync(corrupt)
write(path.join(corrupt, reportFile), mutated)
assert.throws(() => normalizeCoverageReports(corrupt, path.join(output, 'stale'), { root: fixture, moduleDirectory, inventory }), /Stale mapped source/)
delete mutated['source-map-cache'][module.url]
write(path.join(corrupt, reportFile), mutated)
assert.throws(() => normalizeCoverageReports(corrupt, path.join(output, 'missing'), { root: fixture, moduleDirectory, inventory }), /Missing source map/)
}
}
})
test('coverage gates reject missing files, empty branch counts and ignored counters', () => {
const file = 'packages/artplayer/src/lifecycle/scope.ts'
const inventory = { files: [{ file, sha256: 'test' }], excluded: [] }
const policy = { minimum: { [file]: { branches: 90 } } }
const counters = () => Object.fromEntries(['lines', 'branches', 'functions', 'statements'].map(metric => [metric, { covered: 10, total: 10, skipped: 0, pct: 100 }]))
const key = path.join(root, file)
assert.deepEqual(analyzeCoverage(root, { [key]: counters() }, inventory, policy).violations, [])
assert.throws(() => analyzeCoverage(root, {}, inventory, policy), /source inventory differs/)
const empty = counters()
empty.branches = { covered: 0, total: 0, skipped: 0, pct: 100 }
assert.equal(analyzeCoverage(root, { [key]: empty }, inventory, policy).violations.length, 1)
const ignored = counters()
ignored.lines.skipped = 1
assert.throws(() => analyzeCoverage(root, { [key]: ignored }, inventory, policy), /Invalid or ignored lines/)
const low = counters()
low.branches.covered = 1
assert.equal(analyzeCoverage(root, { [key]: low }, inventory, policy).violations[0].actual, 10, 'Recompute percentage instead of trusting a stale pct field')
})
+29 -2
View File
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
import vm from 'node:vm'
import { build as esbuild } from 'esbuild'
@@ -11,7 +12,25 @@ import { getEntryFile } from '../../scripts/projects.js'
import { getGlobalName, getViteBuildConfig } from '../../scripts/utils.js'
export const workspace = fileURLToPath(new URL('../../', import.meta.url))
const importCode = code => import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
function coverageDirectory() {
const output = process.env.ARTPLAYER_COVERAGE_DIR
if (!output)
return undefined
const root = path.resolve(workspace, 'refactor/.cache/coverage')
const directory = path.resolve(output, String(process.pid))
assert(directory.startsWith(root + path.sep), 'Coverage modules must stay inside the workspace coverage cache')
fs.mkdirSync(directory, { recursive: true })
return directory
}
function importCode(code) {
const directory = coverageDirectory()
if (!directory)
return import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
const filename = path.join(directory, `${hash(code)}.mjs`)
fs.writeFileSync(filename, code)
return import(pathToFileURL(filename))
}
export function resolveSource(name, root = workspace) {
const files = ['.js', '.ts'].map(extension => path.resolve(root, name + extension)).filter(file => fs.existsSync(file))
@@ -25,7 +44,9 @@ export async function loadModules(exports, root = workspace) {
assert(/^[\w$]+$/.test(alias) && /^[\w$]+$/.test(name), 'Invalid test export')
return `export { ${name} as ${alias} } from ${JSON.stringify(resolveSource(file, root))};`
}).join('\n')
const { outputFiles } = await esbuild({ stdin: { contents, resolveDir: root }, bundle: true, write: false, platform: 'node', format: 'esm' })
const directory = coverageDirectory()
const coverage = directory ? { outfile: path.join(directory, 'modules.mjs'), sourcemap: 'inline', treeShaking: false } : {}
const { outputFiles } = await esbuild({ stdin: { contents, resolveDir: root }, bundle: true, write: false, platform: 'node', format: 'esm', ...coverage })
return importCode(outputFiles[0].contents)
}
@@ -33,6 +54,12 @@ export async function compilePackage(name, format = 'es', root = workspace) {
const project = path.join(root, 'packages', name)
const config = getViteBuildConfig({ entry: getEntryFile(project), name: getGlobalName(name), format, fileName: 'index.js', minify: false })
config.build.write = false
const directory = coverageDirectory()
if (directory) {
config.build.outDir = directory
config.build.sourcemap = 'inline'
config.build.rollupOptions.treeshake = false
}
const result = await viteBuild({ root: project, ...config })
const chunks = (Array.isArray(result) ? result : [result]).flatMap(item => item.output).filter(item => item.type === 'chunk')
assert.equal(chunks.length, 1, 'Source fixture loader requires one self-contained JS chunk')
+69 -2
View File
@@ -4,11 +4,78 @@ import { test } from 'node:test'
import { loadModules } from './helpers/load.js'
const file = 'packages/artplayer/src/lifecycle/instance'
const { beginLifecycle, destroyInstance, duringTemplateMount, finishLifecycle, getScope, urlMix } = await loadModules({
...Object.fromEntries(['beginLifecycle', 'destroyInstance', 'duringTemplateMount', 'finishLifecycle', 'getScope'].map(name => [name, { file, name }])),
const { beginLifecycle, destroyInstance, duringTemplateMount, finishLifecycle, getScope, getFinalizationScope, ownContainer, isClosing, urlMix } = await loadModules({
...Object.fromEntries(['beginLifecycle', 'destroyInstance', 'duringTemplateMount', 'finishLifecycle', 'getScope', 'getFinalizationScope', 'ownContainer', 'isClosing'].map(name => [name, { file, name }])),
urlMix: 'packages/artplayer/src/player/urlMix',
})
test('container reservations prevent concurrent mounts and release after failed rollback', () => {
const container = {}
const calls = []
const first = { isDestroy: false, reset() {}, emit() {
calls.push('destroy')
} }
const second = { isDestroy: false, reset() {}, emit() {} }
assert.throws(() => getScope(first), /has not been initialized/)
beginLifecycle(first)
beginLifecycle(second)
assert.equal(isClosing(first), false)
ownContainer(first, container, () => {
assert(isClosing(first))
assert.throws(() => ownContainer(second, container, () => {}), /multiple instances/)
calls.push('rollback')
})
assert.throws(() => ownContainer(second, container, () => {}), /multiple instances/)
destroyInstance(first, [], false, false, true)
assert.deepEqual(calls, ['destroy', 'rollback'])
ownContainer(second, container, () => {
throw new Error('Successful mounting must discard rollback')
})
assert(finishLifecycle(second))
destroyInstance(second, [], true, false)
assert.equal(finishLifecycle(second), false)
})
test('direct scope disposal finalizes retained resources once without a destroy phase', () => {
const owner = {}
const calls = []
beginLifecycle(owner)
getFinalizationScope(owner).add(() => {
calls.push('finalized')
})
getScope(owner).dispose()
assert(isClosing(owner))
assert(getFinalizationScope(owner).closed)
getScope(owner).dispose()
assert.deepEqual(calls, ['finalized'])
})
test('cleanup failures preserve the first value and still finalize, roll back and release reservations', (context) => {
const failures = [new Error('scope'), new Error('finalizer'), new Error('rollback')]
const warnings = []
context.mock.method(console, 'warn', (...args) => warnings.push(args))
const container = {}
const owner = { isDestroy: false, reset() {}, emit() {} }
beginLifecycle(owner)
getScope(owner).add(() => {
throw failures[0]
})
getFinalizationScope(owner).add(() => {
throw failures[1]
})
ownContainer(owner, container, () => {
throw failures[2]
})
assert.throws(() => destroyInstance(owner, [owner], true, false, true), error => error === failures[0])
assert.deepEqual(warnings, failures.slice(1).map(error => ['Additional ArtPlayer cleanup failure:', error]))
assert(owner.isDestroy && getScope(owner).closed && getFinalizationScope(owner).closed)
const replacement = { isDestroy: false, reset() {}, emit() {} }
beginLifecycle(replacement)
ownContainer(replacement, container, () => {})
finishLifecycle(replacement)
destroyInstance(replacement, [], true, false)
})
test('mounting template cleanup precedes destroy without exposing an early public property', () => {
const calls = []
const owner = { isDestroy: false, reset() {
+120
View File
@@ -0,0 +1,120 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- Verify evidence rejection without synthetic benchmark claims.
import { test } from 'node:test'
import { fileURLToPath } from 'node:url'
import { hash } from '../refactor/scripts/releases.mjs'
import { verifyPerformanceArtifacts } from '../scripts/performance-artifacts.mjs'
import { performanceHtml, performanceScript } from '../scripts/performance-fixture.mjs'
import { validatePairedPerformance } from '../scripts/performance-report.mjs'
const baseline = JSON.parse(fs.readFileSync(new URL('../refactor/baselines/performance.json', import.meta.url), 'utf8')).runs[0]
function fixture() {
return {
schemaVersion: 1,
environment: { browser: 'fixture', version: '1', platform: 'fixture' },
runs: Array.from({ length: 3 }, (_, group) => (group % 2 ? ['candidate', 'published'] : ['published', 'candidate']).map((variant) => {
const measurements = structuredClone(baseline)
measurements.scripts = [`/${variant}/artplayer.js`, `/${variant}/artplayer-plugin-chapter.js`, '/test/performance.js']
if (variant === 'candidate') {
for (const resource of measurements.resources) {
resource.immediatelyAfter = []
resource.after.timers = []
resource.after.lateCallbacks = []
resource.after.lateResizeEvents = 0
}
}
return { group, variant, measurements }
})).flat(),
}
}
test('paired performance requires comparable cohorts, real playback and strict candidate cleanup', () => {
assert.equal(validatePairedPerformance(fixture()).reviewRequired, false)
for (const mutate of [
report => report.runs.pop(),
report => report.runs.reverse(),
report => report.runs[1].measurements.environment.hardwareConcurrency++,
report => report.runs[1].measurements.samples[2].mediaProgress = 0,
report => report.runs[1].measurements.visibilityChanges.push('hidden'),
report => report.runs[1].measurements.scripts[0] = '/published/artplayer.js',
report => report.runs[1].measurements.resources[0].immediatelyAfter.push({ kind: 'timeout' }),
report => report.runs[1].measurements.resources[0].after.timers.push({ kind: 'interval' }),
report => report.runs[1].measurements.resources[0].after.lateCallbacks.push({ kind: 'timeout' }),
report => report.runs[1].measurements.resources[0].after.lateResizeEvents++,
]) {
const report = fixture()
mutate(report)
assert.throws(() => validatePairedPerformance(report))
}
const slow = fixture()
for (const run of slow.runs.filter(run => run.variant === 'candidate')) {
for (const sample of run.measurements.samples) {
sample.constructorMs += 20
sample.readyMs += 20
}
}
const comparison = validatePairedPerformance(slow)
assert(comparison.reviewRequired && comparison.groups.every(group => group.reviewSignals.some(signal => signal.metric === 'constructorMs')))
})
test('performance adapter changes only delivery while retaining the frozen measurement procedure', () => {
const frozen = fs.readFileSync(new URL('../refactor/fixtures/performance.js', import.meta.url), 'utf8')
const adapted = performanceScript()
const before = 'const response = await fetch(\'/reports/performance\', { method: \'POST\', headers: { \'Content-Type\': \'application/json\' }, body: JSON.stringify(report) })'
const after = 'window.artplayerPerformanceReport = report; const response = { ok: true }'
assert.equal(adapted.replace(after, before), frozen)
assert.match(performanceHtml('candidate'), /src="\/candidate\/artplayer.js"/)
assert.match(performanceHtml('published'), /src="\/published\/artplayer-plugin-chapter.js"/)
assert.throws(() => performanceHtml('../uncompiled'))
})
test('performance refuses stale sources, build tools and altered installed bundles', () => {
const cache = fileURLToPath(new URL('../refactor/.cache/performance/', import.meta.url))
fs.mkdirSync(cache, { recursive: true })
const root = fs.mkdtempSync(path.join(cache, 'guard-'))
const output = path.join(root, 'candidate')
const snapshot = path.join(output, 'build')
const write = (file, content) => {
fs.mkdirSync(path.dirname(file), { recursive: true })
fs.writeFileSync(file, content)
}
const toolchain = {
'package.json': JSON.stringify({ packageManager: 'yarn@1.22.22', devDependencies: {}, resolutions: {} }),
'yarn.lock': '# fixture\n',
'scripts/build.js': 'build fixture\n',
'scripts/utils.js': 'utils fixture\n',
'scripts/projects.js': 'projects fixture\n',
}
for (const [file, content] of Object.entries(toolchain)) {
write(path.join(root, file), content)
write(path.join(snapshot, file), content)
}
const names = ['artplayer', 'artplayer-plugin-chapter']
const packages = names.map((name) => {
const entry = `packages/${name}/src/index.ts`
write(path.join(root, entry), 'export default 1\n')
write(path.join(snapshot, entry), 'export default 1\r\n')
const manifest = `packages/${name}/package.json`
write(path.join(root, manifest), JSON.stringify({ name, version: '1.0.0' }))
write(path.join(snapshot, manifest), JSON.stringify({ name, version: '1.0.0' }))
const bytes = 'installed fixture\n'
write(path.join(output, `artifacts/${name}/dist/${name}.js`), bytes)
return { name, sha256: 'archive fixture', files: { [`package/dist/${name}.js`]: hash(bytes) } }
})
const map = path.join(output, 'browser-artifacts.json')
write(map, JSON.stringify(Object.fromEntries(names.map(name => [name, `artifacts/${name}/dist/${name}.js`]))))
write(path.join(output, 'report.json'), JSON.stringify({ task: 'ENG-07', knownTypeBlockers: 0, node: process.versions.node, packages }))
assert.equal(verifyPerformanceArtifacts(root, map).inputs.length, 2)
const source = path.join(root, 'packages/artplayer/src/index.ts')
write(source, 'export default 2\n')
assert.throws(() => verifyPerformanceArtifacts(root, map), /Stale package source/)
write(source, 'export default 1\n')
write(path.join(root, 'scripts/build.js'), 'changed compiler\n')
assert.throws(() => verifyPerformanceArtifacts(root, map), /Stale build input/)
write(path.join(root, 'scripts/build.js'), toolchain['scripts/build.js'])
write(path.join(output, 'artifacts/artplayer/dist/artplayer.js'), 'changed bundle\n')
assert.throws(() => verifyPerformanceArtifacts(root, map), /Installed performance artifact changed/)
})
+30
View File
@@ -0,0 +1,30 @@
# Paired performance and cleanup
Run `yarn test:package` first. Set `ARTPLAYER_BROWSER_ARTIFACTS` to the resulting
`refactor/.cache/packages/run-*/browser-artifacts.json`, then run
`yarn test:performance`. The command starts its own local server; do not run it
alongside other browser tests or CPU-heavy builds. All three engines run with one
worker, no trace/video recording and the same media and viewport within a pair.
Each engine runs three alternating published/candidate pairs. Each side retains
one warm-up and five timed samples for both core and core + chapter, followed by
separate instrumented resource probes. `scripts/performance-fixture.mjs` adapts
only the frozen BASE-06 fixture's script URLs and report transport. Its original
files remain unchanged and their existing baseline tests still run.
The candidate must come from an isolated package installation. Artifact hashes,
source snapshots, package inputs and build tools are checked before measurement;
an old candidate cannot silently stand in for changed source. Raw, gzip 9 and
Brotli 6 compare matching modern, legacy and ESM files from the same installation.
Timing uses the exact JS files selected by the artifact map.
Reports are in `refactor/.cache/performance/run-<engine>-*/report.json` and
`summary.md`. Playwright evidence is in `refactor/.cache/performance/browser/`.
CI uploads both and appends summaries to the workflow run. Failed or incomplete
runs remain recorded and are not promoted to a passing baseline.
A successful test proves the measurement contract and candidate resource cleanup.
`review-required` means size or timing thresholds were exceeded; it is not release
approval. CORE-22 and release reviews must resolve the corresponding risks before
publishing. These counters do not measure native heap/GPU memory, real mobile
hardware or screen-reader behavior. See `refactor/coverage-performance.md`.
+72
View File
@@ -0,0 +1,72 @@
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { compareArtifactSizes, verifyPerformanceArtifacts } from '../../scripts/performance-artifacts.mjs'
import { validatePairedPerformance } from '../../scripts/performance-report.mjs'
import { performanceSummary } from '../../scripts/performance-summary.mjs'
import { expect, test } from '../browser/fixtures.js'
const root = fileURLToPath(new URL('../../', import.meta.url))
test('installed core and chapter: three paired timing groups with strict candidate cleanup', async ({ page, browser, browserName, request, diagnostics }, testInfo) => {
const artifacts = verifyPerformanceArtifacts(root, process.env.ARTPLAYER_BROWSER_ARTIFACTS)
const manifest = await (await request.get('/test/manifest.json')).json()
for (const input of artifacts.inputs) {
const resource = manifest.resources[`/candidate/${input.name}.js`]
expect(resource.source.kind).toBe('artifact')
expect(resource.sha256).toBe(input.sha256)
}
const output = path.join(root, 'refactor/.cache/performance')
fs.mkdirSync(output, { recursive: true })
const directory = fs.mkdtempSync(path.join(output, `run-${browserName}-`))
const report = {
schemaVersion: 1,
task: 'ENG-08',
capturedAt: new Date().toISOString(),
environment: { browser: browserName, version: browser.version(), platform: process.platform, node: process.version },
inputs: artifacts.inputs,
toolchain: artifacts.toolchain,
manifest,
sizes: await compareArtifactSizes(root, artifacts),
runs: [],
status: 'incomplete',
}
const file = path.join(directory, 'report.json')
const save = () => fs.writeFileSync(file, `${JSON.stringify(report, null, 2)}\n`)
save()
try {
for (let group = 0; group < 3; group++) {
for (const variant of group % 2 ? ['candidate', 'published'] : ['published', 'candidate']) {
await page.goto(`/test/performance.html?variant=${variant}`)
await page.bringToFront()
await page.locator('#run').click()
await page.waitForFunction(() => window.artplayerPerformanceReport, null, { timeout: 25000 })
const measurements = await page.evaluate(() => window.artplayerPerformanceReport)
report.runs.push({ group, variant, measurements })
save()
}
}
Object.assign(report, validatePairedPerformance(report))
report.reviewRequired ||= report.sizes.some(item => item.reviewSignals.length > 0)
expect(diagnostics.errors).toEqual([])
expect(diagnostics.consoleErrors).toEqual([])
report.status = report.reviewRequired ? 'review-required' : 'passed'
save()
const summary = performanceSummary(report)
fs.writeFileSync(path.join(directory, 'summary.md'), summary)
await testInfo.attach('performance-summary', { body: summary, contentType: 'text/markdown' })
if (process.env.GITHUB_STEP_SUMMARY)
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary)
}
catch (error) {
report.status = 'failed'
report.failure = error.message
save()
throw error
}
finally {
await testInfo.attach('paired-performance', { path: file, contentType: 'application/json' })
process.stdout.write(`Paired performance (${browserName}): ${path.relative(root, file)}; ${report.status}\n`)
}
})
+9 -3
View File
@@ -103,7 +103,9 @@ test('timers finish or cancel independently and reject work after disposal', (co
timeout(scope, () => calls.push('disposed'), 20)
context.mock.timers.tick(5)
scope.dispose()
timeout(scope, () => calls.push('late'), 0)
const release = timeout(scope, () => calls.push('late'), 0)
release()
release()
context.mock.timers.tick(100)
assert.deepEqual(calls, ['finished'])
})
@@ -131,7 +133,9 @@ test('queued RAF is guarded after cancellation, timestamp is forwarded and compl
animationFrame(scope, time => calls.push(time))
scope.dispose()
callbacks[2](24)
animationFrame(scope, () => calls.push('late'))
const release = animationFrame(scope, () => calls.push('late'))
release()
release()
assert.deepEqual(calls, [12])
assert.deepEqual(cancelled, [1, 2, 3])
assert.equal(callbacks.length, 3)
@@ -160,7 +164,9 @@ test('DOM once reentry, receiver, object listener, external abort and captured o
controller.abort()
target.dispatchEvent(new Event('object'))
scope.dispose()
listen(scope, target, 'object', listener)
const release = listen(scope, target, 'object', listener)
release()
release()
target.dispatchEvent(new Event('object'))
assert.equal(calls, 2)
})
+161 -5
View File
@@ -239,6 +239,11 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
"@bcoe/v8-coverage@^1.0.1":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa"
integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==
"@clack/core@0.5.0":
version "0.5.0"
resolved "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz"
@@ -771,6 +776,11 @@
resolved "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz"
integrity sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==
"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.6.tgz#8dc9afa2ac1506cb1a58f89940f1c124446c8df3"
integrity sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==
"@jest/schemas@^29.6.3":
version "29.6.3"
resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz"
@@ -804,7 +814,7 @@
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
@@ -1731,6 +1741,11 @@
dependencies:
"@types/unist" "*"
"@types/istanbul-lib-coverage@^2.0.1":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==
"@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
@@ -2269,6 +2284,11 @@ ansi-regex@^6.0.1:
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz"
integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==
ansi-regex@^6.2.2:
version "6.3.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.3.0.tgz#247c8e7b70a1a43b10ce14c0226fcbf58e8815d5"
integrity sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
@@ -2281,7 +2301,7 @@ ansi-styles@^5.0.0:
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
ansi-styles@^6.1.0:
ansi-styles@^6.1.0, ansi-styles@^6.2.1:
version "6.2.3"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz"
integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==
@@ -2474,6 +2494,23 @@ byte-size@8.1.1:
resolved "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz"
integrity sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==
c8@12.0.0:
version "12.0.0"
resolved "https://registry.yarnpkg.com/c8/-/c8-12.0.0.tgz#bc69f204682651a3bbc9b3b6501f3b130c9d0b36"
integrity sha512-4zpJvrd1nKWutnnKC2pXkFmb6iM1l+ffN//o1CzlTNwW7GSOs9a1xrLqkC48nU8oEkjmPZLPiwMsIaOvoF4Pqg==
dependencies:
"@bcoe/v8-coverage" "^1.0.1"
"@istanbuljs/schema" "^0.1.3"
find-up "^5.0.0"
foreground-child "^3.1.1"
istanbul-lib-coverage "^3.2.0"
istanbul-lib-report "^3.0.1"
istanbul-reports "^3.1.6"
test-exclude "^8.0.0"
v8-to-istanbul "^9.0.0"
yargs "^18.0.0"
yargs-parser "^21.1.1"
cac@^6.7.14:
version "6.7.14"
resolved "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz"
@@ -2642,6 +2679,15 @@ cliui@^8.0.1:
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
cliui@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-9.0.1.tgz#6f7890f386f6f1f79953adc1f78dec46fcc2d291"
integrity sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==
dependencies:
string-width "^7.2.0"
strip-ansi "^7.1.0"
wrap-ansi "^9.0.0"
clone-deep@4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz"
@@ -2834,6 +2880,11 @@ conventional-recommended-bump@7.0.1:
git-semver-tags "^5.0.0"
meow "^8.1.2"
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
copy-anything@^2.0.1:
version "2.0.6"
resolved "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz"
@@ -3150,6 +3201,11 @@ emoji-regex-xs@^1.0.0:
resolved "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz"
integrity sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==
emoji-regex@^10.3.0:
version "10.6.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.6.0.tgz#bf3d6e8f7f8fd22a65d9703475bc0147357a6b0d"
integrity sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
@@ -3885,7 +3941,7 @@ follow-redirects@^1.15.11:
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz"
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
foreground-child@^3.1.0:
foreground-child@^3.1.0, foreground-child@^3.1.1:
version "3.3.1"
resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz"
integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
@@ -3964,6 +4020,11 @@ get-caller-file@^2.0.5:
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-east-asian-width@^1.0.0, get-east-asian-width@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz#216900f91df11a8b2c198c3e1d93d6c035a776b9"
integrity sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==
get-intrinsic@^1.2.6:
version "1.3.0"
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
@@ -4091,7 +4152,7 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@13.0.6:
glob@13.0.6, glob@^13.0.6:
version "13.0.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.6.tgz#078666566a425147ccacfbd2e332deb66a2be71d"
integrity sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==
@@ -4276,6 +4337,11 @@ hosted-git-info@^7.0.0, hosted-git-info@^7.0.2:
dependencies:
lru-cache "^10.0.1"
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
html-void-elements@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz"
@@ -4592,6 +4658,28 @@ isobject@^3.0.1:
resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz"
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756"
integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==
istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d"
integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==
dependencies:
istanbul-lib-coverage "^3.0.0"
make-dir "^4.0.0"
supports-color "^7.1.0"
istanbul-reports@^3.1.6:
version "3.2.0"
resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93"
integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==
dependencies:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
jackspeak@^3.1.2:
version "3.4.3"
resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz"
@@ -5047,7 +5135,7 @@ magic-string@^0.30.11, magic-string@^0.30.21:
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
make-dir@4.0.0:
make-dir@4.0.0, make-dir@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz"
integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==
@@ -7262,6 +7350,23 @@ string-width@^5.0.1, string-width@^5.1.2:
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
string-width@^7.0.0, string-width@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc"
integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==
dependencies:
emoji-regex "^10.3.0"
get-east-asian-width "^1.0.0"
strip-ansi "^7.1.0"
string-width@^8.2.1:
version "8.2.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-8.2.2.tgz#7310516493df575742fe98af6fae87d85d5ed0ac"
integrity sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==
dependencies:
get-east-asian-width "^1.5.0"
strip-ansi "^7.1.2"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
@@ -7305,6 +7410,13 @@ strip-ansi@^7.0.1:
dependencies:
ansi-regex "^6.0.1"
strip-ansi@^7.1.0, strip-ansi@^7.1.2:
version "7.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.2.0.tgz#d22a269522836a627af8d04b5c3fd2c7fa3e32e3"
integrity sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==
dependencies:
ansi-regex "^6.2.2"
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz"
@@ -7424,6 +7536,15 @@ terser@5.51.2:
commander "^2.20.0"
source-map-support "~0.5.20"
test-exclude@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-8.0.0.tgz#85891add3fa46bb822b1b1579c7f8c9a3d04ebd8"
integrity sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==
dependencies:
"@istanbuljs/schema" "^0.1.2"
glob "^13.0.6"
minimatch "^10.2.2"
text-extensions@^1.0.0:
version "1.9.0"
resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz"
@@ -7716,6 +7837,15 @@ uuid@^10.0.0:
resolved "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
v8-to-istanbul@^9.0.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175"
integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==
dependencies:
"@jridgewell/trace-mapping" "^0.3.12"
"@types/istanbul-lib-coverage" "^2.0.1"
convert-source-map "^2.0.0"
validate-npm-package-license@3.0.4, validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4:
version "3.0.4"
resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz"
@@ -7930,6 +8060,15 @@ wrap-ansi@^8.1.0:
string-width "^5.0.1"
strip-ansi "^7.0.1"
wrap-ansi@^9.0.0:
version "9.0.2"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz#956832dea9494306e6d209eb871643bb873d7c98"
integrity sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==
dependencies:
ansi-styles "^6.2.1"
string-width "^7.0.0"
strip-ansi "^7.1.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
@@ -8024,6 +8163,11 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3:
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yargs-parser@^22.0.0:
version "22.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-22.0.0.tgz#87b82094051b0567717346ecd00fd14804b357c8"
integrity sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==
yargs@17.7.2, yargs@^17.6.2:
version "17.7.2"
resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz"
@@ -8063,6 +8207,18 @@ yargs@^17.6.0:
y18n "^5.0.5"
yargs-parser "^21.1.1"
yargs@^18.0.0:
version "18.1.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-18.1.0.tgz#cd7e98c703ef51695bbbf062ed58f28e94291b56"
integrity sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==
dependencies:
cliui "^9.0.1"
escalade "^3.1.1"
get-caller-file "^2.0.5"
string-width "^8.2.1"
y18n "^5.0.5"
yargs-parser "^22.0.0"
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"