| | import { writeFileSync } from 'fs' |
| | import { getProjectDir } from '../lib/get-project-dir' |
| | import { printAndExit } from '../server/lib/utils' |
| | import loadConfig from '../server/config' |
| | import { PHASE_PRODUCTION_BUILD } from '../shared/lib/constants' |
| | import { |
| | hasNecessaryDependencies, |
| | type MissingDependency, |
| | } from '../lib/has-necessary-dependencies' |
| | import { installDependencies } from '../lib/install-dependencies' |
| | import type { NextConfigComplete } from '../server/config-shared' |
| | import findUp from 'next/dist/compiled/find-up' |
| | import { findPagesDir } from '../lib/find-pages-dir' |
| | import { verifyTypeScriptSetup } from '../lib/verify-typescript-setup' |
| | import path from 'path' |
| | import spawn from 'next/dist/compiled/cross-spawn' |
| |
|
| | export interface NextTestOptions { |
| | testRunner?: string |
| | } |
| |
|
| | export const SUPPORTED_TEST_RUNNERS_LIST = ['playwright'] as const |
| | export type SupportedTestRunners = (typeof SUPPORTED_TEST_RUNNERS_LIST)[number] |
| |
|
| | const requiredPackagesByTestRunner: { |
| | [k in SupportedTestRunners]: MissingDependency[] |
| | } = { |
| | playwright: [ |
| | { file: 'playwright', pkg: '@playwright/test', exportsRestrict: false }, |
| | ], |
| | } |
| |
|
| | export async function nextTest( |
| | directory?: string, |
| | testRunnerArgs: string[] = [], |
| | options: NextTestOptions = {} |
| | ) { |
| | |
| | |
| | |
| | |
| |
|
| | let baseDir, nextConfig |
| |
|
| | try { |
| | |
| | baseDir = getProjectDir(directory, false) |
| | } catch (err) { |
| | |
| | |
| | testRunnerArgs.unshift(directory) |
| | |
| | baseDir = getProjectDir() |
| | } |
| |
|
| | try { |
| | |
| | |
| | nextConfig = await loadConfig(PHASE_PRODUCTION_BUILD, baseDir) |
| | } catch (err) { |
| | |
| | |
| | testRunnerArgs.unshift(directory) |
| | |
| | baseDir = getProjectDir() |
| | nextConfig = await loadConfig(PHASE_PRODUCTION_BUILD, baseDir) |
| | } |
| |
|
| | |
| | const configuredTestRunner = |
| | options?.testRunner ?? |
| | nextConfig.experimental.defaultTestRunner ?? |
| | 'playwright' |
| |
|
| | if (!nextConfig.experimental.testProxy) { |
| | return printAndExit( |
| | `\`next experimental-test\` requires the \`experimental.testProxy: true\` configuration option.` |
| | ) |
| | } |
| |
|
| | |
| | switch (configuredTestRunner) { |
| | case 'playwright': |
| | return runPlaywright(baseDir, nextConfig, testRunnerArgs) |
| | default: |
| | return printAndExit( |
| | `Test runner ${configuredTestRunner} is not supported.` |
| | ) |
| | } |
| | } |
| |
|
| | async function checkRequiredDeps( |
| | baseDir: string, |
| | testRunner: SupportedTestRunners |
| | ) { |
| | const deps = hasNecessaryDependencies( |
| | baseDir, |
| | requiredPackagesByTestRunner[testRunner] |
| | ) |
| | if (deps.missing.length > 0) { |
| | await installDependencies(baseDir, deps.missing, true) |
| |
|
| | const playwright = spawn( |
| | path.join(baseDir, 'node_modules', '.bin', 'playwright'), |
| | ['install'], |
| | { |
| | cwd: baseDir, |
| | shell: false, |
| | stdio: 'inherit', |
| | env: { |
| | ...process.env, |
| | }, |
| | } |
| | ) |
| |
|
| | return new Promise((resolve, reject) => { |
| | playwright.on('close', (c) => resolve(c)) |
| | playwright.on('error', (err) => reject(err)) |
| | }) |
| | } |
| | } |
| |
|
| | async function runPlaywright( |
| | baseDir: string, |
| | nextConfig: NextConfigComplete, |
| | testRunnerArgs: string[] |
| | ) { |
| | await checkRequiredDeps(baseDir, 'playwright') |
| |
|
| | const playwrightConfigFile = await findUp( |
| | ['playwright.config.js', 'playwright.config.ts'], |
| | { |
| | cwd: baseDir, |
| | } |
| | ) |
| |
|
| | if (!playwrightConfigFile) { |
| | const { pagesDir, appDir } = findPagesDir(baseDir) |
| |
|
| | const { version: typeScriptVersion } = await verifyTypeScriptSetup({ |
| | dir: baseDir, |
| | distDir: nextConfig.distDir, |
| | strictRouteTypes: Boolean(nextConfig.experimental.strictRouteTypes), |
| | typeCheckPreflight: false, |
| | tsconfigPath: nextConfig.typescript.tsconfigPath, |
| | typedRoutes: Boolean(nextConfig.typedRoutes), |
| | disableStaticImages: nextConfig.images.disableStaticImages, |
| | hasAppDir: !!appDir, |
| | hasPagesDir: !!pagesDir, |
| | isolatedDevBuild: nextConfig.experimental.isolatedDevBuild, |
| | appDir: appDir || undefined, |
| | pagesDir: pagesDir || undefined, |
| | }) |
| |
|
| | const isUsingTypeScript = !!typeScriptVersion |
| |
|
| | const playwrightConfigFilename = isUsingTypeScript |
| | ? 'playwright.config.ts' |
| | : 'playwright.config.js' |
| |
|
| | writeFileSync( |
| | path.join(baseDir, playwrightConfigFilename), |
| | defaultPlaywrightConfig(isUsingTypeScript) |
| | ) |
| |
|
| | return printAndExit( |
| | `Successfully generated ${playwrightConfigFilename}. Create your first test and then run \`next experimental-test\`.`, |
| | 0 |
| | ) |
| | } else { |
| | const playwright = spawn( |
| | path.join(baseDir, 'node_modules', '.bin', 'playwright'), |
| | ['test', ...testRunnerArgs], |
| | { |
| | cwd: baseDir, |
| | shell: false, |
| | stdio: 'inherit', |
| | env: { |
| | ...process.env, |
| | }, |
| | } |
| | ) |
| | return new Promise((resolve, reject) => { |
| | playwright.on('close', (c) => resolve(c)) |
| | playwright.on('error', (err) => reject(err)) |
| | }) |
| | } |
| | } |
| |
|
| | function defaultPlaywrightConfig(typescript: boolean) { |
| | const comment = `/* |
| | * Specify any additional Playwright config options here. |
| | * They will be merged with Next.js' default Playwright config. |
| | * You can access the default config by importing \`defaultPlaywrightConfig\` from \`'next/experimental/testmode/playwright'\`. |
| | */` |
| | return typescript |
| | ? `import { defineConfig } from 'next/experimental/testmode/playwright';\n\n${comment}\nexport default defineConfig({});` |
| | : `const { defineConfig } = require('next/experimental/testmode/playwright');\n\n${comment}\nmodule.exports = defineConfig({});` |
| | } |
| |
|