mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
test(packages): ENG-07 verify installed tarball contracts and browsers
This commit is contained in:
1 parent
12b047f9a6
commit
a0286c4e95
17 files changed
+702
-9
No files matched your search
@@ -0,0 +1,35 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
// eslint-disable-next-line test/no-import-node-test -- This fixture exercises the repository's Node test runner.
|
||||
import { test } from 'node:test'
|
||||
import { checkFiles, publishedConsumer } from '../scripts/package-check.mjs'
|
||||
import { removeConsumer, runtimeConsumer } from '../scripts/package-consumer.mjs'
|
||||
|
||||
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' } }
|
||||
const files = ['package/dist/main.js', 'package/dist/main.mjs', 'package/types/main.d.ts', 'package/dist/legacy.js', 'package/dist/lang/fr.js']
|
||||
checkFiles(manifest, files)
|
||||
for (const missing of files)
|
||||
assert.throws(() => checkFiles(manifest, files.filter(file => file !== missing)), /Missing/)
|
||||
assert.throws(() => checkFiles(manifest, [...files, 'package/tsconfig.json']), /tsconfig/)
|
||||
assert.throws(() => checkFiles(manifest, files, ['package/dist/old.js']), /Historical/)
|
||||
})
|
||||
|
||||
test('Actual isolated runtime rejects removed default exports and required files', async () => {
|
||||
const { dir } = await publishedConsumer()
|
||||
try {
|
||||
assert.equal(runtimeConsumer(dir).checks.length, 23)
|
||||
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')
|
||||
assert.throws(() => runtimeConsumer(dir), /Command failed/)
|
||||
fs.writeFileSync(esm, original)
|
||||
const entry = path.join(dir, 'node_modules/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.js')
|
||||
fs.unlinkSync(entry)
|
||||
assert.throws(() => runtimeConsumer(dir), /Command failed/)
|
||||
}
|
||||
finally {
|
||||
removeConsumer(dir)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
# Installed package checks
|
||||
|
||||
Run `yarn test:package` with the pinned Node and Yarn after a frozen install. The
|
||||
initial scope is core and chapter; extend `names` in `scripts/package-consumer.mjs`
|
||||
and add package-specific consumers when migrating another package.
|
||||
|
||||
`scripts/package-check.mjs` copies source into an ignored build snapshot, rebuilds
|
||||
all three formats and core languages using repository scripts, and runs Yarn pack.
|
||||
It checks manifest targets, historical distribution files, archive members and
|
||||
unexpected source/configuration files. It then installs the tarballs in a fresh
|
||||
OS temporary directory outside the repository, using the root lock to resolve
|
||||
runtime dependencies offline, and repeats installation with a frozen consumer lock.
|
||||
Install hooks are disabled; packages with such hooks are rejected until covered.
|
||||
|
||||
Only the build snapshot links workspace build tools. The consumer has no workspace
|
||||
package links, `NODE_PATH` is cleared, installed files must match archive hashes,
|
||||
and compiler inputs may only resolve inside that consumer or the pinned compiler's
|
||||
standard library. Temporary consumers are cleaned up after success or failure.
|
||||
|
||||
`runtime.cjs` tests CJS, ESM, UMD, AMD, legacy and language entrypoints, SSR import,
|
||||
export restrictions and shared Emitter behavior. Public property descriptors and
|
||||
default configuration are compared against the fixed published core/chapter.
|
||||
This is a targeted compatibility check, not a complete behavioral API comparison.
|
||||
|
||||
Public, chapter-options, language and legacy-plugin TypeScript consumers run with
|
||||
TS 5.9.3 (Node10, NodeNext CJS/ESM and Bundler) and TS 4.3.5 (Node10). Exact known
|
||||
diagnostics are referenced by `known-types.json` and `test/types/known-diagnostics.json`.
|
||||
Unexpected errors and unexpectedly removed errors both require investigation.
|
||||
Remove a known case when its owning task fixes it; keep frozen release evidence intact.
|
||||
`yarn test:package:release` additionally rejects any remaining known type errors.
|
||||
Passing this command alone is not authorization or sufficient evidence to publish.
|
||||
|
||||
Reports, archives, installed artifact copies and build/install logs are under
|
||||
`refactor/.cache/packages/run-*`; `latest.json` points to the last successful
|
||||
compatibility run (a strict release rejection still leaves its report). For browser
|
||||
validation set `ARTPLAYER_BROWSER_ARTIFACTS` to that run's `browser-artifacts.json`
|
||||
and run `yarn test:browser`. The service then uses installed package bytes without
|
||||
falling back to source. CI performs this sequence and uploads only reports and
|
||||
artifacts, excluding the build snapshot's node_modules link.
|
||||
|
||||
`test/package-check.test.js` verifies missing targets, leaked configuration and
|
||||
removed historical files, then deliberately removes an actual published default
|
||||
export and required entry in an isolated consumer to prove runtime failures are
|
||||
detected. It is part of `yarn test:node`.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"note": "Exact published diagnostic references, not release waivers. Remove a case only with a verified fix; never edit the frozen baseline to accept a candidate regression.",
|
||||
"cases": [
|
||||
{ "mode": "node10-commonjs", "fixture": "legacy-plugin.ts", "risk": "BASE-TYPE-03" },
|
||||
{ "mode": "nodenext-esm", "fixture": "legacy-plugin.ts", "risk": "BASE-TYPE-01" },
|
||||
{ "mode": "nodenext-esm", "fixture": "language.ts", "risk": "BASE-TYPE-01" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const process = require('node:process')
|
||||
const { pathToFileURL } = require('node:url')
|
||||
|
||||
const vm = require('node:vm');
|
||||
|
||||
(async () => {
|
||||
const checks = []
|
||||
const observations = {}
|
||||
const check = (id, value) => {
|
||||
assert(value, id)
|
||||
checks.push(id)
|
||||
}
|
||||
const root = __dirname
|
||||
const load = relative => fs.readFileSync(path.join(root, 'node_modules', relative), 'utf8')
|
||||
const core = require('artplayer')
|
||||
const chapter = require('artplayer-plugin-chapter')
|
||||
check('DIST.cjs', typeof core === 'function' && core.version === JSON.parse(fs.readFileSync(path.join(root, 'expected.json'))).version && typeof chapter({}) === 'function')
|
||||
check('DIST.cjs-legacy', require('artplayer/legacy').version === core.version && typeof require('artplayer-plugin-chapter/legacy')({}) === 'function')
|
||||
const esm = await import('artplayer')
|
||||
const pluginEsm = await import('artplayer-plugin-chapter')
|
||||
check('DIST.esm', esm.default.version === core.version && typeof pluginEsm.default({}) === 'function')
|
||||
check('DIST.esm-default-only', Object.keys(esm).join() === 'default' && Object.keys(pluginEsm).join() === 'default')
|
||||
const legacy = await import('artplayer/legacy')
|
||||
const pluginLegacy = await import('artplayer-plugin-chapter/legacy')
|
||||
check('DIST.esm-legacy', legacy.default.version === core.version && typeof pluginLegacy.default({}) === 'function')
|
||||
for (const [name, filename, globalName] of [
|
||||
['core', 'artplayer/dist/artplayer.js', 'Artplayer'],
|
||||
['core-legacy', 'artplayer/dist/artplayer.legacy.js', 'Artplayer'],
|
||||
['chapter', 'artplayer-plugin-chapter/dist/artplayer-plugin-chapter.js', 'artplayerPluginChapter'],
|
||||
['chapter-legacy', 'artplayer-plugin-chapter/dist/artplayer-plugin-chapter.legacy.js', 'artplayerPluginChapter'],
|
||||
]) {
|
||||
const code = load(filename)
|
||||
const global = vm.createContext({ console })
|
||||
vm.runInContext(code, global, { timeout: 2000 })
|
||||
check(`DIST.global-${name}`, typeof global[globalName] === 'function')
|
||||
let value
|
||||
let calls = 0
|
||||
const define = (factory) => {
|
||||
calls++
|
||||
value = factory()
|
||||
}
|
||||
define.amd = {}
|
||||
const amd = vm.createContext({ console, define })
|
||||
vm.runInContext(code, amd, { timeout: 2000 })
|
||||
check(`DIST.amd-${name}`, calls === 1 && typeof value === 'function' && amd[globalName] === value)
|
||||
}
|
||||
check('SSR.import-template', core.html.includes('art-video-player') && esm.default.html === core.html && typeof core.STYLE === 'string' && core.instances.length === 0)
|
||||
observations.constructorErrors = []
|
||||
for (const Class of [core, esm.default, legacy.default]) {
|
||||
for (const useSSR of [false, true]) {
|
||||
let error
|
||||
try {
|
||||
Reflect.construct(Class, [{ container: '#server', url: 'video.mp4', useSSR }])
|
||||
}
|
||||
catch (caught) { error = caught }
|
||||
assert.equal(error?.message, 'Artplayer can only be used in the browser environment')
|
||||
observations.constructorErrors.push({ useSSR, name: error.name, message: error.message })
|
||||
}
|
||||
}
|
||||
check('SSR.constructor-browser-only', observations.constructorErrors.length === 6)
|
||||
observations.languages = []
|
||||
const languages = fs.readdirSync(path.join(root, 'node_modules/artplayer/dist/i18n')).filter(file => file.endsWith('.mjs')).map(file => file.slice(0, -4)).sort()
|
||||
for (const name of languages) {
|
||||
const cjs = require(`artplayer/i18n/${name}`)
|
||||
const module = await import(`artplayer/i18n/${name}`)
|
||||
assert.deepEqual(module.default, cjs)
|
||||
const globalName = `artplayerI18n${name.replace(/(^|-)([a-z])/g, (_, _prefix, char) => char.toUpperCase())}`
|
||||
const context = vm.createContext({})
|
||||
vm.runInContext(load(`artplayer/dist/i18n/${name}.js`), context, { timeout: 2000 })
|
||||
assert.equal(JSON.stringify(context[globalName]), JSON.stringify(cjs))
|
||||
observations.languages.push({ name, globalName, keys: Object.keys(cjs).sort(), play: cjs.Play })
|
||||
}
|
||||
check('DIST.i18n-cjs-esm-global', languages.length > 0)
|
||||
observations.blockedDeepImports = []
|
||||
for (const specifier of ['artplayer/dist/artplayer.js', 'artplayer/types/artplayer.d.ts', 'artplayer-plugin-chapter/dist/artplayer-plugin-chapter.js']) {
|
||||
let code
|
||||
try {
|
||||
require.resolve(specifier)
|
||||
}
|
||||
catch (error) { code = error.code }
|
||||
assert.equal(code, 'ERR_PACKAGE_PATH_NOT_EXPORTED')
|
||||
observations.blockedDeepImports.push({ specifier, code })
|
||||
}
|
||||
check('DIST.exports-boundary', observations.blockedDeepImports.length === 3)
|
||||
observations.resolutions = {}
|
||||
for (const specifier of ['artplayer', 'artplayer/legacy', 'artplayer-plugin-chapter', 'artplayer-plugin-chapter/legacy']) {
|
||||
const resolved = require.resolve(specifier)
|
||||
assert(resolved.startsWith(path.join(root, 'node_modules') + path.sep), 'Escaped isolated packages')
|
||||
observations.resolutions[specifier] = path.relative(root, resolved).replaceAll('\\', '/')
|
||||
}
|
||||
// CDN-style absolute URLs bypass package exports; verify the actual files remain readable imports.
|
||||
const direct = await import(pathToFileURL(path.join(root, 'node_modules/artplayer/dist/artplayer.mjs')).href)
|
||||
check('DIST.direct-esm-file', direct.default === esm.default)
|
||||
const { emitterContracts } = await import('./emitter.mjs')
|
||||
for (const [id, run] of Object.entries(emitterContracts)) {
|
||||
run(core.Emitter)
|
||||
checks.push(id)
|
||||
}
|
||||
const shape = value => Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(value)).map(([key, d]) => [key, { enumerable: d.enumerable, configurable: d.configurable, writable: d.writable, get: typeof d.get, set: typeof d.set, value: typeof d.value }]))
|
||||
observations.api = { static: shape(core), prototype: shape(core.prototype), emitter: shape(core.Emitter.prototype), factory: shape(chapter), defaults: JSON.parse(JSON.stringify(core.option, (_, value) => typeof value === 'function' ? '$function' : value)) }
|
||||
process.stdout.write(JSON.stringify({ checks, observations }))
|
||||
})().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
Reference in new issue
Block a user