regtest-html
Version:
RegTest compatible test framework for HTML interpreters
270 lines (236 loc) • 9.39 kB
JavaScript
/*
RegTest-HTML Runner
===================
Copyright (c) 2022 Dannii Willis
MIT licenced
https://github.com/curiousdannii/regtest-html
*/
import fs from 'fs/promises'
import path from 'path'
import {HttpServer} from 'http-server'
import puppeteer from 'puppeteer'
export class Runner {
constructor(testfile_data, options) {
this.errors = 0
this.list_of_tests = []
this.options = options
this.testfile = testfile_data.split(/\n/g).map(line => line.trim())
// Process testfile parameters
for (const line of this.testfile) {
if (line.startsWith('**')) {
const command_parts = /^\*\*\s+(\w+):\s+(\S.+)$/.exec(line)
const command = command_parts?.[1]
if (command === 'game' && !options.gamefile_path) {
options.gamefile_path = path.join(path.dirname(options.testfile_path), command_parts?.[2])
}
if (command === 'interpreter' && !options.interpreter_path) {
options.interpreter_path = path.join(path.dirname(options.testfile_path), command_parts?.[2])
}
}
else if (line.startsWith('*')) {
this.list_of_tests.push(/^\*\s+(\S.+)$/.exec(line)?.[1]?.trim())
}
}
if (!options.interpreter_path) {
console.log('No interpreter specified')
process.exit()
}
const cwd = process.cwd()
// If the interpreter path is not already a URL, then construct it
try {
new URL(options.interpreter_path)
options.interpreter_url = options.interpreter_path
}
catch {
options.interpreter_url = `http://localhost:${options.port}/${path.relative(cwd, options.interpreter_path)}`
}
options.gamefile_url = options.gamefile_path ? `http://localhost:${options.port}/${path.relative(cwd, options.gamefile_path)}` : ''
if (!options.tests || !options.tests.length) {
options.tests = this.list_of_tests.slice()
}
//console.log(options)
}
async run() {
// Start a local HTTP server
const server = new HttpServer()
server.listen(this.options.port)
// Set up Puppeteer
await this.setup_puppeteer()
// Run all the tests
for (const test of this.options.tests) {
await this.run_one_test(test)
}
// We're finished, so shut it down
await this.browser.close()
server.close()
if (this.errors) {
console.error(`FAILED: ${this.errors} errors`)
process.exit(1)
}
}
make_input_request_handler() {
this.input_request_handler = new Promise((resolve, reject) => {
const timer = setTimeout(async () => {
if (this.options.pdf) {
await fs.writeFile('error.pdf', await this.page.pdf())
}
reject(new Error('Timed out awaiting output'))
}, 1000 * this.options.timeout)
this.input_request = data => {
clearTimeout(timer)
resolve(data)
}
})
}
async run_one_test(test) {
console.log(`* ${test}`)
this.buffertext = ''
this.make_input_request_handler()
this.waiting_for_final_text = 0
await this.page.goto(`${this.options.interpreter_url}?story=${this.options.gamefile_url}&do_vm_autosave`)
await this.run_test_script(test)
}
async run_test_script(test) {
const test_regex = new RegExp(`^\\*\\s+${test}\\s*$`)
let found_the_test = false
let checks = []
const process_delayed_checks = async () => {
for (let check of checks) {
const inverted = check.startsWith('!')
if (inverted) {
check = check.substring(1)
}
const found = this.buffertext.includes(check)
if (!found && !inverted) {
this.errors++
console.error(`Literal check "${check}": not found`)
if (this.options.pdf) {
await fs.writeFile('error.pdf', await this.page.pdf())
}
}
else if (found && inverted) {
this.errors++
console.error(`Inverted literal check "${check}": should not be found`)
if (this.options.pdf) {
await fs.writeFile('error.pdf', await this.page.pdf())
}
}
}
// Clear
this.buffertext = ''
checks = []
}
for (const line of this.testfile) {
// Skip everything before the test
if (!found_the_test) {
if (test_regex.test(line)) {
found_the_test = true
}
continue
}
// Skip blank lines and comments
if (line === '' || line.startsWith('#')) {
continue
}
if (line.startsWith('>{include}')) {
await this.run_test_script(line.substring(10).trim())
}
else if (line.startsWith('>{timer}')) {
// Process the delayed checks
await process_delayed_checks()
}
else if (line.startsWith('>')) {
const requested_event = await this.input_request_handler
// Make a new promise to await
this.make_input_request_handler()
// Process the delayed checks
await process_delayed_checks()
// Check the requested event type
let type = 'line'
let command = line.substring(1).trim()
if (requested_event.type === 'fileref_prompt') {
if (!line.startsWith('>{fileref_prompt}')) {
throw new Error('Game is not expecting fileref_prompt input')
}
type = 'fileref_prompt'
command = line.substring(17).trim()
}
else if (requested_event.type === 'char') {
if (!line.startsWith('>{char}')) {
throw new Error('Game is not expecting char input')
}
type = 'char'
if (line === '>{char}') {
command = 'return'
}
else {
command = line.substring(7)
}
if (/^ +$/.test(command)) {
command = ' '
}
else {
command = command.trim()
}
if (/^\d+$/.test(command)) {
command = parseInt(command, 10)
}
else if (/^0[x][0-9a-f]+$/i.test(command)) {
command = String.fromCodePoint(parseInt(command.substring(2), 16))
}
}
else if (requested_event.type !== 'line') {
throw new Error('Game is not expecting line input')
}
// Send the input
await this.page.evaluate((type, command) => {
regtest_event({
type,
value: command,
})
}, type, command)
}
// Skip other tests
else if (line.startsWith('* ')) {
break
}
else {
checks.push(line)
}
}
// We may need to wait for some text to be sent
if (checks.length) {
if (!this.buffertext) {
this.waiting_for_final_text = 1
await this.input_request_handler
}
await process_delayed_checks()
}
}
async setup_puppeteer() {
this.browser = await puppeteer.launch({headless: 'new'})
this.page = await this.browser.newPage()
// Log any page errors
this.page.on('error', err => console.error(`Page error: ${err}`))
this.page.on('pageerror', err => console.error(`Page error: ${err}`))
// Receive data back from the injected script
await this.page.exposeFunction('regtest_data', data => {
if (data.type === 'buffertext') {
this.buffertext += data.text
if (this.options.verbose) {
process.stdout.write(data.text)
}
if (this.waiting_for_final_text) {
this.input_request()
}
}
if (data.type === 'input_requested') {
this.input_request(data.data)
}
})
await this.page.exposeFunction('regtest_log', console.log)
// Inject our mutation observer script
const observer_script = await fs.readFile(new URL('./observer.js', import.meta.url), 'utf8')
await this.page.evaluateOnNewDocument(observer_script)
}
}