mirror of
https://github.com/ThatGuySam/doesitarm.git
synced 2026-05-18 06:44:46 -07:00
test(playwright): cover browser flows with typed harness
Lock the Apple Silicon upload flow behind a real browser regression and migrate the Pagefind browser regression onto a shared typed harness. This keeps the branch non-runtime and repairs the targeted pagefind script alias after moving the browser test to TypeScript. Constraint: Must avoid runtime code changes on this branch Rejected: Ship the app-test coverage without fixing script aliases | leaves a broken targeted check in package.json Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep browser-only support helpers under test/playwright/support until runtime code is typed separately Tested: pnpm run typecheck; pnpm run test:browser; pnpm run test:browser:pagefind Not-tested: pnpm run test:browser:pagefind:live
This commit is contained in:
parent
c5ec942de0
commit
e638bdfa74
7 changed files with 562 additions and 276 deletions
176
test/playwright/support/astro-browser-test.ts
Normal file
176
test/playwright/support/astro-browser-test.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
import { accessSync, constants } from 'node:fs'
|
||||
import net from 'node:net'
|
||||
|
||||
import { chromium, type Browser } from 'playwright-core'
|
||||
|
||||
const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
|
||||
const host = '127.0.0.1'
|
||||
|
||||
export interface AstroDevServer {
|
||||
baseUrl: string
|
||||
output: {
|
||||
text: string
|
||||
}
|
||||
process: ChildProcessWithoutNullStreams | null
|
||||
}
|
||||
|
||||
function canAccessPath ( filePath: string ) {
|
||||
try {
|
||||
accessSync( filePath, constants.X_OK )
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getBrowserExecutablePath () {
|
||||
const candidatePaths = [
|
||||
process.env.PLAYWRIGHT_BROWSER_PATH,
|
||||
process.env.CHROME_BIN,
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
'/opt/homebrew/bin/chromium',
|
||||
].filter( ( value ): value is string => Boolean( value ) )
|
||||
|
||||
const executablePath = candidatePaths.find( canAccessPath )
|
||||
|
||||
if ( !executablePath ) {
|
||||
throw new Error( 'No browser executable found. Set PLAYWRIGHT_BROWSER_PATH or CHROME_BIN.' )
|
||||
}
|
||||
|
||||
return executablePath
|
||||
}
|
||||
|
||||
function getAvailablePort () {
|
||||
return new Promise<number>( ( resolve, reject ) => {
|
||||
const server = net.createServer()
|
||||
|
||||
server.unref()
|
||||
server.on( 'error', reject )
|
||||
server.listen( 0, host, () => {
|
||||
const address = server.address()
|
||||
|
||||
if ( !address || typeof address === 'string' ) {
|
||||
reject( new Error( 'Unable to determine a free port.' ) )
|
||||
return
|
||||
}
|
||||
|
||||
server.close( err => {
|
||||
if ( err ) {
|
||||
reject( err )
|
||||
return
|
||||
}
|
||||
|
||||
resolve( address.port )
|
||||
} )
|
||||
} )
|
||||
} )
|
||||
}
|
||||
|
||||
export async function waitForServer ( url: string, {
|
||||
intervalMs = 250,
|
||||
timeoutMs = 60 * 1000
|
||||
} = {} ) {
|
||||
const startedAt = Date.now()
|
||||
|
||||
while ( Date.now() - startedAt < timeoutMs ) {
|
||||
try {
|
||||
const response = await fetch( url )
|
||||
|
||||
if ( response.ok ) {
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await new Promise( resolve => setTimeout( resolve, intervalMs ) )
|
||||
}
|
||||
|
||||
throw new Error( `Timed out waiting for dev server at ${ url }` )
|
||||
}
|
||||
|
||||
export async function startAstroDevServer ( {
|
||||
env = {},
|
||||
preferConfiguredBaseUrl = true
|
||||
}: {
|
||||
env?: Record<string, string>
|
||||
preferConfiguredBaseUrl?: boolean
|
||||
} = {} ): Promise<AstroDevServer> {
|
||||
const configuredBaseUrl = process.env.PLAYWRIGHT_BASE_URL || ''
|
||||
|
||||
if ( preferConfiguredBaseUrl && configuredBaseUrl.length > 0 ) {
|
||||
await waitForServer( configuredBaseUrl )
|
||||
|
||||
return {
|
||||
baseUrl: configuredBaseUrl,
|
||||
output: { text: '' },
|
||||
process: null
|
||||
}
|
||||
}
|
||||
|
||||
const port = await getAvailablePort()
|
||||
const baseUrl = `http://${ host }:${ port }`
|
||||
const output = { text: '' }
|
||||
|
||||
const childProcess = spawn( command, [
|
||||
'exec',
|
||||
'astro',
|
||||
'dev',
|
||||
'--host',
|
||||
host,
|
||||
'--port',
|
||||
String( port )
|
||||
], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
...env
|
||||
},
|
||||
stdio: [ 'ignore', 'pipe', 'pipe' ]
|
||||
} )
|
||||
|
||||
childProcess.stdout.on( 'data', chunk => {
|
||||
output.text += chunk.toString()
|
||||
} )
|
||||
childProcess.stderr.on( 'data', chunk => {
|
||||
output.text += chunk.toString()
|
||||
} )
|
||||
|
||||
await waitForServer( baseUrl )
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
output,
|
||||
process: childProcess
|
||||
}
|
||||
}
|
||||
|
||||
export function stopChildProcess ( childProcess: ChildProcessWithoutNullStreams | null ) {
|
||||
return new Promise<void>( resolve => {
|
||||
if ( !childProcess ) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
if ( childProcess.killed || childProcess.exitCode !== null ) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
childProcess.once( 'exit', () => resolve() )
|
||||
childProcess.kill( 'SIGTERM' )
|
||||
|
||||
setTimeout( () => {
|
||||
if ( childProcess.exitCode === null ) {
|
||||
childProcess.kill( 'SIGKILL' )
|
||||
}
|
||||
}, 5 * 1000 ).unref()
|
||||
} )
|
||||
}
|
||||
|
||||
export async function launchBrowser (): Promise<Browser> {
|
||||
return chromium.launch({
|
||||
executablePath: getBrowserExecutablePath(),
|
||||
headless: true
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue