Files
ArtPlayer/test/site-build.test.js
T

298 lines
8.8 KiB
JavaScript

import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
// eslint-disable-next-line test/no-import-node-test -- Exercise real build outputs and failure recovery.
import test from 'node:test'
import { pathToFileURL } from 'node:url'
import vm from 'node:vm'
import ts from 'typescript'
import {
stageArtifacts,
treeFingerprint,
} from '../scripts/site-build/artifacts.ts'
import { buildLanguages, languageEntries } from '../scripts/site-build/i18n.ts'
import { loadModules } from './helpers/load.js'
const cache = path.resolve('refactor/.cache')
const outputs = ['packages/artplayer/dist/i18n', 'docs/compiled/i18n']
function fixture() {
const root = fs.mkdtempSync(path.join(cache, 'site-build-test-'))
for (const output of outputs) {
fs.mkdirSync(path.join(root, output), { recursive: true })
fs.writeFileSync(path.join(root, output, 'old.js'), 'Old complete output')
}
return root
}
function remove(root) {
assert.equal(path.dirname(fs.realpathSync(root)), cache)
assert(path.basename(root).startsWith('site-build-test-'))
fs.rmSync(root, { recursive: true, force: true })
}
function snapshot(root) {
return outputs.map(output => treeFingerprint(path.join(root, output)))
}
test('old i18n builder destroys the existing distribution before a compile failure', async () => {
const root = fixture()
try {
const source = execFileSync(
'git',
[
'show',
'078cd9ef72d8b908ef178d650fe0b8c87f867f2a:scripts/build-i18n.js',
],
{ encoding: 'utf8' },
)
const ast = ts.createSourceFile(
'old.js',
source,
ts.ScriptTarget.Latest,
true,
)
const code = ast.statements
.find(
node =>
ts.isFunctionDeclaration(node) && node.name.text === 'buildI18n',
)
.getText(ast)
const distDir = path.join(root, outputs[0])
const before = snapshot(root)
await assert.rejects(
vm.runInNewContext(`${code}; buildI18n()`, {
fs: {
...fs,
rmSync(file, options) {
assert.equal(file, distDir)
fs.rmSync(file, options)
},
},
path,
distDir,
entries: ['ar.ts'],
toPascalCase: value => value,
getViteBuildConfig: () => ({}),
viteBuild: async () => {
throw new Error('Compile failure')
},
}),
/Compile failure/,
)
assert.notEqual(snapshot(root)[0], before[0])
assert.equal(snapshot(root)[1], before[1])
}
finally {
remove(root)
}
})
test('staging isolates failed builds, rejects empty output and rolls back a later rename failure', async () => {
const root = fixture()
try {
const before = snapshot(root)
for (const mode of ['compile', 'empty', 'rename']) {
let moves = 0
await assert.rejects(
stageArtifacts(
root,
'i18n',
async (stages) => {
assert.deepEqual(snapshot(root), before)
if (mode === 'empty')
return
for (const stage of stages)
fs.writeFileSync(path.join(stage, 'new.js'), 'New output')
if (mode === 'compile')
throw new Error('Compile failure')
},
(source, target) => {
if (++moves === 4)
throw new Error('Fourth rename failed')
fs.renameSync(source, target)
},
),
)
assert.deepEqual(snapshot(root), before)
assert.deepEqual(
fs.readdirSync(path.join(root, 'refactor/.cache/site-build')),
[],
)
}
await stageArtifacts(root, 'i18n', async (stages) => {
for (const stage of stages)
fs.writeFileSync(path.join(stage, 'new.js'), 'New output')
})
for (const output of outputs)
assert.deepEqual(fs.readdirSync(path.join(root, output)), ['new.js'])
}
finally {
remove(root)
}
})
test('a competing build is rejected and an output edited during compile is preserved', async () => {
const root = fixture()
try {
await assert.rejects(
stageArtifacts(root, 'i18n', async (stages) => {
await assert.rejects(
stageArtifacts(root, 'i18n', async () => {}),
/EEXIST/,
)
for (const stage of stages)
fs.writeFileSync(path.join(stage, 'new.js'), 'New')
fs.writeFileSync(
path.join(root, outputs[0], 'manual.js'),
'Concurrent author',
)
}),
/changed while building/,
)
assert.equal(
fs.readFileSync(path.join(root, outputs[0], 'manual.js'), 'utf8'),
'Concurrent author',
)
assert.equal(
fs.readFileSync(path.join(root, outputs[1], 'old.js'), 'utf8'),
'Old complete output',
)
}
finally {
remove(root)
}
})
test('rollback conflict retains original backup instead of overwriting an external artifact edit', async () => {
const root = fixture()
try {
let moves = 0
await assert.rejects(
stageArtifacts(
root,
'i18n',
async (stages) => {
for (const stage of stages)
fs.writeFileSync(path.join(stage, 'new.js'), 'Candidate')
},
(source, target) => {
if (++moves === 4) {
fs.writeFileSync(
path.join(root, outputs[0], 'new.js'),
'Concurrent edit',
)
throw new Error('Injected later replacement failure')
}
fs.renameSync(source, target)
},
),
/retained backups/,
)
const runs = fs.readdirSync(path.join(root, 'refactor/.cache/site-build'))
assert.equal(runs.length, 1)
assert.equal(
fs.readFileSync(
path.join(
root,
'refactor/.cache/site-build',
runs[0],
'backup-0/old.js',
),
'utf8',
),
'Old complete output',
)
assert.equal(
fs.readFileSync(path.join(root, outputs[0], 'new.js'), 'utf8'),
'Concurrent edit',
)
assert.equal(
fs.readFileSync(path.join(root, outputs[1], 'old.js'), 'utf8'),
'Old complete output',
)
}
finally {
remove(root)
}
})
test('actual language builds preserve every UMD/CJS/AMD/ESM dictionary and historical window alias', async () => {
const root = fixture()
try {
const sourceDir = path.join(root, 'packages/artplayer/src/i18n')
fs.cpSync(path.resolve('packages/artplayer/src/i18n'), sourceDir, {
recursive: true,
})
const entries = languageEntries(root)
assert.equal(entries.length, 11)
await buildLanguages(root)
assert.equal(snapshot(root)[0], snapshot(root)[1])
for (const entry of entries) {
const name = path.basename(entry, '.ts')
const { language: source } = await loadModules(
{ language: `packages/artplayer/src/i18n/${name}` },
root,
)
const code = fs.readFileSync(
path.join(root, outputs[0], `${name}.js`),
'utf8',
)
const globalName = `artplayerI18n${name.replace(/(^|-)([a-z])/g, (_match, _prefix, char) => char.toUpperCase())}`
for (const mode of ['global', 'cjs', 'amd']) {
let amd
const define = (factory) => {
amd = factory()
}
define.amd = {}
const context = {
window: {},
...(mode === 'cjs' ? { exports: {}, module: { exports: {} } } : {}),
...(mode === 'amd' ? { define } : {}),
}
vm.runInNewContext(code, context)
const result
= mode === 'amd'
? amd
: mode === 'cjs'
? context.module.exports
: context[globalName]
assert.deepEqual(
JSON.parse(JSON.stringify(result)),
source,
`${name}/${mode}`,
)
assert.equal(context.window[`artplayer-i18n-${name}`], result)
}
const previousWindow = Object.getOwnPropertyDescriptor(
globalThis,
'window',
)
try {
globalThis.window = {}
const esm = await import(
pathToFileURL(path.join(root, outputs[0], `${name}.mjs`)).href,
)
assert.deepEqual(esm.default, source)
assert.equal(window[`artplayer-i18n-${name}`], esm.default)
}
finally {
if (previousWindow)
Object.defineProperty(globalThis, 'window', previousWindow)
else delete globalThis.window
}
}
fs.writeFileSync(path.join(sourceDir, 'ar.js'), 'export default {}')
assert.throws(() => languageEntries(root), /Duplicate JS\/TS/)
fs.unlinkSync(path.join(sourceDir, 'ar.js'))
fs.writeFileSync(
path.join(sourceDir, 'ar.ts'),
'export default invalid syntax',
)
const before = snapshot(root)
await assert.rejects(buildLanguages(root))
assert.deepEqual(snapshot(root), before)
}
finally {
remove(root)
}
})