[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat: show browser console in the terminal (#3048)

authored by

Vladimir and committed by
GitHub
(Mar 22, 2023, 4:36 PM +0100) ee6f590d 051bb657

+356 -90
+4
packages/vitest/package.json
··· 61 61 "types": "./dist/environments.d.ts", 62 62 "import": "./dist/environments.js" 63 63 }, 64 + "./utils": { 65 + "types": "./dist/utils.d.ts", 66 + "import": "./dist/utils.js" 67 + }, 64 68 "./config": { 65 69 "types": "./config.d.ts", 66 70 "require": "./dist/config.cjs",
+12 -10
packages/vitest/rollup.config.js
··· 28 28 'src/runtime/entry.ts', 29 29 'src/integrations/spy.ts', 30 30 'src/coverage.ts', 31 + 'src/public/utils.ts', 31 32 ] 32 33 33 - const dtsEntries = [ 34 - 'src/index.ts', 35 - 'src/node.ts', 36 - 'src/environments.ts', 37 - 'src/browser.ts', 38 - 'src/runners.ts', 39 - 'src/suite.ts', 40 - 'src/config.ts', 41 - 'src/coverage.ts', 42 - ] 34 + const dtsEntries = { 35 + index: 'src/index.ts', 36 + node: 'src/node.ts', 37 + environments: 'src/environments.ts', 38 + browser: 'src/browser.ts', 39 + runners: 'src/runners.ts', 40 + suite: 'src/suite.ts', 41 + config: 'src/config.ts', 42 + coverage: 'src/coverage.ts', 43 + utils: 'src/public/utils.ts', 44 + } 43 45 44 46 const external = [ 45 47 ...builtinModules,
+1
packages/vitest/utils.d.ts
··· 1 + export * from './dist/utils.js'
+2 -1
test/browser/package.json
··· 1 1 { 2 2 "name": "@vitest/test-browser", 3 3 "private": true, 4 + "module": "true", 4 5 "scripts": { 5 - "test": "node test.mjs", 6 + "test": "node --test specs/", 6 7 "coverage": "vitest run --coverage" 7 8 }, 8 9 "devDependencies": {
-32
test/browser/test.mjs
··· 1 - import assert from 'node:assert' 2 - import { readFile } from 'node:fs/promises' 3 - import { execa } from 'execa' 4 - 5 - const browser = process.env.BROWSER || 'chrome' 6 - 7 - let error 8 - await execa('npx', ['vitest', `--browser=${browser}`], { 9 - env: { 10 - ...process.env, 11 - CI: 'true', 12 - NO_COLOR: 'true', 13 - }, 14 - stdout: 'inherit', 15 - stderr: 'inherit', 16 - }) 17 - .catch((e) => { 18 - error = e 19 - }) 20 - 21 - if (error) { 22 - console.error(error) 23 - process.exit(1) 24 - } 25 - 26 - const browserResult = await readFile('./browser.json', 'utf-8') 27 - const browserResultJson = JSON.parse(browserResult) 28 - 29 - assert.ok(browserResultJson.testResults.length === 4, 'Not all the tests have been run') 30 - 31 - for (const result of browserResultJson.testResults) 32 - assert.ok(result.status === 'passed')
+1
test/browser/vitest.config.ts
··· 4 4 5 5 export default defineConfig({ 6 6 test: { 7 + include: ['test/**.test.{ts,js}'], 7 8 browser: { 8 9 enabled: true, 9 10 name: 'chrome',
+6 -2
packages/utils/src/display.ts
··· 8 8 return util.format(...args) 9 9 } 10 10 11 + export function utilInspect(item: unknown, options?: util.InspectOptions) { 12 + return util.inspect(item, options) 13 + } 14 + 11 15 // chai utils 12 - export function inspect(obj: unknown): string { 16 + export function loupeInspect(obj: unknown): string { 13 17 return loupe(obj, { 14 18 depth: 2, 15 19 truncate: 40, ··· 18 22 19 23 export function objDisplay(obj: unknown) { 20 24 const truncateThreshold = 40 21 - const str = inspect(obj) 25 + const str = loupeInspect(obj) 22 26 const type = Object.prototype.toString.call(obj) 23 27 24 28 if (str.length >= truncateThreshold) {
+51
test/browser/specs/runner.test.mjs
··· 1 + import assert from 'node:assert' 2 + import { readFile } from 'node:fs/promises' 3 + import test from 'node:test' 4 + import { execa } from 'execa' 5 + 6 + const browser = process.env.BROWSER || 'chrome' 7 + 8 + const { stderr, stdout } = await execa('npx', ['vitest', `--browser=${browser}`], { 9 + env: { 10 + ...process.env, 11 + CI: 'true', 12 + NO_COLOR: 'true', 13 + }, 14 + }) 15 + 16 + test('tests are actually running', async () => { 17 + const browserResult = await readFile('./browser.json', 'utf-8') 18 + const browserResultJson = JSON.parse(browserResult) 19 + 20 + assert.ok(browserResultJson.testResults.length === 5, 'Not all the tests have been run') 21 + 22 + for (const result of browserResultJson.testResults) 23 + assert.ok(result.status === 'passed', `${result.name} has failed`) 24 + }) 25 + 26 + test('logs are redirected to stdout', async () => { 27 + assert.match(stdout, /stdout | test\/logs.test.ts > logging to stdout/) 28 + assert.match(stdout, /hello from console.log/, 'prints console.log') 29 + assert.match(stdout, /hello from console.info/, 'prints console.info') 30 + assert.match(stdout, /hello from console.debug/, 'prints console.debug') 31 + assert.match(stdout, /{ hello: 'from dir' }/, 'prints console.dir') 32 + assert.match(stdout, /{ hello: 'from dirxml' }/, 'prints console.dixml') 33 + assert.match(stdout, /hello from console.trace\s+\w+/, 'prints console.trace') 34 + assert.match(stdout, /dom <div \/>/, 'prints dom') 35 + assert.match(stdout, /default: 1/, 'prints first default count') 36 + assert.match(stdout, /default: 2/, 'prints second default count') 37 + assert.match(stdout, /default: 3/, 'prints third default count') 38 + assert.match(stdout, /count: 1/, 'prints first custom count') 39 + assert.match(stdout, /count: 2/, 'prints second custom count') 40 + assert.match(stdout, /count: 3/, 'prints third custom count') 41 + assert.match(stdout, /default: [\d.]+ ms/, 'prints default time') 42 + assert.match(stdout, /time: [\d.]+ ms/, 'prints custom time') 43 + }) 44 + 45 + test('logs are redirected to stderr', async () => { 46 + assert.match(stderr, /stderr | test\/logs.test.ts > logging to stderr/) 47 + assert.match(stderr, /hello from console.error/, 'prints console.log') 48 + assert.match(stderr, /hello from console.warn/, 'prints console.info') 49 + assert.match(stderr, /Timer "invalid timeLog" does not exist/, 'prints errored timeLog') 50 + assert.match(stderr, /Timer "invalid timeEnd" does not exist/, 'prints errored timeEnd') 51 + })
+54
test/browser/test/logs.test.ts
··· 1 + /* eslint-disable no-console */ 2 + import { test } from 'vitest' 3 + 4 + test('logging to stdout', () => { 5 + console.log('hello from console.log') 6 + console.info('hello from console.info') 7 + console.debug('hello from console.debug') 8 + console.dir({ hello: 'from dir' }) 9 + console.dirxml({ hello: 'from dirxml' }) 10 + console.trace('hello from console.trace') 11 + }) 12 + 13 + test('logging to stderr', () => { 14 + console.error('hello from console.error') 15 + console.warn('hello from console.warn') 16 + }) 17 + 18 + test('logging DOM element', () => { 19 + const element = document.createElement('div') 20 + console.log('dom', element) 21 + }) 22 + 23 + test('logging default counter', () => { 24 + console.count() 25 + console.count() 26 + console.count() 27 + console.countReset() 28 + console.count() 29 + }) 30 + 31 + test('logging custom counter', () => { 32 + console.count('count') 33 + console.count('count') 34 + console.count('count') 35 + console.countReset('count') 36 + console.count('count') 37 + }) 38 + 39 + test('logging default time', () => { 40 + console.time() 41 + console.timeLog() 42 + console.timeEnd() 43 + }) 44 + 45 + test('logging custom time', () => { 46 + console.time('time') 47 + console.timeLog('time') 48 + console.timeEnd('time') 49 + }) 50 + 51 + test('logging invalid time', () => { 52 + console.timeLog('invalid timeLog') 53 + console.timeEnd('invalid timeEnd') 54 + })
+104
packages/browser/src/client/logger.ts
··· 1 + import { rpc } from './rpc' 2 + import { importId } from './utils' 3 + 4 + const { Date, console } = globalThis 5 + 6 + export const setupConsoleLogSpy = async () => { 7 + const { stringify, format, utilInspect } = await importId('vitest/utils') as typeof import('vitest/utils') 8 + const { log, info, error, dir, dirxml, trace, time, timeEnd, timeLog, warn, debug, count, countReset } = console 9 + const formatInput = (input: unknown) => { 10 + if (input instanceof Node) 11 + return stringify(input) 12 + return format(input) 13 + } 14 + const processLog = (args: unknown[]) => args.map(formatInput).join(' ') 15 + const sendLog = (type: 'stdout' | 'stderr', content: string) => { 16 + if (content.startsWith('[vite]')) 17 + return 18 + const unknownTestId = '__vitest__unknown_test__' 19 + // @ts-expect-error untyped global 20 + const taskId = globalThis.__vitest_worker__?.current?.id ?? unknownTestId 21 + rpc().sendLog({ 22 + content, 23 + time: Date.now(), 24 + taskId, 25 + type, 26 + size: content.length, 27 + }) 28 + } 29 + const stdout = (base: (...args: unknown[]) => void) => (...args: unknown[]) => { 30 + sendLog('stdout', processLog(args)) 31 + return base(...args) 32 + } 33 + const stderr = (base: (...args: unknown[]) => void) => (...args: unknown[]) => { 34 + sendLog('stderr', processLog(args)) 35 + return base(...args) 36 + } 37 + console.log = stdout(log) 38 + console.debug = stdout(debug) 39 + console.info = stdout(info) 40 + 41 + console.error = stderr(error) 42 + console.warn = stderr(warn) 43 + 44 + console.dir = (item, options) => { 45 + sendLog('stdout', utilInspect(item, options)) 46 + return dir(item, options) 47 + } 48 + 49 + console.dirxml = (...args) => { 50 + sendLog('stdout', processLog(args)) 51 + return dirxml(...args) 52 + } 53 + 54 + console.trace = (...args: unknown[]) => { 55 + const content = processLog(args) 56 + const error = new Error('Trace') 57 + const stack = (error.stack || '').split('\n').slice(2).join('\n') 58 + sendLog('stdout', `${content}\n${stack}`) 59 + return trace(...args) 60 + } 61 + 62 + const timeLabels: Record<string, number> = {} 63 + 64 + console.time = (label = 'default') => { 65 + const now = performance.now() 66 + time(label) 67 + timeLabels[label] = now 68 + } 69 + 70 + console.timeLog = (label = 'default') => { 71 + timeLog(label) 72 + if (!(label in timeLabels)) 73 + sendLog('stderr', `Timer "${label}" does not exist`) 74 + else 75 + sendLog('stdout', `${label}: ${timeLabels[label]} ms`) 76 + } 77 + 78 + console.timeEnd = (label = 'default') => { 79 + const end = performance.now() 80 + timeEnd(label) 81 + const start = timeLabels[label] 82 + if (!(label in timeLabels)) { 83 + sendLog('stderr', `Timer "${label}" does not exist`) 84 + } 85 + else if (start) { 86 + const duration = end - start 87 + sendLog('stdout', `${label}: ${duration} ms`) 88 + } 89 + } 90 + 91 + const countLabels: Record<string, number> = {} 92 + 93 + console.count = (label = 'default') => { 94 + const counter = (countLabels[label] ?? 0) + 1 95 + countLabels[label] = counter 96 + sendLog('stdout', `${label}: ${counter}`) 97 + return count(label) 98 + } 99 + 100 + console.countReset = (label = 'default') => { 101 + countLabels[label] = 0 102 + return countReset(label) 103 + } 104 + }
+16 -12
packages/browser/src/client/main.ts
··· 1 - import type { VitestClient } from '@vitest/ws-client' 2 1 import { createClient } from '@vitest/ws-client' 3 2 // eslint-disable-next-line no-restricted-imports 4 3 import type { ResolvedConfig } from 'vitest' 5 4 import type { VitestRunner } from '@vitest/runner' 6 5 import { createBrowserRunner } from './runner' 7 6 import { BrowserSnapshotEnvironment } from './snapshot' 7 + import { importId } from './utils' 8 + import { setupConsoleLogSpy } from './logger' 9 + import { createSafeRpc, rpc, rpcDone } from './rpc' 8 10 9 11 // @ts-expect-error mocking some node apis 10 12 globalThis.process = { env: {}, argv: [], cwd: () => '/', stdout: { write: () => {} }, nextTick: cb => cb() } ··· 51 53 ws.addEventListener('open', async () => { 52 54 await loadConfig() 53 55 56 + const { getSafeTimers } = await importId('vitest/utils') as typeof import('vitest/utils') 57 + const safeRpc = createSafeRpc(client, getSafeTimers) 58 + 54 59 // @ts-expect-error mocking vitest apis 55 60 globalThis.__vitest_worker__ = { 56 61 config, 57 62 browserHashMap, 58 63 moduleCache: new Map(), 59 64 rpc: client.rpc, 65 + safeRpc, 60 66 } 61 67 62 68 const paths = getQueryPaths() ··· 64 70 const iFrame = document.getElementById('vitest-ui') as HTMLIFrameElement 65 71 iFrame.setAttribute('src', '/__vitest__/') 66 72 67 - await runTests(paths, config, client) 73 + await setupConsoleLogSpy() 74 + await runTests(paths, config) 68 75 }) 69 76 70 77 let hasSnapshot = false 71 - async function runTests(paths: string[], config: any, client: VitestClient) { 78 + async function runTests(paths: string[], config: any) { 72 79 // need to import it before any other import, otherwise Vite optimizer will hang 73 80 const viteClientPath = '/@vite/client' 74 81 await import(viteClientPath) 75 82 76 - // we use dynamic import here, because this file is bundled with UI, 77 - // but we need to resolve correct path at runtime 78 - const path = '/__vitest_index__' 79 - const { startTests, setupCommonEnv, setupSnapshotEnvironment } = await import(path) as typeof import('vitest/browser') 83 + const { startTests, setupCommonEnv, setupSnapshotEnvironment } = await importId('vitest/browser') as typeof import('vitest/browser') 80 84 81 85 if (!runner) { 82 - const runnerPath = '/__vitest_runners__' 83 - const { VitestTestRunner } = await import(runnerPath) as typeof import('vitest/runners') 86 + const { VitestTestRunner } = await importId('vitest/runners') as typeof import('vitest/runners') 84 87 const BrowserRunner = createBrowserRunner(VitestTestRunner) 85 - runner = new BrowserRunner({ config, client, browserHashMap }) 88 + runner = new BrowserRunner({ config, browserHashMap }) 86 89 } 87 90 88 91 if (!hasSnapshot) { 89 - setupSnapshotEnvironment(new BrowserSnapshotEnvironment(client)) 92 + setupSnapshotEnvironment(new BrowserSnapshotEnvironment()) 90 93 hasSnapshot = true 91 94 } 92 95 ··· 102 105 await startTests(files, runner) 103 106 } 104 107 finally { 105 - await client.rpc.onDone(testId) 108 + await rpcDone() 109 + await rpc().onDone(testId) 106 110 } 107 111 }
+74
packages/browser/src/client/rpc.ts
··· 1 + import type { 2 + getSafeTimers, 3 + } from '@vitest/utils' 4 + import type { VitestClient } from '@vitest/ws-client' 5 + 6 + const { get } = Reflect 7 + const safeRandom = Math.random 8 + 9 + function withSafeTimers(getTimers: typeof getSafeTimers, fn: () => void) { 10 + const { setTimeout, clearTimeout, nextTick, setImmediate, clearImmediate } = getTimers() 11 + 12 + const currentSetTimeout = globalThis.setTimeout 13 + const currentClearTimeout = globalThis.clearTimeout 14 + const currentRandom = globalThis.Math.random 15 + const currentNextTick = globalThis.process.nextTick 16 + const currentSetImmediate = globalThis.setImmediate 17 + const currentClearImmediate = globalThis.clearImmediate 18 + 19 + try { 20 + globalThis.setTimeout = setTimeout 21 + globalThis.clearTimeout = clearTimeout 22 + globalThis.Math.random = safeRandom 23 + globalThis.process.nextTick = nextTick 24 + globalThis.setImmediate = setImmediate 25 + globalThis.clearImmediate = clearImmediate 26 + 27 + const result = fn() 28 + return result 29 + } 30 + finally { 31 + globalThis.setTimeout = currentSetTimeout 32 + globalThis.clearTimeout = currentClearTimeout 33 + globalThis.Math.random = currentRandom 34 + globalThis.setImmediate = currentSetImmediate 35 + globalThis.clearImmediate = currentClearImmediate 36 + nextTick(() => { 37 + globalThis.process.nextTick = currentNextTick 38 + }) 39 + } 40 + } 41 + 42 + const promises = new Set<Promise<unknown>>() 43 + 44 + export const rpcDone = async () => { 45 + if (!promises.size) 46 + return 47 + const awaitable = Array.from(promises) 48 + return Promise.all(awaitable) 49 + } 50 + 51 + export const createSafeRpc = (client: VitestClient, getTimers: () => any): VitestClient['rpc'] => { 52 + return new Proxy(client.rpc, { 53 + get(target, p, handler) { 54 + const sendCall = get(target, p, handler) 55 + const safeSendCall = (...args: any[]) => withSafeTimers(getTimers, async () => { 56 + const result = sendCall(...args) 57 + promises.add(result) 58 + try { 59 + return await result 60 + } 61 + finally { 62 + promises.delete(result) 63 + } 64 + }) 65 + safeSendCall.asEvent = sendCall.asEvent 66 + return safeSendCall 67 + }, 68 + }) 69 + } 70 + 71 + export const rpc = (): VitestClient['rpc'] => { 72 + // @ts-expect-error not typed global 73 + return globalThis.__vitest_worker__.safeRpc 74 + }
+3 -6
packages/browser/src/client/runner.ts
··· 1 1 import type { File, TaskResult, Test } from '@vitest/runner' 2 - import type { VitestClient } from '@vitest/ws-client' 2 + import { rpc } from './rpc' 3 3 import type { ResolvedConfig } from '#types' 4 4 5 5 interface BrowserRunnerOptions { 6 6 config: ResolvedConfig 7 - client: VitestClient 8 7 browserHashMap: Map<string, string> 9 8 } 10 9 ··· 12 11 return class BrowserTestRunner extends original { 13 12 public config: ResolvedConfig 14 13 hashMap = new Map<string, string>() 15 - client: VitestClient 16 14 17 15 constructor(options: BrowserRunnerOptions) { 18 16 super(options.config) 19 17 this.config = options.config 20 18 this.hashMap = options.browserHashMap 21 - this.client = options.client 22 19 } 23 20 24 21 async onAfterRunTest(task: Test) { ··· 29 26 } 30 27 31 28 onCollected(files: File[]): unknown { 32 - return this.client.rpc.onCollected(files) 29 + return rpc().onCollected(files) 33 30 } 34 31 35 32 onTaskUpdate(task: [string, TaskResult | undefined][]): Promise<void> { 36 - return this.client.rpc.onTaskUpdate(task) 33 + return rpc().onTaskUpdate(task) 37 34 } 38 35 39 36 async importFile(filepath: string) {
+6 -8
packages/browser/src/client/snapshot.ts
··· 1 - import type { VitestClient } from '@vitest/ws-client' 1 + import { rpc } from './rpc' 2 2 import type { SnapshotEnvironment } from '#types' 3 3 4 4 export class BrowserSnapshotEnvironment implements SnapshotEnvironment { 5 - constructor(private client: VitestClient) {} 6 - 7 5 readSnapshotFile(filepath: string): Promise<string | null> { 8 - return this.client.rpc.readFile(filepath) 6 + return rpc().readFile(filepath) 9 7 } 10 8 11 9 saveSnapshotFile(filepath: string, snapshot: string): Promise<void> { 12 - return this.client.rpc.writeFile(filepath, snapshot) 10 + return rpc().writeFile(filepath, snapshot) 13 11 } 14 12 15 13 resolvePath(filepath: string): Promise<string> { 16 - return this.client.rpc.resolveSnapshotPath(filepath) 14 + return rpc().resolveSnapshotPath(filepath) 17 15 } 18 16 19 17 removeSnapshotFile(filepath: string): Promise<void> { 20 - return this.client.rpc.removeFile(filepath) 18 + return rpc().removeFile(filepath) 21 19 } 22 20 23 21 async prepareDirectory(filepath: string): Promise<void> { 24 - await this.client.rpc.createDirectory(filepath) 22 + await rpc().createDirectory(filepath) 25 23 } 26 24 }
+4
packages/browser/src/client/utils.ts
··· 1 + export const importId = (id: string) => { 2 + const name = `/@id/${id}` 3 + return import(name) 4 + }
+9 -16
packages/browser/src/node/index.ts
··· 18 18 { 19 19 enforce: 'pre', 20 20 name: 'vitest:browser', 21 - async resolveId(id) { 22 - if (id === '/__vitest_index__') 23 - return this.resolve('vitest/browser') 24 - 25 - if (id === '/__vitest_runners__') 26 - return this.resolve('vitest/runners') 27 - 28 - if (id.startsWith('node:')) 29 - id = id.slice(5) 30 - 31 - if (polyfills.includes(id)) 32 - return polyfillPath(normalizeId(id)) 33 - 34 - return null 35 - }, 36 21 async configureServer(server) { 37 22 server.middlewares.use( 38 23 base, ··· 45 30 }, 46 31 { 47 32 name: 'modern-node-polyfills', 33 + enforce: 'pre', 34 + config() { 35 + return { 36 + optimizeDeps: { 37 + exclude: [...polyfills, ...builtinModules], 38 + }, 39 + } 40 + }, 48 41 async resolveId(id) { 49 - if (!builtinModules.includes(id)) 42 + if (!builtinModules.includes(id) && !polyfills.includes(id) && !id.startsWith('node:')) 50 43 return 51 44 52 45 id = normalizeId(id)
+2 -1
packages/runner/src/utils/collect.ts
··· 1 1 import type { Suite, TaskBase } from '../types' 2 + import { processError } from './error' 2 3 3 4 /** 4 5 * If any tasks been marked as `only`, mark all other tasks as `skip`. ··· 65 66 function checkAllowOnly(task: TaskBase, allowOnly?: boolean) { 66 67 if (allowOnly) 67 68 return 68 - const error = new Error('[Vitest] Unexpected .only modifier. Remove it or pass --allowOnly argument to bypass this error') 69 + const error = processError(new Error('[Vitest] Unexpected .only modifier. Remove it or pass --allowOnly argument to bypass this error')) 69 70 task.result = { 70 71 state: 'fail', 71 72 error,
+3
packages/vitest/src/api/setup.ts
··· 52 52 getPaths() { 53 53 return ctx.state.getPaths() 54 54 }, 55 + sendLog(log) { 56 + return ctx.report('onUserConsoleLog', log) 57 + }, 55 58 resolveSnapshotPath(testPath) { 56 59 return ctx.snapshot.resolvePath(testPath) 57 60 },
+2 -1
packages/vitest/src/api/types.ts
··· 1 1 import type { TransformResult } from 'vite' 2 - import type { File, ModuleGraphData, Reporter, ResolvedConfig, SnapshotResult, TaskResultPack } from '../types' 2 + import type { File, ModuleGraphData, Reporter, ResolvedConfig, SnapshotResult, TaskResultPack, UserConsoleLog } from '../types' 3 3 4 4 export interface TransformResultWithSource extends TransformResult { 5 5 source?: string ··· 9 9 onCollected(files?: File[]): Promise<void> 10 10 onTaskUpdate(packs: TaskResultPack[]): void 11 11 onDone(name: string): void 12 + sendLog(log: UserConsoleLog): void 12 13 getFiles(): File[] 13 14 getPaths(): string[] 14 15 getConfig(): ResolvedConfig
+1
packages/vitest/src/public/utils.ts
··· 1 + export * from '@vitest/utils'
+1 -1
packages/vitest/src/runtime/rpc.ts
··· 1 1 import { 2 2 getSafeTimers, 3 3 } from '@vitest/utils' 4 - import { getWorkerState } from '../utils' 4 + import { getWorkerState } from '../utils/global' 5 5 6 6 const { get } = Reflect 7 7 const safeRandom = Math.random