mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-12 15:26:16 -08:00
refactor(docs): [SITE-SMOKE-01] verify example readiness and cleanup
This commit is contained in:
1 parent
d74ecc43cc
commit
1a5440a67f
25 files changed
+3510
-4770
No files matched your search
+10
-134
@@ -1,137 +1,13 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { generateDocumentationSmoke } from './docs-smoke/generator.ts'
|
||||
|
||||
// 使用中文文档作为源(中文是原始文档,英文是 AI 翻译的)
|
||||
const docsDir = path.resolve('packages/artplayer-vitepress/docs')
|
||||
const outputFile = path.resolve('docs/test/test.js')
|
||||
|
||||
// 排除的目录
|
||||
const excludeDirs = ['en', '.vitepress', 'public', 'plugin']
|
||||
|
||||
// Regex to match code blocks after "Run Code" markers
|
||||
// Match until we find a closing ``` that's on its own line
|
||||
const runCodePattern = /<div className="run-code">.*?<\/div>[\t\v\f\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*\n\s*```js[^\n]*\n([\s\S]*?)\n```(?=\s*\n|$)/g
|
||||
|
||||
function extractCodeBlocks(content, filePath) {
|
||||
const blocks = []
|
||||
let match = runCodePattern.exec(content)
|
||||
|
||||
while (match !== null) {
|
||||
const code = match[1].trim()
|
||||
|
||||
// Skip if code contains markdown syntax (unclosed code block issue)
|
||||
if (code.includes('\n## ') || code.includes('\n:::') || code.includes('<div className="run-code">')) {
|
||||
console.warn(`⚠️ Skipping malformed code block in ${filePath}`)
|
||||
continue
|
||||
}
|
||||
|
||||
blocks.push({
|
||||
code,
|
||||
file: filePath,
|
||||
})
|
||||
match = runCodePattern.exec(content)
|
||||
}
|
||||
|
||||
return blocks
|
||||
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use yarn build:test [--check]')
|
||||
const outputs = await generateDocumentationSmoke(process.cwd())
|
||||
for (const [file, content] of outputs) {
|
||||
if (process.argv.includes('--check'))
|
||||
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), content, `Generated documentation smoke drift: ${file}`)
|
||||
else fs.writeFileSync(file, content)
|
||||
}
|
||||
|
||||
function processMarkdownFiles(dir, relativePath = '') {
|
||||
const results = []
|
||||
const items = fs.readdirSync(dir)
|
||||
|
||||
for (const item of items) {
|
||||
// 跳过排除的目录
|
||||
if (excludeDirs.includes(item)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const fullPath = path.join(dir, item)
|
||||
const relPath = path.join(relativePath, item)
|
||||
const stat = fs.statSync(fullPath)
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
results.push(...processMarkdownFiles(fullPath, relPath))
|
||||
}
|
||||
else if (item.endsWith('.md')) {
|
||||
const content = fs.readFileSync(fullPath, 'utf-8')
|
||||
const blocks = extractCodeBlocks(content, relPath)
|
||||
if (blocks.length > 0) {
|
||||
results.push({
|
||||
file: relPath,
|
||||
blocks,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
function generateTestCode(fileResults) {
|
||||
const lines = []
|
||||
|
||||
lines.push(`// Auto-generated smoke tests from documentation code blocks`)
|
||||
lines.push(`// Generated at: ${new Date().toISOString()}`)
|
||||
lines.push(`// Do not edit manually - run 'node scripts/build-test.js' to regenerate`)
|
||||
lines.push(``)
|
||||
lines.push(`describe('Documentation Code Examples', function () {`)
|
||||
lines.push(` this.timeout(5000);`)
|
||||
lines.push(``)
|
||||
lines.push(` beforeEach(function () {`)
|
||||
lines.push(` // Destroy any existing instances`)
|
||||
lines.push(` [...Artplayer.instances].forEach((art) => art.destroy());`)
|
||||
lines.push(` // Reset container`)
|
||||
lines.push(` const container = document.querySelector('.artplayer-app');`)
|
||||
lines.push(` if (container) {`)
|
||||
lines.push(` container.innerHTML = '';`)
|
||||
lines.push(` }`)
|
||||
lines.push(` });`)
|
||||
lines.push(``)
|
||||
lines.push(` afterEach(function () {`)
|
||||
lines.push(` [...Artplayer.instances].forEach((art) => art.destroy());`)
|
||||
lines.push(` });`)
|
||||
lines.push(``)
|
||||
|
||||
for (const { file, blocks } of fileResults) {
|
||||
const suiteName = file.replace(/\.md$/, '').replace(/\//g, ' > ')
|
||||
|
||||
lines.push(` describe('${suiteName}', function () {`)
|
||||
|
||||
blocks.forEach((block, index) => {
|
||||
const testName = `Example ${index + 1}`
|
||||
|
||||
lines.push(` it('${testName}', function (done) {`)
|
||||
lines.push(` try {`)
|
||||
lines.push(` ${block.code.split('\n').join('\n ')}`)
|
||||
lines.push(` // Wait a bit for async initialization`)
|
||||
lines.push(` setTimeout(() => done(), 100);`)
|
||||
lines.push(` } catch (err) {`)
|
||||
lines.push(` done(err);`)
|
||||
lines.push(` }`)
|
||||
lines.push(` });`)
|
||||
lines.push(``)
|
||||
})
|
||||
|
||||
lines.push(` });`)
|
||||
lines.push(``)
|
||||
}
|
||||
|
||||
lines.push(`});`)
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const fileResults = processMarkdownFiles(docsDir)
|
||||
|
||||
let totalBlocks = 0
|
||||
for (const { file, blocks } of fileResults) {
|
||||
console.log(`📄 ${file}: ${blocks.length} code blocks`)
|
||||
totalBlocks += blocks.length
|
||||
}
|
||||
|
||||
const testCode = generateTestCode(fileResults)
|
||||
fs.writeFileSync(outputFile, testCode)
|
||||
|
||||
console.log(`\n✅ Generated ${totalBlocks} test cases from ${fileResults.length} files`)
|
||||
console.log(`📝 Output: ${outputFile}`)
|
||||
console.log(`Documentation readiness smoke ${process.argv.includes('--check') ? 'checked' : 'generated'}: ${outputs.size} outputs; complete demo acceptance remains separate`)
|
||||
@@ -0,0 +1,75 @@
|
||||
# Documentation readiness smoke
|
||||
|
||||
This generator keeps the existing `yarn build:test` / `node scripts/build-test.js`
|
||||
entrypoint and `docs/test/test.js` URL. Use the pinned Node 24.21.0 runtime and
|
||||
Yarn 1.22.22. Node runs the owned TypeScript modules using native type stripping;
|
||||
the command shim remains JavaScript and is included in strict `checkJs` checking.
|
||||
The root typecheck commands use `node node_modules/typescript/bin/tsc` explicitly:
|
||||
Yarn's shared `tsc` binary can point at the installed 5.1.6 compatibility alias.
|
||||
The main 5.9.3 compiler and older compatibility compilers retain distinct roles.
|
||||
|
||||
| File | Ownership |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `parser.ts` | Markdown tokens, Run Code/fence adjacency, source locations and closing fences |
|
||||
| `generator.ts` | Deterministic file discovery, syntax compilation without execution, dependency list and browser bundle |
|
||||
| `runtime.ts` | One sequential example frame, script loading, error/readiness observation, teardown and Mocha registration |
|
||||
| `../build-test.js` | Original CLI path, argument validation and write/read-only check |
|
||||
| `../tsconfig.docs.json` | Strict Node/DOM tools check, including the JavaScript shim |
|
||||
|
||||
```sh
|
||||
yarn build:test
|
||||
yarn check:docs-smoke
|
||||
yarn typecheck:docs-tools
|
||||
node --test refactor/scripts/docs-smoke.test.mjs
|
||||
yarn test:browser test/browser/docs-smoke.spec.js
|
||||
```
|
||||
|
||||
`docs/test/examples.json` records stable source file/line IDs, original code and
|
||||
the ordered script list from `docs/test/index.html`. No timestamps enter generated
|
||||
content. Generation validates every selected example before writing either output;
|
||||
`--check` compares both files without modifying them. Parse/syntax errors fail the
|
||||
command with a source location. This is not a transactional multi-file filesystem
|
||||
write if the filesystem itself fails during output.
|
||||
|
||||
The scope stays Chinese JavaScript fences after Run Code markers, excluding
|
||||
`en`, `plugin`, `public` and `.vitepress`. There are 233 current cases in 11 files.
|
||||
CRLF is normalized to LF. New cases must be reviewed and the deliberate count
|
||||
assertion updated. JS syntax compilation does not execute snippets or validate
|
||||
their API semantics. TypeScript/editor declaration generation remains SITE-02.
|
||||
|
||||
The browser bundle registers cases with the existing Mocha BDD page. Each case:
|
||||
|
||||
1. Creates an iframe with its own ArtPlayer container and loads the page's runtime
|
||||
scripts in order, excluding Mocha, Chai and the generated test script itself.
|
||||
2. Evaluates the snippet as a classic script in that frame, awaits its returned
|
||||
promise if any, and waits for the currently created players' `ready` events.
|
||||
3. Observes thrown errors, unhandled rejections and media/player errors until that
|
||||
readiness boundary plus one task turn. A 4500 ms deadline produces failure,
|
||||
never success. Cases without players cover evaluation only.
|
||||
4. Detaches observer listeners, destroys frame players, removes the iframe and
|
||||
restores local/session storage. Cleanup errors are failures and retain an
|
||||
earlier failure in the error list. Concurrent calls are rejected.
|
||||
|
||||
The iframe is an owned browsing context, **not a security sandbox**. It shares
|
||||
the parent origin. Run the test page on an isolated test origin with no concurrent
|
||||
same-origin work: storage restoration does not merge writes from other tabs.
|
||||
Unchanged browser storage is restored after each case, while frame-owned timers
|
||||
and globals disappear when the frame is removed. Arbitrary external resources
|
||||
that a snippet deliberately hands to another window are outside this ownership.
|
||||
|
||||
Readiness is not playback, interaction or complete asynchronous feature acceptance.
|
||||
Delayed actions scheduled after this boundary are not verified; examples involving
|
||||
clicks, seek, SDKs, windows and full lifecycle behavior remain EX-03/package tests.
|
||||
The automated harness tests use actual core/media and three selected unchanged
|
||||
documentation snippets with controlled media. They do not certify all 233 examples
|
||||
or the remote SDK endpoints in the full manual page.
|
||||
|
||||
Windows WebKit has the existing DPIP-MEDIA-01 dimension discrepancy: the same
|
||||
320x180 media can report 640x360 layout dimensions. The harness retains exact raw
|
||||
readings and only these documented pairs; it does not close intrinsic-size or
|
||||
real Safari/device acceptance. See `refactor/site-inventory.md` for broader ownership.
|
||||
|
||||
Dependencies: root development-only `@types/node` 24.10.0 and
|
||||
`@types/markdown-it` 14.1.2 provide strict tools typing. Runtime parsing uses the
|
||||
existing MarkdownIt 14.1.0 and browser bundling uses the existing pinned esbuild.
|
||||
No player package runtime dependency changes.
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { DocumentationExample } from './parser.ts'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { Script } from 'node:vm'
|
||||
import { build } from 'esbuild'
|
||||
import { extractExamples } from './parser.ts'
|
||||
|
||||
export function collectExamples(directory: string, prefix = ''): DocumentationExample[] {
|
||||
return fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, 'en')).flatMap((entry) => {
|
||||
if (['en', '.vitepress', 'public', 'plugin'].includes(entry.name))
|
||||
return []
|
||||
const relative = prefix ? `${prefix}/${entry.name}` : entry.name
|
||||
const file = path.join(directory, entry.name)
|
||||
if (entry.isDirectory())
|
||||
return collectExamples(file, relative)
|
||||
if (!entry.name.endsWith('.md'))
|
||||
return []
|
||||
const examples = extractExamples(fs.readFileSync(file, 'utf8'), relative)
|
||||
for (const example of examples) {
|
||||
// eslint-disable-next-line no-new -- Compile syntax without executing documentation code.
|
||||
new Script(example.code, { filename: example.id })
|
||||
}
|
||||
return examples
|
||||
})
|
||||
}
|
||||
|
||||
export async function generateDocumentationSmoke(root: string): Promise<Map<string, string>> {
|
||||
const examples = collectExamples(path.join(root, 'packages/artplayer-vitepress/docs'))
|
||||
const html = fs.readFileSync(path.join(root, 'docs/test/index.html'), 'utf8').replace(/<!--[\s\S]*?-->/g, '')
|
||||
const scripts = [...html.matchAll(/<script\b[^>]+\bsrc=["']([^"']+)["']/g)].map(match => match[1]!).filter(url => !['./chai.js', './mocha.js', './test.js'].includes(url))
|
||||
if (!scripts.some(url => /\/artplayer\.js$/.test(url)))
|
||||
throw new Error('Documentation smoke page must load the core')
|
||||
const bundle = await build({ stdin: { contents: `import { registerDocumentationSmoke } from './runtime.ts';\nregisterDocumentationSmoke(${JSON.stringify(examples)}, ${JSON.stringify(scripts)});`, resolveDir: path.join(root, 'scripts/docs-smoke'), sourcefile: 'documentation-smoke-entry.ts', loader: 'ts' }, bundle: true, write: false, platform: 'browser', format: 'iife', target: 'es2020', legalComments: 'none', logLevel: 'silent' })
|
||||
const script = bundle.outputFiles?.[0]?.text
|
||||
if (!script)
|
||||
throw new Error('Documentation smoke build produced no script')
|
||||
return new Map([
|
||||
['docs/test/test.js', `// Generated by yarn build:test. Readiness smoke only; do not edit.\n${script}`],
|
||||
['docs/test/examples.json', `${JSON.stringify({ scope: 'Chinese Run Code JavaScript; excludes en/plugin/public/.vitepress. Waits for created players to become ready, observes errors until that boundary, then destroys the frame. Not complete feature, delayed-callback or interaction acceptance.', examples, scripts }, null, 2)}\n`],
|
||||
])
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
export interface DocumentationExample {
|
||||
id: string
|
||||
file: string
|
||||
line: number
|
||||
code: string
|
||||
}
|
||||
|
||||
const markdown = new MarkdownIt({ html: true })
|
||||
|
||||
export function extractExamples(content: string, file: string): DocumentationExample[] {
|
||||
const normalized = content.replaceAll('\r\n', '\n')
|
||||
const lines = normalized.split('\n')
|
||||
const tokens = markdown.parse(normalized, {})
|
||||
const examples: DocumentationExample[] = []
|
||||
for (const [index, marker] of tokens.entries()) {
|
||||
if (marker.type !== 'html_block' || !/<div\s+class(?:Name)?="run-code"[\s>]/.test(marker.content))
|
||||
continue
|
||||
const line = (marker.map?.[0] ?? 0) + 1
|
||||
const invalid = (message: string) => new Error(`${file}:${line}: ${message}`)
|
||||
const fence = tokens[index + 1]
|
||||
if (!fence || fence.type !== 'fence' || !/^js(?:\{|\s|$)/.test(fence.info))
|
||||
throw invalid('Run Code must be followed by a JavaScript fence')
|
||||
const range = fence.map
|
||||
if (!range)
|
||||
throw invalid('Missing code fence source range')
|
||||
const closing = lines[range[1] - 1]?.trim() || ''
|
||||
if (closing.length < fence.markup.length || [...closing].some(character => character !== fence.markup[0]))
|
||||
throw invalid('Unclosed Run Code fence')
|
||||
const code = fence.content.trim()
|
||||
if (!code)
|
||||
throw invalid('Empty Run Code example')
|
||||
examples.push({ id: `${file}:${range[0] + 2}`, file, line: range[0] + 2, code })
|
||||
}
|
||||
return examples
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import type Artplayer from '../../packages/artplayer/public/artplayer.ts'
|
||||
import type { DocumentationExample } from './parser.ts'
|
||||
|
||||
interface FrameWindow extends Window {
|
||||
Artplayer?: typeof Artplayer
|
||||
eval: (code: string) => unknown
|
||||
}
|
||||
export interface SmokeResult {
|
||||
id: string
|
||||
instances: number
|
||||
ready: number
|
||||
media: { readyState: number, width: number, height: number, duration: number }[]
|
||||
}
|
||||
|
||||
let running = false
|
||||
|
||||
/** Evaluate one example in an owned browsing context; readiness is not feature acceptance. */
|
||||
export async function runExample(
|
||||
example: DocumentationExample,
|
||||
scripts: string[],
|
||||
timeoutMs = 4500,
|
||||
): Promise<SmokeResult> {
|
||||
if (running)
|
||||
throw new Error('Documentation smoke cases must run sequentially')
|
||||
const savedStorage = [localStorage, sessionStorage].map(storage => ({ storage, entries: Object.entries(storage) }))
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.dataset.documentationSmoke = example.id
|
||||
iframe.title = `Documentation smoke: ${example.id}`
|
||||
iframe.style.cssText = 'width:640px;height:360px;border:0;display:block'
|
||||
const context: { frame: FrameWindow | null } = { frame: null }
|
||||
let active = true
|
||||
const disposers: (() => void)[] = []
|
||||
let fail: (error: unknown) => void = () => {}
|
||||
const failure = new Promise<never>((_resolve, reject) => {
|
||||
fail = reject
|
||||
})
|
||||
const deadline = window.setTimeout(
|
||||
() => fail(new Error(`${example.id}: readiness timeout (${timeoutMs}ms)`)),
|
||||
timeoutMs,
|
||||
)
|
||||
running = true
|
||||
const ensureActive = () => {
|
||||
if (!active)
|
||||
throw new Error('Documentation context was disposed')
|
||||
}
|
||||
function loaded(target: EventTarget, action: () => void, description: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const done = () => {
|
||||
cleanup()
|
||||
resolve()
|
||||
}
|
||||
const error = () => {
|
||||
cleanup()
|
||||
reject(new Error(`Failed to load ${description}`))
|
||||
}
|
||||
function cleanup() {
|
||||
target.removeEventListener('load', done)
|
||||
target.removeEventListener('error', error)
|
||||
}
|
||||
target.addEventListener('load', done, { once: true })
|
||||
target.addEventListener('error', error, { once: true })
|
||||
disposers.push(() => {
|
||||
cleanup()
|
||||
reject(new Error(`Cancelled ${description}`))
|
||||
})
|
||||
action()
|
||||
})
|
||||
}
|
||||
const work = async (): Promise<SmokeResult> => {
|
||||
await loaded(
|
||||
iframe,
|
||||
() => {
|
||||
iframe.srcdoc
|
||||
= '<!doctype html><html><body style="margin:0"><div class="artplayer-app" style="width:640px;height:360px"></div></body></html>'
|
||||
document.body.appendChild(iframe)
|
||||
},
|
||||
'documentation frame',
|
||||
)
|
||||
ensureActive()
|
||||
const frame = iframe.contentWindow as FrameWindow | null
|
||||
context.frame = frame
|
||||
if (!frame)
|
||||
throw new Error('Documentation frame is unavailable')
|
||||
const onError = (event: ErrorEvent) => {
|
||||
event.preventDefault()
|
||||
fail(new Error(`${example.id}: ${event.message}`))
|
||||
}
|
||||
const onRejection = (event: PromiseRejectionEvent) => {
|
||||
event.preventDefault()
|
||||
fail(new Error(`${example.id}: ${String(event.reason)}`))
|
||||
}
|
||||
frame.addEventListener('error', onError)
|
||||
frame.addEventListener('unhandledrejection', onRejection)
|
||||
disposers.push(() => {
|
||||
frame?.removeEventListener('error', onError)
|
||||
frame?.removeEventListener('unhandledrejection', onRejection)
|
||||
})
|
||||
for (const url of scripts) {
|
||||
const script = frame.document.createElement('script')
|
||||
script.src = new URL(url, location.href).href
|
||||
await loaded(script, () => frame.document.head.appendChild(script), script.src)
|
||||
ensureActive()
|
||||
}
|
||||
if (!frame.Artplayer)
|
||||
throw new Error('Artplayer was not loaded in the documentation frame')
|
||||
// Preserve classic-script globals while keeping timers, DOM and listeners inside this frame.
|
||||
await frame.eval(`${example.code}\n//# sourceURL=artplayer-doc-example-${encodeURIComponent(example.id)}.js`)
|
||||
ensureActive()
|
||||
const instances = frame.Artplayer.instances.slice()
|
||||
await Promise.all(
|
||||
instances.map(
|
||||
instance =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
if (instance.isReady) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const ready = () => {
|
||||
cleanup()
|
||||
resolve()
|
||||
}
|
||||
const error = () => {
|
||||
cleanup()
|
||||
reject(new Error(`${example.id}: player readiness failed`))
|
||||
}
|
||||
function cleanup() {
|
||||
instance.off('ready', ready)
|
||||
instance.off('error', error)
|
||||
instance.off('video:error', error)
|
||||
}
|
||||
instance.on('ready', ready)
|
||||
instance.on('error', error)
|
||||
instance.on('video:error', error)
|
||||
disposers.push(() => {
|
||||
cleanup()
|
||||
reject(new Error('Cancelled player readiness'))
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
// A task boundary delivers unhandled promise rejections from evaluation/ready callbacks.
|
||||
await new Promise<void>((resolve) => {
|
||||
const timer = window.setTimeout(resolve, 0)
|
||||
disposers.push(() => window.clearTimeout(timer))
|
||||
})
|
||||
ensureActive()
|
||||
return {
|
||||
id: example.id,
|
||||
instances: instances.length,
|
||||
ready: instances.filter(instance => instance.isReady).length,
|
||||
media: instances.map(instance => ({
|
||||
readyState: instance.video.readyState,
|
||||
width: instance.video.videoWidth,
|
||||
height: instance.video.videoHeight,
|
||||
duration: instance.video.duration,
|
||||
})),
|
||||
}
|
||||
}
|
||||
let result: SmokeResult | undefined
|
||||
let problem: unknown
|
||||
let failed = false
|
||||
try {
|
||||
result = await Promise.race([work(), failure])
|
||||
}
|
||||
catch (error) {
|
||||
failed = true
|
||||
problem = error
|
||||
}
|
||||
finally {
|
||||
active = false
|
||||
window.clearTimeout(deadline)
|
||||
for (const dispose of disposers) dispose()
|
||||
const cleanupErrors: unknown[] = []
|
||||
for (const instance of context.frame?.Artplayer?.instances.slice() || []) {
|
||||
try {
|
||||
instance.destroy(true)
|
||||
}
|
||||
catch (error) {
|
||||
cleanupErrors.push(error)
|
||||
}
|
||||
}
|
||||
iframe.remove()
|
||||
for (const { storage, entries } of savedStorage) {
|
||||
try {
|
||||
storage.clear()
|
||||
for (const [key, value] of entries) storage.setItem(key, value)
|
||||
}
|
||||
catch (error) {
|
||||
cleanupErrors.push(error)
|
||||
}
|
||||
}
|
||||
running = false
|
||||
if (cleanupErrors.length) {
|
||||
const errors = failed ? [problem, ...cleanupErrors] : cleanupErrors
|
||||
problem = Object.assign(new Error(`Documentation cleanup failed: ${errors.map(String).join('; ')}`), { errors })
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
if (failed)
|
||||
throw problem
|
||||
if (!result)
|
||||
throw new Error('Documentation smoke did not produce a result')
|
||||
return result
|
||||
}
|
||||
|
||||
interface MochaHost {
|
||||
describe: (name: string, callback: (this: { timeout: (ms: number) => void }) => void) => void
|
||||
it: (name: string, callback: () => Promise<void>) => void
|
||||
}
|
||||
|
||||
export function registerDocumentationSmoke(examples: DocumentationExample[], scripts: string[]): void {
|
||||
const host = globalThis as unknown as MochaHost
|
||||
if (typeof host.describe !== 'function' || typeof host.it !== 'function')
|
||||
throw new Error('Documentation smoke requires the existing Mocha BDD page')
|
||||
host.describe('Documentation readiness smoke (not feature acceptance)', function () {
|
||||
this.timeout(6000)
|
||||
for (const example of examples) {
|
||||
host.it(example.id, async () => {
|
||||
await runExample(example, scripts)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"types": ["node"],
|
||||
"allowImportingTsExtensions": true,
|
||||
"checkJs": true
|
||||
},
|
||||
"include": ["docs-smoke/**/*.ts", "build-test.js"]
|
||||
}
|
||||
Reference in new issue
Block a user