672 lines
18 KiB
JavaScript
672 lines
18 KiB
JavaScript
import { existsSync, writeFileSync, readFileSync } from 'node:fs';
|
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
import { resolve, dirname, relative } from 'node:path';
|
|
import { detectPackageManager, installPackage } from './index.CqYx2Nsr.js';
|
|
import { p as prompt, f as findUp } from './index.BJDntFik.js';
|
|
import { x } from 'tinyexec';
|
|
import c from 'tinyrainbow';
|
|
import { c as configFiles } from './constants.fzPh7AOq.js';
|
|
import 'process';
|
|
import 'node:process';
|
|
import 'fs';
|
|
import 'path';
|
|
import 'node:url';
|
|
import './_commonjsHelpers.BFTU3MAI.js';
|
|
import 'readline';
|
|
import 'events';
|
|
|
|
const jsxExample = {
|
|
name: "HelloWorld.jsx",
|
|
js: `
|
|
export default function HelloWorld({ name }) {
|
|
return (
|
|
<div>
|
|
<h1>Hello {name}!</h1>
|
|
</div>
|
|
)
|
|
}
|
|
`,
|
|
ts: `
|
|
export default function HelloWorld({ name }: { name: string }) {
|
|
return (
|
|
<div>
|
|
<h1>Hello {name}!</h1>
|
|
</div>
|
|
)
|
|
}
|
|
`,
|
|
test: `
|
|
import { expect, test } from 'vitest'
|
|
import { render } from '@testing-library/jsx'
|
|
import HelloWorld from './HelloWorld.jsx'
|
|
|
|
test('renders name', async () => {
|
|
const { getByText } = render(<HelloWorld name="Vitest" />)
|
|
await expect.element(getByText('Hello Vitest!')).toBeInTheDocument()
|
|
})
|
|
`
|
|
};
|
|
const vueExample = {
|
|
name: "HelloWorld.vue",
|
|
js: `
|
|
<script setup>
|
|
defineProps({
|
|
name: String
|
|
})
|
|
<\/script>
|
|
|
|
<template>
|
|
<div>
|
|
<h1>Hello {{ name }}!</h1>
|
|
</div>
|
|
</template>
|
|
`,
|
|
ts: `
|
|
<script setup lang="ts">
|
|
defineProps<{
|
|
name: string
|
|
}>()
|
|
<\/script>
|
|
|
|
<template>
|
|
<div>
|
|
<h1>Hello {{ name }}!</h1>
|
|
</div>
|
|
</template>
|
|
`,
|
|
test: `
|
|
import { expect, test } from 'vitest'
|
|
import { render } from 'vitest-browser-vue'
|
|
import HelloWorld from './HelloWorld.vue'
|
|
|
|
test('renders name', async () => {
|
|
const { getByText } = render(HelloWorld, {
|
|
props: { name: 'Vitest' },
|
|
})
|
|
await expect.element(getByText('Hello Vitest!')).toBeInTheDocument()
|
|
})
|
|
`
|
|
};
|
|
const svelteExample = {
|
|
name: "HelloWorld.svelte",
|
|
js: `
|
|
<script>
|
|
export let name
|
|
<\/script>
|
|
|
|
<h1>Hello {name}!</h1>
|
|
`,
|
|
ts: `
|
|
<script lang="ts">
|
|
export let name: string
|
|
<\/script>
|
|
|
|
<h1>Hello {name}!</h1>
|
|
`,
|
|
test: `
|
|
import { expect, test } from 'vitest'
|
|
import { render } from 'vitest-browser-svelte'
|
|
import HelloWorld from './HelloWorld.svelte'
|
|
|
|
test('renders name', async () => {
|
|
const { getByText } = render(HelloWorld, { name: 'Vitest' })
|
|
await expect.element(getByText('Hello Vitest!')).toBeInTheDocument()
|
|
})
|
|
`
|
|
};
|
|
const markoExample = {
|
|
name: "HelloWorld.marko",
|
|
js: `
|
|
class {
|
|
onCreate() {
|
|
this.state = { name: null }
|
|
}
|
|
}
|
|
|
|
<h1>Hello \${state.name}!</h1>
|
|
`,
|
|
ts: `
|
|
export interface Input {
|
|
name: string
|
|
}
|
|
|
|
<h1>Hello \${input.name}!</h1>
|
|
`,
|
|
test: `
|
|
import { expect, test } from 'vitest'
|
|
import { render } from '@marko/testing-library'
|
|
import HelloWorld from './HelloWorld.svelte'
|
|
|
|
test('renders name', async () => {
|
|
const { getByText } = await render(HelloWorld, { name: 'Vitest' })
|
|
const element = getByText('Hello Vitest!')
|
|
expect(element).toBeInTheDocument()
|
|
})
|
|
`
|
|
};
|
|
const vanillaExample = {
|
|
name: "HelloWorld.js",
|
|
js: `
|
|
export default function HelloWorld({ name }) {
|
|
const parent = document.createElement('div')
|
|
|
|
const h1 = document.createElement('h1')
|
|
h1.textContent = 'Hello ' + name + '!'
|
|
parent.appendChild(h1)
|
|
|
|
return parent
|
|
}
|
|
`,
|
|
ts: `
|
|
export default function HelloWorld({ name }: { name: string }): HTMLDivElement {
|
|
const parent = document.createElement('div')
|
|
|
|
const h1 = document.createElement('h1')
|
|
h1.textContent = 'Hello ' + name + '!'
|
|
parent.appendChild(h1)
|
|
|
|
return parent
|
|
}
|
|
`,
|
|
test: `
|
|
import { expect, test } from 'vitest'
|
|
import { getByText } from '@testing-library/dom'
|
|
import HelloWorld from './HelloWorld.js'
|
|
|
|
test('renders name', () => {
|
|
const parent = HelloWorld({ name: 'Vitest' })
|
|
document.body.appendChild(parent)
|
|
|
|
const element = getByText(parent, 'Hello Vitest!')
|
|
expect(element).toBeInTheDocument()
|
|
})
|
|
`
|
|
};
|
|
function getExampleTest(framework) {
|
|
switch (framework) {
|
|
case "solid":
|
|
case "preact":
|
|
return {
|
|
...jsxExample,
|
|
test: jsxExample.test.replace("@testing-library/jsx", `@testing-library/${framework}`)
|
|
};
|
|
case "react":
|
|
return {
|
|
...jsxExample,
|
|
test: jsxExample.test.replace("@testing-library/jsx", "vitest-browser-react")
|
|
};
|
|
case "vue":
|
|
return vueExample;
|
|
case "svelte":
|
|
return svelteExample;
|
|
case "marko":
|
|
return markoExample;
|
|
default:
|
|
return vanillaExample;
|
|
}
|
|
}
|
|
async function generateExampleFiles(framework, lang) {
|
|
const example = getExampleTest(framework);
|
|
let fileName = example.name;
|
|
const folder = resolve(process.cwd(), "vitest-example");
|
|
const fileContent = example[lang];
|
|
if (!existsSync(folder)) {
|
|
await mkdir(folder, { recursive: true });
|
|
}
|
|
const isJSX = fileName.endsWith(".jsx");
|
|
if (isJSX && lang === "ts") {
|
|
fileName = fileName.replace(".jsx", ".tsx");
|
|
} else if (fileName.endsWith(".js") && lang === "ts") {
|
|
fileName = fileName.replace(".js", ".ts");
|
|
}
|
|
const filePath = resolve(folder, fileName);
|
|
const testPath = resolve(folder, `HelloWorld.test.${isJSX ? `${lang}x` : lang}`);
|
|
writeFileSync(filePath, fileContent.trimStart(), "utf-8");
|
|
writeFileSync(testPath, example.test.trimStart(), "utf-8");
|
|
return testPath;
|
|
}
|
|
|
|
const log = console.log;
|
|
function getProviderOptions() {
|
|
const providers = {
|
|
playwright: "Playwright relies on Chrome DevTools protocol. Read more: https://playwright.dev",
|
|
webdriverio: "WebdriverIO uses WebDriver protocol. Read more: https://webdriver.io",
|
|
preview: "Preview is useful to quickly run your tests in the browser, but not suitable for CI."
|
|
};
|
|
return Object.entries(providers).map(([provider, description]) => {
|
|
return {
|
|
title: provider,
|
|
description,
|
|
value: provider
|
|
};
|
|
});
|
|
}
|
|
function getBrowserNames(provider) {
|
|
switch (provider) {
|
|
case "webdriverio":
|
|
return ["chrome", "firefox", "edge", "safari"];
|
|
case "playwright":
|
|
return ["chromium", "firefox", "webkit"];
|
|
case "preview":
|
|
return ["chrome", "firefox", "safari"];
|
|
}
|
|
}
|
|
function getProviderPackageNames(provider) {
|
|
switch (provider) {
|
|
case "webdriverio":
|
|
return {
|
|
types: "@vitest/browser/providers/webdriverio",
|
|
pkg: "webdriverio"
|
|
};
|
|
case "playwright":
|
|
return {
|
|
types: "@vitest/browser/providers/playwright",
|
|
pkg: "playwright"
|
|
};
|
|
case "preview":
|
|
return {
|
|
types: "@vitest/browser/matchers",
|
|
pkg: null
|
|
};
|
|
}
|
|
throw new Error(`Unsupported provider: ${provider}`);
|
|
}
|
|
function getFramework() {
|
|
return [
|
|
{
|
|
title: "vanilla",
|
|
value: "vanilla",
|
|
description: "No framework, just plain JavaScript or TypeScript."
|
|
},
|
|
{
|
|
title: "vue",
|
|
value: "vue",
|
|
description: '"The Progressive JavaScript Framework"'
|
|
},
|
|
{
|
|
title: "svelte",
|
|
value: "svelte",
|
|
description: '"Svelte: cybernetically enhanced web apps"'
|
|
},
|
|
{
|
|
title: "react",
|
|
value: "react",
|
|
description: '"The library for web and native user interfaces"'
|
|
},
|
|
{
|
|
title: "preact",
|
|
value: "preact",
|
|
description: '"Fast 3kB alternative to React with the same modern API"'
|
|
},
|
|
{
|
|
title: "solid",
|
|
value: "solid",
|
|
description: '"Simple and performant reactivity for building user interfaces"'
|
|
},
|
|
{
|
|
title: "marko",
|
|
value: "marko",
|
|
description: '"A declarative, HTML-based language that makes building web apps fun"'
|
|
}
|
|
];
|
|
}
|
|
function getFrameworkTestPackage(framework) {
|
|
switch (framework) {
|
|
case "vanilla":
|
|
return null;
|
|
case "vue":
|
|
return "vitest-browser-vue";
|
|
case "svelte":
|
|
return "vitest-browser-svelte";
|
|
case "react":
|
|
return "vitest-browser-react";
|
|
case "preact":
|
|
return "@testing-library/preact";
|
|
case "solid":
|
|
return "@solidjs/testing-library";
|
|
case "marko":
|
|
return "@marko/testing-library";
|
|
}
|
|
throw new Error(`Unsupported framework: ${framework}`);
|
|
}
|
|
function getFrameworkPluginPackage(framework) {
|
|
switch (framework) {
|
|
case "vue":
|
|
return "@vitejs/plugin-vue";
|
|
case "svelte":
|
|
return "@sveltejs/vite-plugin-svelte";
|
|
case "react":
|
|
return "@vitejs/plugin-react";
|
|
case "preact":
|
|
return "@preact/preset-vite";
|
|
case "solid":
|
|
return "vite-plugin-solid";
|
|
case "marko":
|
|
return "@marko/vite";
|
|
}
|
|
return null;
|
|
}
|
|
async function updateTsConfig(type) {
|
|
if (type == null) {
|
|
return;
|
|
}
|
|
const msg = `Add "${c.bold(type)}" to your tsconfig.json "${c.bold("compilerOptions.types")}" field to have better intellisense support.`;
|
|
log();
|
|
log(c.yellow("\u25FC"), c.yellow(msg));
|
|
}
|
|
function getLanguageOptions() {
|
|
return [
|
|
{
|
|
title: "TypeScript",
|
|
description: "Use TypeScript.",
|
|
value: "ts"
|
|
},
|
|
{
|
|
title: "JavaScript",
|
|
description: "Use plain JavaScript.",
|
|
value: "js"
|
|
}
|
|
];
|
|
}
|
|
async function installPackages(pkgManager, packages) {
|
|
if (!packages.length) {
|
|
log(c.green("\u2714"), c.bold("All packages are already installed."));
|
|
return;
|
|
}
|
|
log(c.cyan("\u25FC"), c.bold("Installing packages..."));
|
|
log(c.cyan("\u25FC"), packages.join(", "));
|
|
log();
|
|
await installPackage(packages, { dev: true, packageManager: pkgManager ?? void 0 });
|
|
}
|
|
function readPkgJson(path) {
|
|
if (!existsSync(path)) {
|
|
return null;
|
|
}
|
|
const content = readFileSync(path, "utf-8");
|
|
return JSON.parse(content);
|
|
}
|
|
function getPossibleDefaults(dependencies) {
|
|
const provider = getPossibleProvider(dependencies);
|
|
const framework = getPossibleFramework(dependencies);
|
|
return {
|
|
lang: "ts",
|
|
provider,
|
|
framework
|
|
};
|
|
}
|
|
function getPossibleFramework(dependencies) {
|
|
if (dependencies.vue || dependencies["vue-tsc"] || dependencies["@vue/reactivity"]) {
|
|
return "vue";
|
|
}
|
|
if (dependencies.react || dependencies["react-dom"]) {
|
|
return "react";
|
|
}
|
|
if (dependencies.svelte || dependencies["@sveltejs/kit"]) {
|
|
return "svelte";
|
|
}
|
|
if (dependencies.preact) {
|
|
return "preact";
|
|
}
|
|
if (dependencies["solid-js"] || dependencies["@solidjs/start"]) {
|
|
return "solid";
|
|
}
|
|
if (dependencies.marko) {
|
|
return "marko";
|
|
}
|
|
return "vanilla";
|
|
}
|
|
function getPossibleProvider(dependencies) {
|
|
if (dependencies.webdriverio || dependencies["@wdio/cli"] || dependencies["@wdio/config"]) {
|
|
return "webdriverio";
|
|
}
|
|
return "playwright";
|
|
}
|
|
function getProviderDocsLink(provider) {
|
|
switch (provider) {
|
|
case "playwright":
|
|
return "https://playwright.dev";
|
|
case "webdriverio":
|
|
return "https://webdriver.io";
|
|
}
|
|
}
|
|
function sort(choices, value) {
|
|
const index = choices.findIndex((i) => i.value === value);
|
|
if (index === -1) {
|
|
return choices;
|
|
}
|
|
const item = choices.splice(index, 1)[0];
|
|
return [item, ...choices];
|
|
}
|
|
function fail() {
|
|
process.exitCode = 1;
|
|
}
|
|
async function generateWorkspaceFile(options) {
|
|
const relativeRoot = relative(dirname(options.configPath), options.rootConfig);
|
|
const workspaceContent = [
|
|
`import { defineWorkspace } from 'vitest/config'`,
|
|
"",
|
|
"export default defineWorkspace([",
|
|
" // If you want to keep running your existing tests in Node.js, uncomment the next line.",
|
|
` // '${relativeRoot}',`,
|
|
` {`,
|
|
` extends: '${relativeRoot}',`,
|
|
` test: {`,
|
|
` browser: {`,
|
|
` enabled: true,`,
|
|
` name: '${options.browser}',`,
|
|
` provider: '${options.provider}',`,
|
|
options.provider !== "preview" && ` // ${getProviderDocsLink(options.provider)}`,
|
|
options.provider !== "preview" && ` providerOptions: {},`,
|
|
` },`,
|
|
` },`,
|
|
` },`,
|
|
`])`,
|
|
""
|
|
].filter((c2) => typeof c2 === "string").join("\n");
|
|
await writeFile(options.configPath, workspaceContent);
|
|
}
|
|
async function generateFrameworkConfigFile(options) {
|
|
const frameworkImport = options.framework === "svelte" ? `import { svelte } from '${options.frameworkPlugin}'` : `import ${options.framework} from '${options.frameworkPlugin}'`;
|
|
const configContent = [
|
|
`import { defineConfig } from 'vitest/config'`,
|
|
options.frameworkPlugin ? frameworkImport : null,
|
|
``,
|
|
"export default defineConfig({",
|
|
options.frameworkPlugin ? ` plugins: [${options.framework}()],` : null,
|
|
` test: {`,
|
|
` browser: {`,
|
|
` enabled: true,`,
|
|
` name: '${options.browser}',`,
|
|
` provider: '${options.provider}',`,
|
|
options.provider !== "preview" && ` // ${getProviderDocsLink(options.provider)}`,
|
|
options.provider !== "preview" && ` providerOptions: {},`,
|
|
` },`,
|
|
` },`,
|
|
`})`,
|
|
""
|
|
].filter((t) => typeof t === "string").join("\n");
|
|
await writeFile(options.configPath, configContent);
|
|
}
|
|
async function updatePkgJsonScripts(pkgJsonPath, vitestScript) {
|
|
if (!existsSync(pkgJsonPath)) {
|
|
const pkg = {
|
|
scripts: {
|
|
"test:browser": vitestScript
|
|
}
|
|
};
|
|
await writeFile(pkgJsonPath, `${JSON.stringify(pkg, null, 2)}
|
|
`, "utf-8");
|
|
} else {
|
|
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
pkg.scripts = pkg.scripts || {};
|
|
pkg.scripts["test:browser"] = vitestScript;
|
|
await writeFile(pkgJsonPath, `${JSON.stringify(pkg, null, 2)}
|
|
`, "utf-8");
|
|
}
|
|
log(c.green("\u2714"), 'Added "test:browser" script to your package.json.');
|
|
}
|
|
function getRunScript(pkgManager) {
|
|
switch (pkgManager) {
|
|
case "yarn@berry":
|
|
case "yarn":
|
|
return "yarn test:browser";
|
|
case "pnpm@6":
|
|
case "pnpm":
|
|
return "pnpm test:browser";
|
|
case "bun":
|
|
return "bun test:browser";
|
|
default:
|
|
return "npm run test:browser";
|
|
}
|
|
}
|
|
function getPlaywrightRunArgs(pkgManager) {
|
|
switch (pkgManager) {
|
|
case "yarn@berry":
|
|
case "yarn":
|
|
return ["yarn", "exec"];
|
|
case "pnpm@6":
|
|
case "pnpm":
|
|
return ["pnpx"];
|
|
case "bun":
|
|
return ["bunx"];
|
|
default:
|
|
return ["npx"];
|
|
}
|
|
}
|
|
async function create() {
|
|
log(c.cyan("\u25FC"), "This utility will help you set up a browser testing environment.\n");
|
|
const pkgJsonPath = resolve(process.cwd(), "package.json");
|
|
const pkg = readPkgJson(pkgJsonPath) || {};
|
|
const dependencies = {
|
|
...pkg.dependencies,
|
|
...pkg.devDependencies
|
|
};
|
|
const defaults = getPossibleDefaults(dependencies);
|
|
const { lang } = await prompt({
|
|
type: "select",
|
|
name: "lang",
|
|
message: "Choose a language for your tests",
|
|
choices: sort(getLanguageOptions(), defaults?.lang)
|
|
});
|
|
if (!lang) {
|
|
return fail();
|
|
}
|
|
const { provider } = await prompt({
|
|
type: "select",
|
|
name: "provider",
|
|
message: "Choose a browser provider. Vitest will use its API to control the testing environment",
|
|
choices: sort(getProviderOptions(), defaults?.provider)
|
|
});
|
|
if (!provider) {
|
|
return fail();
|
|
}
|
|
const { browser } = await prompt({
|
|
type: "select",
|
|
name: "browser",
|
|
message: "Choose a browser",
|
|
choices: getBrowserNames(provider).map((browser2) => ({
|
|
title: browser2,
|
|
value: browser2
|
|
}))
|
|
});
|
|
if (!provider) {
|
|
return fail();
|
|
}
|
|
const { framework } = await prompt({
|
|
type: "select",
|
|
name: "framework",
|
|
message: "Choose your framework",
|
|
choices: sort(getFramework(), defaults?.framework)
|
|
});
|
|
if (!framework) {
|
|
return fail();
|
|
}
|
|
let installPlaywright = false;
|
|
if (provider === "playwright") {
|
|
({ installPlaywright } = await prompt({
|
|
type: "confirm",
|
|
name: "installPlaywright",
|
|
message: `Install Playwright browsers (can be done manually via 'pnpm exec playwright install')?`
|
|
}));
|
|
}
|
|
if (installPlaywright == null) {
|
|
return fail();
|
|
}
|
|
const dependenciesToInstall = [
|
|
"@vitest/browser"
|
|
];
|
|
const frameworkPackage = getFrameworkTestPackage(framework);
|
|
if (frameworkPackage) {
|
|
dependenciesToInstall.push(frameworkPackage);
|
|
}
|
|
const providerPkg = getProviderPackageNames(provider);
|
|
if (providerPkg.pkg) {
|
|
dependenciesToInstall.push(providerPkg.pkg);
|
|
}
|
|
const frameworkPlugin = getFrameworkPluginPackage(framework);
|
|
if (frameworkPlugin) {
|
|
dependenciesToInstall.push(frameworkPlugin);
|
|
}
|
|
const pkgManager = await detectPackageManager();
|
|
log();
|
|
await installPackages(
|
|
pkgManager,
|
|
dependenciesToInstall.filter((pkg2) => !dependencies[pkg2])
|
|
);
|
|
const rootConfig = await findUp(configFiles, {
|
|
cwd: process.cwd()
|
|
});
|
|
let scriptCommand = "vitest";
|
|
log();
|
|
if (rootConfig) {
|
|
let browserWorkspaceFile = resolve(dirname(rootConfig), `vitest.workspace.${lang}`);
|
|
if (existsSync(browserWorkspaceFile)) {
|
|
log(c.yellow("\u26A0"), c.yellow("A workspace file already exists. Creating a new one for the browser tests - you can merge them manually if needed."));
|
|
browserWorkspaceFile = resolve(process.cwd(), `vitest.workspace.browser.${lang}`);
|
|
}
|
|
scriptCommand = `vitest --workspace=${relative(process.cwd(), browserWorkspaceFile)}`;
|
|
await generateWorkspaceFile({
|
|
configPath: browserWorkspaceFile,
|
|
rootConfig,
|
|
provider,
|
|
browser
|
|
});
|
|
log(c.green("\u2714"), "Created a workspace file for browser tests:", c.bold(relative(process.cwd(), browserWorkspaceFile)));
|
|
} else {
|
|
const configPath = resolve(process.cwd(), `vitest.config.${lang}`);
|
|
await generateFrameworkConfigFile({
|
|
configPath,
|
|
framework,
|
|
frameworkPlugin,
|
|
provider,
|
|
browser
|
|
});
|
|
log(c.green("\u2714"), "Created a config file for browser tests", c.bold(relative(process.cwd(), configPath)));
|
|
}
|
|
log();
|
|
await updatePkgJsonScripts(pkgJsonPath, scriptCommand);
|
|
if (installPlaywright) {
|
|
log();
|
|
const [command, ...args] = getPlaywrightRunArgs(pkgManager);
|
|
const allArgs = [...args, "playwright", "install", "--with-deps"];
|
|
log(c.cyan("\u25FC"), `Installing Playwright dependencies with \`${c.bold(command)} ${c.bold(allArgs.join(" "))}\`...`);
|
|
log();
|
|
await x(command, allArgs, {
|
|
nodeOptions: {
|
|
stdio: ["pipe", "inherit", "inherit"]
|
|
}
|
|
});
|
|
}
|
|
if (lang === "ts") {
|
|
await updateTsConfig(providerPkg?.types);
|
|
}
|
|
log();
|
|
const exampleTestFile = await generateExampleFiles(framework, lang);
|
|
log(c.green("\u2714"), "Created example test file in", c.bold(relative(process.cwd(), exampleTestFile)));
|
|
log(c.dim(" You can safely delete this file once you have written your own tests."));
|
|
log();
|
|
log(c.cyan("\u25FC"), "All done! Run your tests with", c.bold(getRunScript(pkgManager)));
|
|
}
|
|
|
|
export { create };
|