You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vitepress/examples/vitestSetup.ts

234 lines
6.2 KiB

import fs from 'fs-extra'
import * as http from 'http'
import os from 'os'
import path, { dirname, join, resolve } from 'path'
import type { Browser, Page } from 'playwright-chromium'
import { chromium } from 'playwright-chromium'
import type { RollupWatcher } from 'rollup'
import sirv from 'sirv'
import type { ResolvedConfig, ServerOptions, ViteDevServer } from 'vite'
import { build, createServer } from 'vitepress'
import type { File } from 'vitest'
import { beforeAll } from 'vitest'
type VitePressBuildOptions = Parameters<typeof build>[1]
// #region env
export const workspaceRoot = resolve(__dirname, '../')
export const isBuild = !!process.env.VITE_TEST_BUILD
export const isServe = !isBuild
export const isWindows = process.platform === 'win32'
export const vitePressBinPath = path.posix.join(
workspaceRoot,
'bin/vitepress.js'
)
// #endregion
// #region context
let server: ViteDevServer | http.Server
/**
* Vite Dev Server when testing serve
*/
export let viteServer: ViteDevServer
/**
* Root of the VitePress fixture
*/
export let rootDir: string
/**
* Path to the current test file
*/
export let testPath: string
/**
* Path to the test folder
*/
export let testDir: string
/**
* Test folder name
*/
export let testName: string
export const serverLogs: string[] = []
export const browserLogs: string[] = []
export const browserErrors: Error[] = []
export let resolvedConfig: ResolvedConfig = undefined!
export let page: Page = undefined!
export let browser: Browser = undefined!
export let vitePressTestUrl: string = ''
export let watcher: RollupWatcher | undefined = undefined
declare module 'vite' {
interface InlineConfig {
testConfig?: {
// relative base output use relative path
// rewrite the url to truth file path
baseRoute: string
}
}
}
export function setViteUrl(url: string): void {
vitePressTestUrl = url
}
// #endregion
const DIR = join(os.tmpdir(), 'vitest_playwright_global_setup')
beforeAll(async (s) => {
const suite = s as File
// skip browser setup for non-examples tests
if (!suite.filepath.includes('examples')) {
return
}
const wsEndpoint = fs.readFileSync(join(DIR, 'wsEndpoint'), 'utf-8')
if (!wsEndpoint) {
throw new Error('wsEndpoint not found')
}
browser = await chromium.connect(wsEndpoint)
page = await browser.newPage()
const globalConsole = global.console
const warn = globalConsole.warn
globalConsole.warn = (msg, ...args) => {
// suppress @vue/reactivity-transform warning
if (msg.includes('@vue/reactivity-transform')) return
if (msg.includes('Generated an empty chunk')) return
warn.call(globalConsole, msg, ...args)
}
try {
page.on('console', (msg) => {
browserLogs.push(msg.text())
})
page.on('pageerror', (error) => {
browserErrors.push(error)
})
testPath = suite.filepath!
testName = slash(testPath).match(/examples\/([\w-]+)\//)?.[1]
testDir = dirname(testPath)
// if this is a test placed under examples/xxx/__tests__
// start a vite server in that directory.
if (testName) {
testDir = resolve(workspaceRoot, 'examples-temp', testName)
// when `root` dir is present, use it as vite's root
const testCustomRoot = resolve(testDir, 'root')
rootDir = fs.existsSync(testCustomRoot) ? testCustomRoot : testDir
await startDefaultServe()
}
} catch (e) {
// Closing the page since an error in the setup, for example a runtime error
// when building the examples should skip further tests.
// If the page remains open, a command like `await page.click(...)` produces
// a timeout with an exception that hides the real error in the console.
await page.close()
await server?.close()
throw e
}
return async () => {
serverLogs.length = 0
await page?.close()
await server?.close()
watcher?.close()
if (browser) {
await browser.close()
}
}
})
export async function startDefaultServe(): Promise<void> {
const options: ServerOptions = undefined
setupConsoleWarnCollector(serverLogs)
if (!isBuild) {
viteServer = server = await (await createServer(rootDir, options)).listen()
const devBase = server.config.base
vitePressTestUrl = `http://localhost:${server.config.server.port}${
devBase === '/' ? '' : devBase
}`
await page.goto(vitePressTestUrl)
// TODO: A manual reload is needed because the first load of page will crash
// because of multiple vue instances. (see https://github.com/vuejs/vitepress/issues/1016)
// Try to remove this after migrating to Vite3.
if (isServe) {
await page.reload()
}
} else {
const options: VitePressBuildOptions = {}
await build(rootDir, options)
vitePressTestUrl = await startStaticServer()
await page.goto(vitePressTestUrl)
}
}
function startStaticServer(config?: VitePressBuildOptions): Promise<string> {
if (!config) {
// check if the test project has base config
const configFile = resolve(rootDir, '.vitepress/config.ts')
try {
config = require(configFile)
} catch (e) {}
}
// fallback internal base to ''
let base = config?.base
if (!base || base === '/' || base === './') {
base = ''
}
// start static file server
const serve = sirv(resolve(rootDir, '.vitepress/dist'))
const baseDir = config?.base
const httpServer = (server = http.createServer((req, res) => {
if (req.url === '/ping') {
res.statusCode = 200
res.end('pong')
} else {
if (baseDir) {
req.url = path.posix.join(baseDir, req.url)
}
serve(req, res)
}
}))
let port = 4173
return new Promise((resolve, reject) => {
const onError = (e: any) => {
if (e.code === 'EADDRINUSE') {
httpServer.close()
httpServer.listen(++port)
} else {
reject(e)
}
}
httpServer.on('error', onError)
httpServer.listen(port, () => {
httpServer.removeListener('error', onError)
resolve(`http://localhost:${port}${base}`)
})
})
}
function setupConsoleWarnCollector(logs: string[]) {
const warn = console.warn
console.warn = (...args) => {
serverLogs.push(args.join(' '))
return warn.call(console, ...args)
}
}
export function slash(p: string): string {
return p.replace(/\\/g, '/')
}