refactor(tooling): [MOD-DEV-01] own dev server lifecycle and report port failures

This commit is contained in:
Harvey Zhao committed 2026-09-14 11:29:46 +08:00
1 parent 401d4cdd65
commit 772d79a671
24 files changed
+1249 -132

No files matched your search

+7 -3
View File
@@ -1,10 +1,14 @@
# Browser regression entry
`library-development.spec.js` starts the actual typed development runner on an
isolated fixture port. It checks generated TS/Less/SVG/inline-worker assets,
`library-development.spec.js` starts the actual JS dev CLI and typed runner on an
atomically assigned fixture port. It checks generated TS/Less/SVG/inline-worker assets,
automatic reload on edits and recovery after a compiler error in all three
engines. It owns and stops only its child server, never the user's port 8082
session. This is development-tool validation, not media playback acceptance.
session. SIGTERM must settle the child naturally with exit0. Its second case serves
actual docs/Monaco, executes TypeScript and plays/pauses/seeks a local MP4, checking
a decoded canvas frame. The attachment records browser/version, OS and actual
core/media hashes; the tracked core5.4.1 asset is not a new release candidate.
Analytics/ads are inert routes; real-device and remote OS checks remain separate.
`site-loading.spec.js` covers actual desktop/mobile loading, dependency order,
failure/retry/AMD restoration, example priority, repeated Run and stale responses,
+80 -20
View File
@@ -1,10 +1,11 @@
import { spawn } from 'node:child_process'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import net from 'node:net'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { expect, test } from '@playwright/test'
import { startDevServer } from '../../scripts/library/server.ts'
const root = fileURLToPath(new URL('../../', import.meta.url))
const cache = path.join(root, 'refactor/.cache')
@@ -15,17 +16,6 @@ function removeFixture(directory) {
fs.rmSync(directory, { recursive: true, force: true })
}
async function freePort() {
const server = net.createServer()
await new Promise((resolve, reject) => {
server.once('error', reject)
server.listen(0, resolve)
})
const port = server.address().port
await new Promise(resolve => server.close(resolve))
return port
}
test('typed development builds real assets and workers, watches edits and recovers from compilation errors', async ({ page }, testInfo) => {
test.setTimeout(60000)
const directory = fs.mkdtempSync(path.join(cache, 'library-development-'))
@@ -40,15 +30,14 @@ test('typed development builds real assets and workers, watches edits and recove
window.probe = artplayerPluginBuildProbe();
document.querySelector('main').textContent = String(probe.value);
</script></body></html>`)
const port = await freePort()
const url = `http://localhost:${port}`
const module = pathToFileURL(path.join(root, 'scripts/library/development.ts')).href
const code = `import { runDevelop } from ${JSON.stringify(module)};
const module = pathToFileURL(path.join(root, 'scripts/dev.js')).href
const code = `process.on('message', () => process.emit('SIGTERM'));
process.argv = [process.execPath, 'fixture.js', '${name}', '--no-open'];
await runDevelop({port:${port}});`
await import(${JSON.stringify(module)});
process.disconnect();`
const launcher = path.join(directory, 'launch.mjs')
fs.writeFileSync(launcher, code)
const child = spawn(process.execPath, [launcher], { cwd: directory, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
const child = spawn(process.execPath, [launcher], { cwd: directory, env: { ...process.env, ARTPLAYER_DEV_PORT: '0' }, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe', 'ipc'] })
let output = ''
child.stdout.on('data', (data) => {
output += data
@@ -60,6 +49,7 @@ await runDevelop({port:${port}});`
try {
await expect.poll(() => output, { timeout: 20000 }).toContain('Watching')
expect(child.exitCode).toBeNull()
const url = /Demo: (http:\/\/localhost:\d+)/.exec(output)[1]
await page.goto(url)
await expect(page.locator('main')).toHaveText('42')
expect(await page.evaluate(() => window.probe.css)).toContain('#123abc')
@@ -91,11 +81,81 @@ await runDevelop({port:${port}});`
fs.writeFileSync(entry, original.replace('add(20, 22)', 'add(20, 24)'))
await expect.poll(() => output.slice(failed), { timeout: 15000 }).toContain('Built in')
await expect(page.locator('main')).toHaveText('44')
const html = path.join(directory, 'docs/index.html')
fs.appendFileSync(html, '<title>Docs edit reloaded</title>')
await expect(page).toHaveTitle('Docs edit reloaded')
await expect(page.locator('main')).toHaveText('44')
}
finally {
await testInfo.attach('development-server.log', { body: output, contentType: 'text/plain' })
child.kill()
await exited
const watchdog = setTimeout(() => child.kill(), 5000)
if (child.connected)
child.send('close')
const exitCode = await exited
clearTimeout(watchdog)
removeFixture(directory)
expect(exitCode, 'Dev CLI must close its resources and exit naturally').toBe(0)
}
})
test('actual docs editor runs TypeScript and plays and seeks local media through the owned server', async ({ page, browser }, testInfo) => {
test.setTimeout(60000)
const errors = []
const pageErrors = []
const service = await startDevServer({ root: path.join(root, 'docs'), port: 0, onError: error => errors.push(error.message) })
page.on('pageerror', error => pageErrors.push(error.message))
await page.route('https://**/*', route => route.fulfill({ status: 200, body: '' }))
await page.addInitScript((origin) => {
if (location.origin === origin)
localStorage.setItem('ts', 'true')
}, service.url)
try {
const code = 'const typed: number = 17; window.devServerTyped = typed; var art = new Artplayer({container:".artplayer-app",url:"/assets/sample/video.mp4",muted:true});'
await page.goto(`${service.url}/?code=${encodeURIComponent(code)}`)
await page.waitForFunction(() => window.art?.isReady && window.devServerTyped === 17)
await page.evaluate(() => window.art.play())
await page.waitForFunction(() => window.art.currentTime > 0.1)
const target = await page.evaluate(() => {
window.art.pause()
const target = Math.min(2, window.art.duration / 2)
window.art.seek = target
return target
})
await page.waitForFunction(target => Math.abs(window.art.currentTime - target) < 0.3 && !window.art.video.seeking, target)
const frameAlpha = await page.evaluate(() => {
const video = window.art.video
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const context = canvas.getContext('2d')
context.drawImage(video, 0, 0)
return context.getImageData(0, 0, 1, 1).data[3]
})
expect(frameAlpha).toBe(255)
const state = await page.evaluate(() => ({
version: window.Artplayer.version,
typed: window.devServerTyped,
currentTime: window.art.currentTime,
paused: window.art.video.paused,
width: window.art.video.videoWidth,
height: window.art.video.videoHeight,
error: window.art.video.error?.code || 0,
editorModels: window.monaco.editor.getModels().length,
}))
expect(state.typed).toBe(17)
expect(state.paused).toBe(true)
expect(state.width).toBeGreaterThan(0)
expect(state.height).toBeGreaterThan(0)
expect(state.error).toBe(0)
expect(state.editorModels).toBeGreaterThan(1)
const inputs = ['docs/uncompiled/artplayer/index.js', 'docs/assets/sample/video.mp4'].map(file => ({ file, sha256: createHash('sha256').update(fs.readFileSync(path.join(root, file))).digest('hex') }))
await testInfo.attach('actual-docs-server.json', { body: JSON.stringify({ browser: browser.version(), platform: process.platform, inputs, frameAlpha, state }, null, 2), contentType: 'application/json' })
await page.screenshot({ path: testInfo.outputPath('actual-docs.png') })
await page.goto('about:blank')
expect(pageErrors).toEqual([])
}
finally {
await service.close()
expect(errors).toEqual([])
}
})