2020-12-20 03:33:13 +00:00
|
|
|
// test utils used in e2e tests for playgrounds.
|
2022-05-11 09:03:19 +00:00
|
|
|
// `import { getColor } from '~utils'`
|
2020-12-20 03:33:13 +00:00
|
|
|
|
2022-06-19 19:59:52 +00:00
|
|
|
import fs from 'node:fs'
|
|
|
|
import path from 'node:path'
|
2020-12-20 03:33:13 +00:00
|
|
|
import colors from 'css-color-names'
|
2023-08-14 09:51:05 +00:00
|
|
|
import type {
|
|
|
|
ConsoleMessage,
|
|
|
|
ElementHandle,
|
|
|
|
Locator,
|
|
|
|
} from 'playwright-chromium'
|
2023-06-07 08:13:24 +00:00
|
|
|
import type { DepOptimizationMetadata, Manifest } from 'vite'
|
2022-03-30 06:56:20 +00:00
|
|
|
import { normalizePath } from 'vite'
|
2022-03-28 12:41:00 +00:00
|
|
|
import { fromComment } from 'convert-source-map'
|
2023-12-02 14:40:45 +00:00
|
|
|
import type { Assertion } from 'vitest'
|
2022-05-11 06:33:20 +00:00
|
|
|
import { expect } from 'vitest'
|
2024-05-13 07:29:57 +00:00
|
|
|
import type { ResultPromise as ExecaResultPromise } from 'execa'
|
2024-11-19 03:01:20 +00:00
|
|
|
import { isWindows, page, testDir } from './vitestSetup'
|
2022-05-11 09:03:19 +00:00
|
|
|
|
2022-05-11 10:09:52 +00:00
|
|
|
export * from './vitestSetup'
|
2022-05-11 09:03:19 +00:00
|
|
|
|
2022-04-04 18:33:48 +00:00
|
|
|
// make sure these ports are unique
|
|
|
|
export const ports = {
|
|
|
|
cli: 9510,
|
|
|
|
'cli-module': 9511,
|
2023-05-09 09:33:34 +00:00
|
|
|
json: 9512,
|
2022-04-04 18:33:48 +00:00
|
|
|
'legacy/ssr': 9520,
|
|
|
|
lib: 9521,
|
|
|
|
'optimize-missing-deps': 9522,
|
2022-08-24 19:09:01 +00:00
|
|
|
'legacy/client-and-ssr': 9523,
|
2024-08-01 02:53:07 +00:00
|
|
|
'assets/encoded-base': 9554, // not imported but used in `assets/vite.config-encoded-base.js`
|
|
|
|
'assets/url-base': 9525, // not imported but used in `assets/vite.config-url-base.js`
|
2023-03-22 09:12:02 +00:00
|
|
|
ssr: 9600,
|
|
|
|
'ssr-deps': 9601,
|
|
|
|
'ssr-html': 9602,
|
|
|
|
'ssr-noexternal': 9603,
|
|
|
|
'ssr-pug': 9604,
|
|
|
|
'ssr-webworker': 9605,
|
2024-01-26 05:15:31 +00:00
|
|
|
'proxy-bypass': 9606, // not imported but used in `proxy-hmr/vite.config.js`
|
|
|
|
'proxy-bypass/non-existent-app': 9607, // not imported but used in `proxy-hmr/other-app/vite.config.js`
|
2024-02-14 15:16:55 +00:00
|
|
|
'ssr-hmr': 9609, // not imported but used in `hmr-ssr/__tests__/hmr.spec.ts`
|
2024-01-26 05:15:31 +00:00
|
|
|
'proxy-hmr': 9616, // not imported but used in `proxy-hmr/vite.config.js`
|
|
|
|
'proxy-hmr/other-app': 9617, // not imported but used in `proxy-hmr/other-app/vite.config.js`
|
|
|
|
'ssr-conditions': 9620,
|
2022-04-04 18:33:48 +00:00
|
|
|
'css/postcss-caching': 5005,
|
2022-09-23 09:55:09 +00:00
|
|
|
'css/postcss-plugins-different-dir': 5006,
|
|
|
|
'css/dynamic-import': 5007,
|
2023-07-24 12:55:08 +00:00
|
|
|
'css/lightningcss-proxy': 5008,
|
2024-08-20 07:52:19 +00:00
|
|
|
'backend-integration': 5009,
|
2024-10-25 04:54:11 +00:00
|
|
|
'client-reload': 5010,
|
|
|
|
'client-reload/hmr-port': 5011,
|
|
|
|
'client-reload/cross-origin': 5012,
|
2022-04-04 18:33:48 +00:00
|
|
|
}
|
2022-05-17 12:25:34 +00:00
|
|
|
export const hmrPorts = {
|
|
|
|
'optimize-missing-deps': 24680,
|
2023-03-22 09:12:02 +00:00
|
|
|
ssr: 24681,
|
|
|
|
'ssr-deps': 24682,
|
|
|
|
'ssr-html': 24683,
|
|
|
|
'ssr-noexternal': 24684,
|
|
|
|
'ssr-pug': 24685,
|
2023-07-24 12:55:08 +00:00
|
|
|
'css/lightningcss-proxy': 24686,
|
2023-09-26 12:33:30 +00:00
|
|
|
json: 24687,
|
2023-10-05 08:54:06 +00:00
|
|
|
'ssr-conditions': 24688,
|
2024-10-25 04:54:11 +00:00
|
|
|
'client-reload/hmr-port': 24689,
|
|
|
|
'client-reload/cross-origin': 24690,
|
2022-05-17 12:25:34 +00:00
|
|
|
}
|
2022-04-04 18:33:48 +00:00
|
|
|
|
2020-12-20 03:33:13 +00:00
|
|
|
const hexToNameMap: Record<string, string> = {}
|
|
|
|
Object.keys(colors).forEach((color) => {
|
|
|
|
hexToNameMap[colors[color]] = color
|
|
|
|
})
|
|
|
|
|
|
|
|
function componentToHex(c: number): string {
|
2021-05-29 14:04:50 +00:00
|
|
|
const hex = c.toString(16)
|
2021-07-13 15:05:08 +00:00
|
|
|
return hex.length === 1 ? '0' + hex : hex
|
2020-12-20 03:33:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function rgbToHex(rgb: string): string {
|
2020-12-22 21:27:36 +00:00
|
|
|
const match = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/)
|
|
|
|
if (match) {
|
|
|
|
const [_, rs, gs, bs] = match
|
|
|
|
return (
|
|
|
|
'#' +
|
|
|
|
componentToHex(parseInt(rs, 10)) +
|
|
|
|
componentToHex(parseInt(gs, 10)) +
|
|
|
|
componentToHex(parseInt(bs, 10))
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
return '#000000'
|
|
|
|
}
|
2020-12-20 03:33:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const timeout = (n: number) => new Promise((r) => setTimeout(r, n))
|
|
|
|
|
2023-08-14 09:51:05 +00:00
|
|
|
async function toEl(
|
|
|
|
el: string | ElementHandle | Locator,
|
|
|
|
): Promise<ElementHandle> {
|
2020-12-20 03:33:13 +00:00
|
|
|
if (typeof el === 'string') {
|
2023-03-30 17:22:08 +00:00
|
|
|
const realEl = await page.$(el)
|
|
|
|
if (realEl == null) {
|
|
|
|
throw new Error(`Cannot find element: "${el}"`)
|
|
|
|
}
|
|
|
|
return realEl
|
2020-12-20 03:33:13 +00:00
|
|
|
}
|
2023-08-14 09:51:05 +00:00
|
|
|
if ('elementHandle' in el) {
|
|
|
|
return el.elementHandle()
|
|
|
|
}
|
2020-12-20 03:33:13 +00:00
|
|
|
return el
|
|
|
|
}
|
|
|
|
|
2023-08-14 09:51:05 +00:00
|
|
|
export async function getColor(
|
|
|
|
el: string | ElementHandle | Locator,
|
|
|
|
): Promise<string> {
|
2020-12-20 03:33:13 +00:00
|
|
|
el = await toEl(el)
|
|
|
|
const rgb = await el.evaluate((el) => getComputedStyle(el as Element).color)
|
2022-02-15 07:21:55 +00:00
|
|
|
return hexToNameMap[rgbToHex(rgb)] ?? rgb
|
2020-12-20 03:33:13 +00:00
|
|
|
}
|
|
|
|
|
2023-08-14 09:51:05 +00:00
|
|
|
export async function getBg(
|
|
|
|
el: string | ElementHandle | Locator,
|
|
|
|
): Promise<string> {
|
2020-12-21 23:37:04 +00:00
|
|
|
el = await toEl(el)
|
|
|
|
return el.evaluate((el) => getComputedStyle(el as Element).backgroundImage)
|
|
|
|
}
|
|
|
|
|
2023-08-14 09:51:05 +00:00
|
|
|
export async function getBgColor(
|
|
|
|
el: string | ElementHandle | Locator,
|
|
|
|
): Promise<string> {
|
2022-03-03 09:31:45 +00:00
|
|
|
el = await toEl(el)
|
|
|
|
return el.evaluate((el) => getComputedStyle(el as Element).backgroundColor)
|
|
|
|
}
|
|
|
|
|
2021-07-29 05:18:57 +00:00
|
|
|
export function readFile(filename: string): string {
|
2022-05-12 05:12:43 +00:00
|
|
|
return fs.readFileSync(path.resolve(testDir, filename), 'utf-8')
|
2021-03-29 20:20:21 +00:00
|
|
|
}
|
|
|
|
|
2021-06-21 15:50:26 +00:00
|
|
|
export function editFile(
|
|
|
|
filename: string,
|
|
|
|
replacer: (str: string) => string,
|
|
|
|
): void {
|
2022-05-12 05:12:43 +00:00
|
|
|
filename = path.resolve(testDir, filename)
|
2020-12-20 03:33:13 +00:00
|
|
|
const content = fs.readFileSync(filename, 'utf-8')
|
|
|
|
const modified = replacer(content)
|
|
|
|
fs.writeFileSync(filename, modified)
|
|
|
|
}
|
|
|
|
|
2021-07-29 05:18:57 +00:00
|
|
|
export function addFile(filename: string, content: string): void {
|
2024-09-12 09:34:07 +00:00
|
|
|
const resolvedFilename = path.resolve(testDir, filename)
|
|
|
|
fs.mkdirSync(path.dirname(resolvedFilename), { recursive: true })
|
|
|
|
fs.writeFileSync(resolvedFilename, content)
|
2021-01-20 22:45:41 +00:00
|
|
|
}
|
|
|
|
|
2021-07-29 05:18:57 +00:00
|
|
|
export function removeFile(filename: string): void {
|
2022-05-12 05:12:43 +00:00
|
|
|
fs.unlinkSync(path.resolve(testDir, filename))
|
2021-01-20 22:45:41 +00:00
|
|
|
}
|
|
|
|
|
2021-07-29 05:18:57 +00:00
|
|
|
export function listAssets(base = ''): string[] {
|
2022-05-12 05:12:43 +00:00
|
|
|
const assetsDir = path.join(testDir, 'dist', base, 'assets')
|
2021-02-01 15:59:42 +00:00
|
|
|
return fs.readdirSync(assetsDir)
|
|
|
|
}
|
|
|
|
|
2022-05-18 05:07:41 +00:00
|
|
|
export function findAssetFile(
|
|
|
|
match: string | RegExp,
|
|
|
|
base = '',
|
|
|
|
assets = 'assets',
|
2024-03-12 13:13:13 +00:00
|
|
|
matchAll = false,
|
2022-05-18 05:07:41 +00:00
|
|
|
): string {
|
|
|
|
const assetsDir = path.join(testDir, 'dist', base, assets)
|
2022-07-28 08:58:37 +00:00
|
|
|
let files: string[]
|
|
|
|
try {
|
|
|
|
files = fs.readdirSync(assetsDir)
|
|
|
|
} catch (e) {
|
|
|
|
if (e.code === 'ENOENT') {
|
|
|
|
return ''
|
|
|
|
}
|
|
|
|
throw e
|
|
|
|
}
|
2024-03-12 13:13:13 +00:00
|
|
|
if (matchAll) {
|
|
|
|
const matchedFiles = files.filter((file) => file.match(match))
|
|
|
|
return matchedFiles.length
|
|
|
|
? matchedFiles
|
|
|
|
.map((file) =>
|
|
|
|
fs.readFileSync(path.resolve(assetsDir, file), 'utf-8'),
|
|
|
|
)
|
|
|
|
.join('')
|
|
|
|
: ''
|
|
|
|
} else {
|
|
|
|
const matchedFile = files.find((file) => file.match(match))
|
|
|
|
return matchedFile
|
|
|
|
? fs.readFileSync(path.resolve(assetsDir, matchedFile), 'utf-8')
|
|
|
|
: ''
|
|
|
|
}
|
2020-12-30 23:47:35 +00:00
|
|
|
}
|
|
|
|
|
2021-07-29 05:18:57 +00:00
|
|
|
export function readManifest(base = ''): Manifest {
|
2021-02-01 15:59:42 +00:00
|
|
|
return JSON.parse(
|
2023-09-18 08:40:49 +00:00
|
|
|
fs.readFileSync(
|
|
|
|
path.join(testDir, 'dist', base, '.vite/manifest.json'),
|
|
|
|
'utf-8',
|
|
|
|
),
|
2021-02-01 15:59:42 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-06-07 08:13:24 +00:00
|
|
|
export function readDepOptimizationMetadata(): DepOptimizationMetadata {
|
|
|
|
return JSON.parse(
|
|
|
|
fs.readFileSync(
|
|
|
|
path.join(testDir, 'node_modules/.vite/deps/_metadata.json'),
|
|
|
|
'utf-8',
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-12-20 03:33:13 +00:00
|
|
|
/**
|
|
|
|
* Poll a getter until the value it returns includes the expected value.
|
|
|
|
*/
|
|
|
|
export async function untilUpdated(
|
2020-12-23 04:07:57 +00:00
|
|
|
poll: () => string | Promise<string>,
|
2023-04-02 08:58:15 +00:00
|
|
|
expected: string | RegExp,
|
2021-07-29 05:18:57 +00:00
|
|
|
): Promise<void> {
|
2022-06-13 17:34:46 +00:00
|
|
|
const maxTries = process.env.CI ? 200 : 50
|
2020-12-20 03:33:13 +00:00
|
|
|
for (let tries = 0; tries < maxTries; tries++) {
|
2022-02-15 07:21:55 +00:00
|
|
|
const actual = (await poll()) ?? ''
|
2023-04-02 08:58:15 +00:00
|
|
|
if (
|
|
|
|
(typeof expected === 'string'
|
|
|
|
? actual.indexOf(expected) > -1
|
|
|
|
: actual.match(expected)) ||
|
|
|
|
tries === maxTries - 1
|
|
|
|
) {
|
2020-12-20 03:33:13 +00:00
|
|
|
expect(actual).toMatch(expected)
|
|
|
|
break
|
|
|
|
} else {
|
|
|
|
await timeout(50)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-06-21 15:50:26 +00:00
|
|
|
|
2022-07-03 16:53:28 +00:00
|
|
|
/**
|
|
|
|
* Retry `func` until it does not throw error.
|
|
|
|
*/
|
2024-11-19 02:53:59 +00:00
|
|
|
export async function withRetry(func: () => Promise<void>): Promise<void> {
|
2022-07-03 16:53:28 +00:00
|
|
|
const maxTries = process.env.CI ? 200 : 50
|
|
|
|
for (let tries = 0; tries < maxTries; tries++) {
|
|
|
|
try {
|
|
|
|
await func()
|
|
|
|
return
|
|
|
|
} catch {}
|
|
|
|
await timeout(50)
|
|
|
|
}
|
|
|
|
await func()
|
|
|
|
}
|
|
|
|
|
2023-12-02 14:40:45 +00:00
|
|
|
export const expectWithRetry = <T>(getActual: () => Promise<T>) => {
|
|
|
|
return new Proxy(
|
|
|
|
{},
|
|
|
|
{
|
|
|
|
get(_target, key) {
|
|
|
|
return async (...args) => {
|
2024-11-19 02:53:59 +00:00
|
|
|
await withRetry(async () => expect(await getActual())[key](...args))
|
2023-12-02 14:40:45 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
2024-07-22 03:48:47 +00:00
|
|
|
) as Assertion<T>['resolves']
|
|
|
|
// NOTE: `Assertion<T>['resolves']` has the special "promisify all assertion property functions"
|
|
|
|
// behaviour that we're lending here, which is the same as `PromisifyAssertion<T>` if Vitest exposes it
|
2023-12-02 14:40:45 +00:00
|
|
|
}
|
|
|
|
|
2022-11-25 15:49:08 +00:00
|
|
|
type UntilBrowserLogAfterCallback = (logs: string[]) => PromiseLike<void> | void
|
|
|
|
|
|
|
|
export async function untilBrowserLogAfter(
|
|
|
|
operation: () => any,
|
|
|
|
target: string | RegExp | Array<string | RegExp>,
|
|
|
|
expectOrder?: boolean,
|
|
|
|
callback?: UntilBrowserLogAfterCallback,
|
|
|
|
): Promise<string[]>
|
2022-06-20 12:57:51 +00:00
|
|
|
export async function untilBrowserLogAfter(
|
|
|
|
operation: () => any,
|
|
|
|
target: string | RegExp | Array<string | RegExp>,
|
2022-11-25 15:49:08 +00:00
|
|
|
callback?: UntilBrowserLogAfterCallback,
|
|
|
|
): Promise<string[]>
|
|
|
|
export async function untilBrowserLogAfter(
|
|
|
|
operation: () => any,
|
|
|
|
target: string | RegExp | Array<string | RegExp>,
|
|
|
|
arg3?: boolean | UntilBrowserLogAfterCallback,
|
|
|
|
arg4?: UntilBrowserLogAfterCallback,
|
2022-06-20 12:57:51 +00:00
|
|
|
): Promise<string[]> {
|
2022-11-25 15:49:08 +00:00
|
|
|
const expectOrder = typeof arg3 === 'boolean' ? arg3 : false
|
|
|
|
const callback = typeof arg3 === 'boolean' ? arg4 : arg3
|
|
|
|
|
|
|
|
const promise = untilBrowserLog(target, expectOrder)
|
2022-06-20 12:57:51 +00:00
|
|
|
await operation()
|
|
|
|
const logs = await promise
|
|
|
|
if (callback) {
|
|
|
|
await callback(logs)
|
|
|
|
}
|
|
|
|
return logs
|
|
|
|
}
|
|
|
|
|
|
|
|
async function untilBrowserLog(
|
|
|
|
target?: string | RegExp | Array<string | RegExp>,
|
|
|
|
expectOrder = true,
|
|
|
|
): Promise<string[]> {
|
2023-11-29 07:13:27 +00:00
|
|
|
const { promise, resolve, reject } = promiseWithResolvers<void>()
|
2022-06-20 12:57:51 +00:00
|
|
|
|
|
|
|
const logs = []
|
|
|
|
|
|
|
|
try {
|
|
|
|
const isMatch = (matcher: string | RegExp) => (text: string) =>
|
|
|
|
typeof matcher === 'string' ? text === matcher : matcher.test(text)
|
|
|
|
|
|
|
|
let processMsg: (text: string) => boolean
|
|
|
|
|
|
|
|
if (!target) {
|
|
|
|
processMsg = () => true
|
|
|
|
} else if (Array.isArray(target)) {
|
|
|
|
if (expectOrder) {
|
|
|
|
const remainingTargets = [...target]
|
|
|
|
processMsg = (text: string) => {
|
|
|
|
const nextTarget = remainingTargets.shift()
|
|
|
|
expect(text).toMatch(nextTarget)
|
|
|
|
return remainingTargets.length === 0
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const remainingMatchers = target.map(isMatch)
|
|
|
|
processMsg = (text: string) => {
|
|
|
|
const nextIndex = remainingMatchers.findIndex((matcher) =>
|
|
|
|
matcher(text),
|
|
|
|
)
|
|
|
|
if (nextIndex >= 0) {
|
|
|
|
remainingMatchers.splice(nextIndex, 1)
|
|
|
|
}
|
|
|
|
return remainingMatchers.length === 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
processMsg = isMatch(target)
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleMsg = (msg: ConsoleMessage) => {
|
|
|
|
try {
|
|
|
|
const text = msg.text()
|
|
|
|
logs.push(text)
|
|
|
|
const done = processMsg(text)
|
|
|
|
if (done) {
|
|
|
|
resolve()
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
reject(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
page.on('console', handleMsg)
|
|
|
|
} catch (err) {
|
|
|
|
reject(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
await promise
|
|
|
|
|
|
|
|
return logs
|
|
|
|
}
|
|
|
|
|
2022-06-13 17:34:46 +00:00
|
|
|
export const extractSourcemap = (content: string): any => {
|
2022-03-28 12:41:00 +00:00
|
|
|
const lines = content.trim().split('\n')
|
|
|
|
return fromComment(lines[lines.length - 1]).toObject()
|
|
|
|
}
|
|
|
|
|
2022-06-13 17:34:46 +00:00
|
|
|
export const formatSourcemapForSnapshot = (map: any): any => {
|
2022-05-12 05:12:43 +00:00
|
|
|
const root = normalizePath(testDir)
|
2022-03-28 12:41:00 +00:00
|
|
|
const m = { ...map }
|
|
|
|
delete m.file
|
|
|
|
delete m.names
|
|
|
|
m.sources = m.sources.map((source) => source.replace(root, '/root'))
|
2023-05-15 10:20:23 +00:00
|
|
|
if (m.sourceRoot) {
|
|
|
|
m.sourceRoot = m.sourceRoot.replace(root, '/root')
|
|
|
|
}
|
2022-03-28 12:41:00 +00:00
|
|
|
return m
|
|
|
|
}
|
2022-05-12 05:12:43 +00:00
|
|
|
|
|
|
|
// helper function to kill process, uses taskkill on windows to ensure child process is killed too
|
|
|
|
export async function killProcess(
|
2024-05-13 07:29:57 +00:00
|
|
|
serverProcess: ExecaResultPromise,
|
2022-05-12 05:12:43 +00:00
|
|
|
): Promise<void> {
|
|
|
|
if (isWindows) {
|
|
|
|
try {
|
2022-06-13 18:26:27 +00:00
|
|
|
const { execaCommandSync } = await import('execa')
|
|
|
|
execaCommandSync(`taskkill /pid ${serverProcess.pid} /T /F`)
|
2022-05-12 05:12:43 +00:00
|
|
|
} catch (e) {
|
|
|
|
console.error('failed to taskkill:', e)
|
|
|
|
}
|
|
|
|
} else {
|
2024-05-13 07:29:57 +00:00
|
|
|
serverProcess.kill('SIGTERM')
|
2022-05-12 05:12:43 +00:00
|
|
|
}
|
|
|
|
}
|
2023-11-29 07:13:27 +00:00
|
|
|
|
|
|
|
export interface PromiseWithResolvers<T> {
|
|
|
|
promise: Promise<T>
|
|
|
|
resolve: (value: T | PromiseLike<T>) => void
|
|
|
|
reject: (reason?: any) => void
|
|
|
|
}
|
|
|
|
export function promiseWithResolvers<T>(): PromiseWithResolvers<T> {
|
|
|
|
let resolve: any
|
|
|
|
let reject: any
|
|
|
|
const promise = new Promise<T>((_resolve, _reject) => {
|
|
|
|
resolve = _resolve
|
|
|
|
reject = _reject
|
|
|
|
})
|
|
|
|
return { promise, resolve, reject }
|
|
|
|
}
|