mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-13 15:56:15 -08:00
refactor(core): CORE-02 type emitter maps and fix nested once dispatch
This commit is contained in:
1 parent
d3c695024a
commit
362c616998
21 files changed
+943
-72
No files matched your search
@@ -0,0 +1,28 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: emitter event names and once across nested dispatch`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
const result = await page.evaluate(() => {
|
||||
const emitter = new window.Artplayer.Emitter()
|
||||
const freshKeys = Object.keys(emitter)
|
||||
let reservedCalls = 0
|
||||
let errorName
|
||||
try {
|
||||
emitter.on('__proto__', () => reservedCalls++).emit('__proto__').off('__proto__')
|
||||
}
|
||||
catch (error) { errorName = error.name }
|
||||
let nested = false
|
||||
let onceCalls = 0
|
||||
emitter.on('value', () => {
|
||||
if (!nested) {
|
||||
nested = true
|
||||
emitter.emit('value')
|
||||
}
|
||||
}).once('value', () => onceCalls++)
|
||||
emitter.emit('value')
|
||||
return { freshKeys, reservedCalls, errorName, onceCalls }
|
||||
})
|
||||
expect(result).toEqual({ freshKeys: [], reservedCalls: core === 'published' ? 0 : 1, errorName: core === 'published' ? 'TypeError' : undefined, onceCalls: core === 'published' ? 2 : 1 })
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,51 @@
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
export const emitterContracts = {
|
||||
'EVENT.lazy-registry': function (Emitter) {
|
||||
const emitter = new Emitter()
|
||||
assert.deepEqual(Object.keys(emitter), [])
|
||||
emitter.emit('empty').off('empty')
|
||||
assert.equal(Object.getOwnPropertyDescriptor(emitter, 'e').enumerable, true)
|
||||
assert.deepEqual(Object.keys(emitter.e), [])
|
||||
assert.equal(typeof emitter.e.hasOwnProperty, 'function')
|
||||
},
|
||||
'EVENT.symbol-number-channels': function (Emitter) {
|
||||
const emitter = new Emitter()
|
||||
const symbol = Symbol('channel')
|
||||
const seen = []
|
||||
emitter.on(symbol, value => seen.push(value)).on(1, value => seen.push(value))
|
||||
emitter.emit(symbol, 'symbol').emit('1', 'numeric')
|
||||
emitter.off(symbol).off(1).emit(symbol).emit('1')
|
||||
assert.deepEqual(seen, ['symbol', 'numeric'])
|
||||
},
|
||||
'EVENT.context-removal': function (Emitter) {
|
||||
const emitter = new Emitter()
|
||||
const seen = []
|
||||
function fn() {
|
||||
seen.push(this)
|
||||
}
|
||||
const a = {}
|
||||
const b = {}
|
||||
emitter.on('value', fn, a).once('value', fn, b)
|
||||
const wrapper = emitter.e.value[1].fn
|
||||
assert.equal(wrapper._, fn)
|
||||
emitter.off('value', wrapper).emit('value')
|
||||
assert.deepEqual(seen, [a])
|
||||
emitter.on('value', fn, b).off('value', fn).emit('value')
|
||||
assert.deepEqual(seen, [a])
|
||||
},
|
||||
'EVENT.once-throw-consumed': function (Emitter) {
|
||||
const emitter = new Emitter()
|
||||
const marker = new Error('once failed')
|
||||
let calls = 0
|
||||
emitter.once('value', () => {
|
||||
calls++
|
||||
throw marker
|
||||
})
|
||||
assert.throws(() => emitter.emit('value'), error => error === marker)
|
||||
emitter.emit('value')
|
||||
assert.equal(calls, 1)
|
||||
},
|
||||
'EVENT.chain-context-arguments': function (Emitter) {
|
||||
const emitter = new Emitter()
|
||||
const context = {}
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from 'node:path'
|
||||
import { test } from 'node:test'
|
||||
import { checkFiles, publishedConsumer } from '../scripts/package-check.mjs'
|
||||
import { removeConsumer, runtimeConsumer } from '../scripts/package-consumer.mjs'
|
||||
import { emitterContracts } from './contracts/emitter.js'
|
||||
|
||||
test('Package checks reject missing files, missing wildcard exports and internal configuration', () => {
|
||||
const manifest = { name: 'fixture', main: './dist/main.js', module: './dist/main.mjs', types: './types/main.d.ts', legacy: './dist/legacy.js', exports: { '.': './dist/main.js', './lang/*': './dist/lang/*.js' } }
|
||||
@@ -19,7 +20,7 @@ test('Package checks reject missing files, missing wildcard exports and internal
|
||||
test('Actual isolated runtime rejects removed default exports and required files', async () => {
|
||||
const { dir } = await publishedConsumer()
|
||||
try {
|
||||
assert.equal(runtimeConsumer(dir).checks.length, 23)
|
||||
assert.equal(runtimeConsumer(dir).checks.length, 18 + Object.keys(emitterContracts).length)
|
||||
const esm = path.join(dir, 'node_modules/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.mjs')
|
||||
const original = fs.readFileSync(esm)
|
||||
fs.writeFileSync(esm, 'export const removedDefault = true\n')
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Run the same public contract against each actual implementation.
|
||||
import { test } from 'node:test'
|
||||
@@ -12,3 +13,37 @@ for (const { name, Artplayer } of targets) {
|
||||
for (const [id, verify] of Object.entries(emitterContracts))
|
||||
test(`${id}: ${name}`, () => verify(Artplayer.Emitter))
|
||||
}
|
||||
|
||||
test('EVENT.prototype-event-names: candidate supports names that collide with Object.prototype', () => {
|
||||
for (const name of ['__proto__', 'constructor', 'toString', 'hasOwnProperty']) {
|
||||
assert.throws(() => new targets[0].Artplayer.Emitter().on(name, () => {}), error => error.name === 'TypeError')
|
||||
for (const { Artplayer } of targets.slice(1)) {
|
||||
const emitter = new Artplayer.Emitter()
|
||||
let calls = 0
|
||||
const fn = () => calls++
|
||||
emitter.emit(name).off(name)
|
||||
const prototype = Object.getPrototypeOf(emitter.e)
|
||||
emitter.on(name, fn).once(name, fn)
|
||||
emitter.emit(name).emit(name).off(name, fn).emit(name)
|
||||
assert.equal(calls, 3)
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(emitter.e, name), false)
|
||||
assert.equal(Object.getPrototypeOf(emitter.e), prototype)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('EVENT.once-nested-snapshot: candidate consumes a once registration across nested dispatch snapshots', () => {
|
||||
for (const [index, { Artplayer }] of targets.entries()) {
|
||||
const emitter = new Artplayer.Emitter()
|
||||
let nested = false
|
||||
let calls = 0
|
||||
emitter.on('value', () => {
|
||||
if (!nested) {
|
||||
nested = true
|
||||
emitter.emit('value')
|
||||
}
|
||||
}).once('value', () => calls++)
|
||||
emitter.emit('value')
|
||||
assert.equal(calls, index === 0 ? 2 : 1)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { Events } from 'artplayer'
|
||||
import Emitter from '../../packages/artplayer/src/utils/emitter'
|
||||
|
||||
const symbol = Symbol('message')
|
||||
interface Messages {
|
||||
ready: []
|
||||
value: [count: number, label?: string]
|
||||
readonly: readonly [value: number]
|
||||
[symbol]: [message: string]
|
||||
}
|
||||
class TypedEmitter extends Emitter<Messages> {
|
||||
count = 0
|
||||
}
|
||||
const events = new TypedEmitter()
|
||||
const context = { total: 0 }
|
||||
const chain: TypedEmitter = events.on('value', function (count, label) {
|
||||
this.total += count
|
||||
label?.toUpperCase()
|
||||
}, context).once('ready', () => {}).emit('value', 1).off('ready')
|
||||
events.emit(symbol, 'message')
|
||||
events.emit('readonly', 1)
|
||||
const playerEvents = new Emitter<Events & Record<string, unknown[]>>()
|
||||
playerEvents.emit('setBar', 'loaded', 0.5)
|
||||
playerEvents.emit('custom:event', { extra: true })
|
||||
// @ts-expect-error Known events retain their payload types with open custom names.
|
||||
playerEvents.emit('setBar', 'invalid', 0.5)
|
||||
// @ts-expect-error A declared map rejects unknown names.
|
||||
events.emit('missing')
|
||||
// @ts-expect-error Payload types are correlated with the event name.
|
||||
events.emit('value', 'one')
|
||||
// @ts-expect-error Required payloads cannot be omitted.
|
||||
events.emit('value')
|
||||
// @ts-expect-error Empty events reject extra values.
|
||||
events.emit('ready', 1)
|
||||
// @ts-expect-error Listener arguments must match the event.
|
||||
events.on('value', (count: string) => count)
|
||||
// @ts-expect-error Explicit receiver types must agree with the provided context.
|
||||
events.on('ready', contextListener, { total: 'zero' })
|
||||
function contextListener(this: { total: number }) {
|
||||
return this.total
|
||||
}
|
||||
void chain
|
||||
Reference in new issue
Block a user