@typed/test
Version:
Testing made simple.
253 lines (196 loc) • 7.84 kB
text/typescript
import { createServer } from 'http'
import { sync } from 'resolve'
import { findOpenPort } from '../browser/findOpenPort'
import { getLauncher, openBrowser } from '../browser/openBrowser'
import { setupServer } from '../browser/server'
import { Logger, TestMetadata } from '../types'
import { StatsAndResults, TypedTestOptions } from './types'
import { basename, join, isAbsolute } from 'path'
import * as tempy from 'tempy'
import { writeFileSync } from 'fs'
import { generateTestBundle } from '../browser/generateTestBundle'
import { createIndexHtml } from '../browser/createIndexHtml'
import { ProcessResults, typecheckInAnotherProcess } from '../typescript/typeCheckInAnotherProcess'
import { watchTestMetadata } from '../tests/watchTestMetadata'
import { CompilerOptions } from 'typescript'
import { watchFile } from '../browser/webpack/watchFile'
import { collectByKey } from '../common/collectByKey'
import { Stats } from 'webpack'
import { Results } from './Results'
import { makeAbsolute } from '../common/makeAbsolute'
import { getTestStats, getTestResults } from '../results'
const isEqual: (a: any, b: any) => boolean = require('lodash.isequal')
const uniq = <A>(list: Array<A>): Array<A> => Array.from(new Set(list))
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
const updateQueue = (metadata: TestMetadata[], queue: TestMetadata[]) => {
const queuedMetadataByFilePath = collectByKey(x => x.filePath, queue)
const queuedFilePaths = Object.keys(queuedMetadataByFilePath).sort()
const metadataByFilePath = collectByKey(x => x.filePath, metadata)
const filePaths = Object.keys(metadataByFilePath).sort()
const allFilePaths = uniq([...filePaths, ...queuedFilePaths])
return allFilePaths.reduce(
(xs, filePath) =>
metadataByFilePath[filePath] // replace updated files
? xs.concat(metadataByFilePath[filePath])
: xs.concat(queuedMetadataByFilePath[filePath]),
[] as TestMetadata[],
)
}
export async function watchBrowserTests(
fileGlobs: Array<string>,
compilerOptions: CompilerOptions,
options: TypedTestOptions,
cwd: string,
logger: Logger,
cb: (results: StatsAndResults) => void,
errCb: (error: Error) => void,
typeCheckCb: (results: ProcessResults) => void,
) {
const { keepAlive, timeout, typeCheck, mode } = options
const outputDirectory = tempy.directory()
const temporaryPath = join(outputDirectory, basename(tempy.file({ extension: 'ts' })))
const bundlePath = join(outputDirectory, basename(tempy.file({ extension: 'js' })))
const indexHtmlPath = join(outputDirectory, 'index.html')
const port = await findOpenPort()
const url = `http://localhost:${port}`
const logErrors = (errors: string[]) => Promise.all(errors.map(logger.error))
const makeAbsolutePath = (path: string) => (isAbsolute(path) ? path : join(cwd, path))
const { updateResults, removeFilePath } = new Results()
// shared mutable state
let queuedMetadata: TestMetadata[] = []
let previousMetadata: TestMetadata[] = []
let testsAreRunning = false
let writingToDisk = false
let scheduleNextRunHandle: NodeJS.Timer
let newFileOnDisk = false
let killBrowser: () => void = () => void 0
let previousWebpackHash: string = ''
let firstRun = true
let timeToRunTests: number = 0
let browserOpenTime: number = 0
let testsCompletedTime: number = 0
const killTestBrowser = () => (!keepAlive && killBrowser ? killBrowser() : void 0)
const setupTestBrowser = async () => {
const { browser, keepAlive } = options
if (browser !== 'chrome-headless') {
const launcher = await getLauncher()
const run = async () => {
killTestBrowser()
logger.log('Opening browser...')
browserOpenTime = Date.now()
const instance = await openBrowser(browser, url, keepAlive, launcher)
if (!firstRun) {
const lastCompletion = testsCompletedTime
// Re-run tests if they seem to have stalled - this can happen when browsers are opened in succession quite quickly
// I've only been able to make it do this when I am very rapidly changing and saving a test file to purposely try and break things.
delay(timeToRunTests * 3).then(
() => (testsCompletedTime === lastCompletion ? run() : void 0),
)
}
killBrowser = () => instance.stop()
}
return run
}
const { launch } = require(sync('chrome-launcher', { basedir: cwd }))
const run = async () => {
killTestBrowser()
logger.log('Opening browser...')
browserOpenTime = Date.now()
const chrome = await launch({ startingUrl: url })
if (!firstRun) {
const lastCompletion = testsCompletedTime
delay(timeToRunTests * 3).then(
() => (testsCompletedTime === lastCompletion ? run() : void 0),
)
}
killBrowser = () => chrome.kill()
}
return run
}
const runTestsInBrowser = await setupTestBrowser()
const newStats = async (stats: Stats) => {
const shouldReturnEarly =
(stats as any).hash === previousWebpackHash || !newFileOnDisk || testsAreRunning
if (shouldReturnEarly) {
previousWebpackHash = (stats as any).hash
return
}
if (stats.hasErrors()) {
const { errors } = stats.toJson()
return logErrors(errors)
}
testsAreRunning = true
runTestsInBrowser()
}
const writeToDisk = (metadata: TestMetadata[]) => {
writeFileSync(temporaryPath, generateTestBundle(cwd, outputDirectory, port, timeout, metadata))
newFileOnDisk = true
writingToDisk = false
if (firstRun) {
watchFile(cwd, temporaryPath, bundlePath, options.webpackConfiguration, newStats, errCb)
}
}
const typeCheckMetadata = async (metadata: TestMetadata[]) => {
logger.log('Typechecking...')
const processResults = await typecheckInAnotherProcess(cwd, metadata.map(x => x.filePath))
logger.log('Type checking complete')
typeCheckCb(processResults)
}
const removeFileFromQueue = (filePath: string) => {
const path = makeAbsolute(cwd, filePath)
removeFilePath(path)
queuedMetadata = queuedMetadata.filter(x => makeAbsolutePath(x.filePath) !== path)
}
const scheduleNextBundleWrite = () => {
if (testsAreRunning || writingToDisk) {
clearTimeout(scheduleNextRunHandle)
scheduleNextRunHandle = setTimeout(scheduleNextBundleWrite, 600)
return
}
writingToDisk = true
const currentQueue = queuedMetadata.slice()
writeToDisk(currentQueue)
if (typeCheck) {
typeCheckMetadata(currentQueue)
}
queuedMetadata = []
}
const updateMetadataAndWriteBundle = async (metadata: TestMetadata[]) => {
queuedMetadata = updateQueue(metadata, queuedMetadata)
if (isEqual(metadata, previousMetadata) || queuedMetadata.length === 0) {
return
}
previousMetadata = metadata
scheduleNextBundleWrite()
}
const server = createServer(
setupServer(logger, outputDirectory, newResults => {
firstRun = false
if (!keepAlive && killBrowser) {
killBrowser()
}
const results = updateResults(newResults)
const stats = getTestStats(getTestResults(results))
cb({ results, stats })
testsCompletedTime = Date.now()
timeToRunTests = testsCompletedTime - browserOpenTime
newFileOnDisk = testsAreRunning = false
}),
)
writeFileSync(indexHtmlPath, createIndexHtml(basename(bundlePath)))
server.listen(port, '0.0.0.0')
const { dispose: stopWatchingMetadata } = await watchTestMetadata(
cwd,
fileGlobs,
compilerOptions,
mode,
logger,
removeFileFromQueue,
updateMetadataAndWriteBundle,
)
const dispose = () => {
stopWatchingMetadata()
server.close()
}
return { dispose }
}