mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
test: [ENG-08] add verified TS coverage and paired performance reports
This commit is contained in:
1 parent
8ed0a5228c
commit
4a9a26d4fc
38 files changed
+24335
-61
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
Vendored
+1
-1
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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 的接口与声明成果。
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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 项通过。
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 测试与验收
|
||||
|
||||
核心/chapter 的 Node TS 覆盖率使用 `yarn test:coverage`。源映射防伪验证、关键生命周期门槛、报告位置及资源/性能实施状态见 [覆盖率与性能维护说明](coverage-performance.md)。
|
||||
|
||||
## 测试层次
|
||||
|
||||
浏览器验收采用连接的 @Chrome 交互检查与仓库自动回归相结合;具体操作断言、证据范围和真机边界见 [多轮复盘与 npm 准入](release-reviews.md)。浏览器已连接不代表播放器用例已通过。
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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`)
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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)}`)
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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/)
|
||||
})
|
||||
@@ -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`.
|
||||
@@ -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`)
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user