refactor(ads): [PKG-ADS-04] reconcile public types and historical imports

This commit is contained in:
Harvey Zhao committed 2026-09-12 08:25:17 +08:00
1 parent 39e2d4cdfb
commit fb1cfa701d
41 files changed
+1208 -149

No files matched your search

+14
View File
@@ -8,6 +8,20 @@ const skips = host => host.calls.filter(call => call[0] === 'emit' && call[1] ==
const clicks = host => host.calls.filter(call => call[0] === 'emit' && call[1] === 'artplayerPluginAds:click')
const contentPlays = host => host.calls.filter(call => call[0] === 'content.play')
for (const { name, factory } of implementations.filter(item => !item.historical)) {
test(`${name}: historical default and callable entry share one factory and synchronous contract`, (t) => {
const env = adsEnvironment(t)
assert.equal(factory.default, factory)
const host = env.host()
const result = factory.default({ html: 'ad' })(host.art)
assert.equal(result.name, 'artplayerPluginAds')
assert.equal(result.skip(), undefined)
host.start()
assert.equal(host.node(), undefined)
assert.equal(env.timers.size, 0)
})
}
for (const { name, factory } of implementations) {
test(`${name}: lazy synchronous factory starts once after ready and either playback event`, (t) => {
const env = adsEnvironment(t)
+66
View File
@@ -0,0 +1,66 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { expect, test } from './fixtures.js'
test('Monaco compiles Ads namespaces and executes callable and historical default factories', async ({ page }, testInfo) => {
const code = process.env.ARTPLAYER_ADS_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_ADS_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-ads', 'umd')
const declarations = Object.fromEntries(['artplayer', 'artplayer-plugin-ads'].map(name => [name, hash(fs.readFileSync(new URL(`../../docs/assets/ts/${name}.d.ts`, import.meta.url)))]))
await testInfo.attach('ads-editor-inputs', { contentType: 'application/json', body: JSON.stringify({ candidate: process.env.ARTPLAYER_ADS_ARTIFACT || 'workspace source build', sha256: hash(code), declarations }) })
await page.goto('/test/player.html?core=candidate&chapter=published')
await page.addScriptTag({ content: code })
await page.addScriptTag({ url: '/assets/js/vs/loader.js' })
const result = await page.evaluate(async () => {
window.require.config({ paths: { vs: '/assets/js/vs' } })
await new Promise((resolve, reject) => window.require(['vs/editor/editor.main'], resolve, reject))
const api = window.monaco
const types = api.languages.typescript
types.typescriptDefaults.setCompilerOptions({ target: types.ScriptTarget.ES2020, strict: true, skipLibCheck: false, noEmit: false, types: [], allowNonTsExtensions: true })
const libraries = await Promise.all(['artplayer', 'artplayer-plugin-ads'].map(async (name) => {
const uri = `file:///${name}.d.ts`
const source = await (await fetch(`/assets/ts/${name}.d.ts`)).text()
return { uri, handle: types.typescriptDefaults.addExtraLib(source, uri) }
}))
const model = api.editor.createModel(`
const adsOption: artplayerPluginAds.Option = { html: '<b>ad</b>', totalDuration: 5,
i18n: { close: 'Close', countdown: '%s seconds', detail: 'Details', canBeClosed: '%s seconds' } };
const typedFactory: (art: Artplayer) => artplayerPluginAds.Result = artplayerPluginAds(adsOption);
const defaultFactory = artplayerPluginAds.default(adsOption);
const historical: Parameters<typeof artplayerPluginAds>[0] = { html: 'ad', totalDuration: '10' };
const workspace: artplayerPluginAds.WorkspaceOption = { source: 'ad.mp4', type: 'video' };
const oldFactory = artplayerPluginAds(historical);
const workspaceFactory = artplayerPluginAds(workspace);
`, 'typescript', api.Uri.parse('file:///ads-consumer.ts'))
const invalid = api.editor.createModel('artplayerPluginAds({ totalDuration: false })', 'typescript', api.Uri.parse('file:///ads-invalid.ts'))
try {
const worker = await (await types.getTypeScriptWorker())(model.uri, invalid.uri)
const syntax = await worker.getSyntacticDiagnostics(model.uri.toString())
const semantic = await worker.getSemanticDiagnostics(model.uri.toString())
const declarations = (await Promise.all(libraries.map(lib => worker.getSemanticDiagnostics(lib.uri)))).flat()
const bad = await worker.getSemanticDiagnostics(invalid.uri.toString())
if (syntax.length || semantic.length || declarations.length)
return { syntax, semantic, declarations, invalid: bad.map(item => item.code), executed: false }
const output = await worker.getEmitOutput(model.uri.toString())
const script = output.outputFiles.find(file => file.name.endsWith('.js'))
if (!script)
throw new Error('Monaco did not emit the Ads fixture')
// eslint-disable-next-line no-new-func -- Exercise the local editor Run action with its real emitted code.
const executed = new Function(`${script.text}\nreturn [typedFactory, defaultFactory, oldFactory, workspaceFactory].every(value => typeof value === 'function') && artplayerPluginAds.default === artplayerPluginAds`)()
return { syntax, semantic, declarations, invalid: bad.map(item => item.code), executed }
}
finally {
model.dispose()
invalid.dispose()
for (const library of libraries)
library.handle.dispose()
}
})
expect(result.syntax).toEqual([])
expect(result.semantic).toEqual([])
expect(result.declarations).toEqual([])
expect(result.invalid).toEqual([2769])
expect(result.executed).toBe(true)
})
+7
View File
@@ -0,0 +1,7 @@
import type { Factory, RuntimeFactory } from 'artplayer-plugin-ads'
import factory from '../../packages/artplayer-plugin-ads/src/index'
const compatible: Factory = factory
const precise: RuntimeFactory = factory
const acceptsCompatible: typeof factory = compatible
void [compatible, precise, acceptsCompatible]
+52
View File
@@ -0,0 +1,52 @@
import type { CompatOption, Factory, LegacyOption, Option, Result, WorkspaceOption } from 'artplayer-plugin-ads'
import Artplayer from 'artplayer'
import ads from 'artplayer-plugin-ads'
import legacy from 'artplayer-plugin-ads/legacy'
import precise from 'artplayer-plugin-ads/runtime'
const option: Option = { video: 'ad.mp4', html: '<b>ad</b>', url: '/details', totalDuration: 10, muted: true, i18n: { close: 'Close', countdown: '%s seconds', detail: 'Details', canBeClosed: 'Wait %s seconds' } }
const oldPublished: LegacyOption = { html: 'ad', totalDuration: '10' }
const oldWorkspace: WorkspaceOption = { source: 'ad.mp4', type: 'video', totalDuration: 10 }
const art = new Artplayer({ container: '#player', url: 'video.mp4', plugins: [ads(), ads(option), legacy(option), precise(option)] })
ads(oldPublished)
ads(oldWorkspace)
ads.default(option)
legacy.default(option)
precise.default(option)
ads(undefined)
precise(undefined)
const result: Result = ads(option)(art)
const methods: void[] = [result.skip(), result.play(), result.pause()]
const factory: Factory = ads
const input: CompatOption = oldWorkspace
const inferredOld: Parameters<typeof ads>[0] = oldPublished
const inferredWorkspace: Parameters<typeof ads>[0] = oldWorkspace
const raw: Parameters<typeof precise>[0] = option
const numericDuration: number | undefined = option.totalDuration
function readCompatibleDuration(value: CompatOption): number | undefined {
if (value.totalDuration !== undefined && typeof value.totalDuration !== 'number')
throw new TypeError('Ads requires numeric duration')
return value.totalDuration
}
// @ts-expect-error The precise entry rejects the historical duration typo.
precise({ totalDuration: '10' })
// @ts-expect-error source/type are not implemented runtime aliases.
precise({ source: 'ad.mp4', type: 'video' })
// @ts-expect-error Durations are not boolean in either declaration family.
ads({ totalDuration: false })
// @ts-expect-error No declaration accepted a string skip threshold.
ads({ playDuration: '1' })
// @ts-expect-error HTML is text, not a DOM node.
ads({ html: document.body })
// @ts-expect-error Translation objects replace all four fields together.
ads({ i18n: { close: 'Close' } })
// @ts-expect-error The plugin methods are synchronous.
const promise: Promise<void> = result.skip()
// @ts-expect-error Plugin name retains its literal spelling.
const name: Result['name'] = 'ads'
// @ts-expect-error Historical workspace type remains a fixed union.
ads({ source: 'ad', type: 'audio' })
// @ts-expect-error Historical string durations cannot become accurate numeric options.
const wrong: Option = oldPublished
void [methods, factory, input, inferredOld, inferredWorkspace, raw, numericDuration, readCompatibleDuration, promise, name, wrong]