import assert from 'node:assert/strict' import process from 'node:process' // eslint-disable-next-line test/no-import-node-test -- Control actual timers without wall-clock sleeps. import { test } from 'node:test' import { loadCoreArtifact, loadPackage, loadPublishedCore } from './helpers/load.js' const globals = { setTimeout: (...args) => setTimeout(...args), clearTimeout: timer => clearTimeout(timer) } const published = await loadPublishedCore(globals) const current = { name: 'workspace', Artplayer: (await loadPackage('artplayer')).default } const targets = [published, current] if (process.env.ARTPLAYER_TEST_CORE) targets.push(await loadCoreArtifact(process.env.ARTPLAYER_TEST_CORE, globals)) for (const { name, Artplayer } of targets) { const utils = Artplayer.utils test(`utils formatting and extension conventions: ${name}`, () => { assert.deepEqual([utils.clamp(4, 5, 1), utils.clamp(-2, 5, 1), utils.clamp(9, 5, 1)], [4, 1, 5]) assert(Number.isNaN(utils.clamp(Number.NaN, 0, 1))) for (const [value, result] of [[0, '00:00'], [59.9, '00:59'], [60, '01:00'], [3601, '01:00:01'], [360000, '100:00:00']]) assert.equal(utils.secondToTime(value), result) const text = `'Text'` assert.equal(utils.escape(text), '<b title="A&B">'Text'</b>') assert.equal(utils.unescape(utils.escape(text)), text) assert.equal(utils.unescape('&lt; <'), '< <') assert.equal(utils.capitalize('hello'), 'Hello') for (const [url, result] of [[' clip.MP4?token=a.b#hash ', 'mp4'], ['clip.webm#x?y=z', 'webm'], [' video ', 'video'], ['', '']]) assert.equal(utils.getExt(url), result) }) test(`utils property descriptors and deep merge array/reference behavior: ${name}`, () => { const key = Symbol('key') const object = Object.create({ inherited: true }) assert.equal(utils.def(object, key, { value: 42 }), object) assert.equal(utils.has(object, key), true) assert.equal(utils.has(object, 'inherited'), false) assert.equal(utils.get(object, 'missing'), undefined) assert.deepEqual({ ...utils.get(object, key) }, { value: 42, writable: false, enumerable: false, configurable: false }) const first = { nested: { x: 1 }, array: [1] } const array = [2, [3, 4]] const result = utils.mergeDeep(first, { nested: { y: 2 }, array }) assert.equal(JSON.stringify(result), '{"nested":{"x":1,"y":2},"array":[1,2,3,4]}') assert.equal(first.array.length, 1) assert.equal(utils.mergeDeep({ array }).array, array) assert.equal(utils.has(utils.mergeDeep(), 'toString'), false) }) test(`utils subtitle text output: ${name}`, () => { const srt = '1\n00:00:01,2 --> 00:00:02.3456\n{i}Hello{/i} {unknown}\n' assert.equal(utils.srtToVtt(srt), 'WEBVTT \r\n\r\n1\n00:00:01.200 --> 00:00:02.345\nHello \n\r\n\r\n') const ass = '[Events]\nDialogue: 0,0:00:01.20,0:00:02.34,Default,,0,0,0,,{\\i1} Hello\\N World\n' assert.equal(utils.assToVtt(ass), 'WEBVTT\n\n1\n00:00:01.200 --> 00:00:02.340\nHello\nWorld') assert.equal(utils.assToVtt('[Script Info]\nno dialogue'), 'WEBVTT\n\n') }) test(`utils errors retain identity and truthy return values: ${name}`, () => { const object = {} assert.equal(utils.errorHandle(object, 'unused'), object) assert.equal(utils.errorHandle('supported', 'unused'), 'supported') for (const condition of [false, 0, '', null, undefined]) { assert.throws(() => utils.errorHandle(condition, 'Failure'), error => error instanceof utils.ArtPlayerError && error.name === 'ArtPlayerError' && error.message === 'Failure') } }) test(`utils timer arguments, receivers and leading/trailing behavior: ${name}`, async (context) => { context.mock.timers.enable({ apis: ['setTimeout'] }) let slept = false const pending = utils.sleep(10).then(() => slept = true) context.mock.timers.tick(9) await Promise.resolve() assert.equal(slept, false) context.mock.timers.tick(1) await pending const calls = [] function receive(value) { calls.push([this.id, value]) return 42 } const trailing = utils.debounce(receive, 10) assert.equal(trailing.call({ id: 'first' }, 1), undefined) context.mock.timers.tick(5) trailing.call({ id: 'last' }, 2) context.mock.timers.tick(9) assert.deepEqual(calls, []) context.mock.timers.tick(1) assert.deepEqual(calls, [['last', 2]]) const leading = utils.throttle(receive, 10) assert.equal(leading.call({ id: 'leading' }, 3), undefined) leading.call({ id: 'ignored' }, 4) context.mock.timers.tick(10) assert.deepEqual(calls, [['last', 2], ['leading', 3]]) leading.call({ id: 'later' }, 5) assert.deepEqual(calls, [['last', 2], ['leading', 3], ['later', 5]]) }) } test('utils export surface preserves the published namespace plus existing workspace additions', () => { const expected = [...Object.keys(published.Artplayer.utils), 'silencePromise', 'getSafeAreaInsets'].sort() for (const { Artplayer } of targets.slice(1)) { assert.deepEqual(Object.keys(Artplayer.utils).sort(), expected) for (const key of Object.keys(published.Artplayer.utils)) { assert.equal(typeof Artplayer.utils[key], typeof published.Artplayer.utils[key], key) if (typeof Artplayer.utils[key] === 'function') assert.equal(Artplayer.utils[key].length, published.Artplayer.utils[key].length, key) } } }) test('mergeDeep keeps __proto__ as own data without replacing result prototypes', () => { const input = JSON.parse('{"__proto__":{"unexpected":true},"constructor":{"prototype":{"marker":true}}}') const old = published.Artplayer.utils.mergeDeep({}, input) assert.equal(Object.prototype.hasOwnProperty.call(old, '__proto__'), false) assert.equal(old.unexpected, true, 'Published defect remains demonstrable') for (const { Artplayer } of targets.slice(1)) { const result = Artplayer.utils.mergeDeep({}, input) assert.equal(Object.prototype.hasOwnProperty.call(result, '__proto__'), true) assert.equal(result.unexpected, undefined) assert.equal(Object.getPrototypeOf(result).unexpected, undefined) assert.equal(Object.getOwnPropertyDescriptor(result, '__proto__').value.unexpected, true) const nested = Artplayer.utils.mergeDeep({ nested: {} }, { nested: input }) assert.equal(nested.nested.unexpected, undefined) assert.equal(Object.prototype.hasOwnProperty.call(nested.nested, '__proto__'), true) assert.equal({}.unexpected, undefined) assert.equal(JSON.stringify(input), '{"__proto__":{"unexpected":true},"constructor":{"prototype":{"marker":true}}}') } }) test('throttle preserves synchronous reentry and retries after callback failure', (context) => { context.mock.timers.enable({ apis: ['setTimeout'] }) for (const { Artplayer } of targets) { let calls = 0 const invoke = Artplayer.utils.throttle(() => { calls++ if (calls === 1) invoke() }, 10) invoke() assert.equal(calls, 2) const broken = Artplayer.utils.throttle(() => { throw new Error('callback failure') }, 10) assert.throws(broken, /callback failure/) assert.throws(broken, /callback failure/) } })