Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add next experimental-test command #64352

Merged
merged 52 commits into from Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
852c078
wip
Ethan-Arrowood Apr 11, 2024
c7881df
share supported test runner types
Ethan-Arrowood Apr 11, 2024
34b22bc
.
Ethan-Arrowood Apr 11, 2024
2a5934c
.
Ethan-Arrowood Apr 11, 2024
e47cdb3
chore(cli): uncomment .usage()
samcx Apr 11, 2024
5e71cda
popsicle sticks and duct tape
Ethan-Arrowood Apr 11, 2024
abb1434
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 11, 2024
cd41ce2
fix
Ethan-Arrowood Apr 12, 2024
2601aa5
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 15, 2024
bd63c40
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 15, 2024
99de698
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 15, 2024
424a2ed
start on integration tests
Ethan-Arrowood Apr 15, 2024
0ebe076
test-runner validation
Ethan-Arrowood Apr 16, 2024
932ea29
beep boop
Ethan-Arrowood Apr 16, 2024
c44f927
move tests to e2e
Ethan-Arrowood Apr 16, 2024
2a3c8a3
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 16, 2024
73b3d0e
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 16, 2024
4c53c01
changes
Ethan-Arrowood Apr 17, 2024
7637259
add comment
Ethan-Arrowood Apr 17, 2024
fdfcf25
fixes
Ethan-Arrowood Apr 17, 2024
ff24c14
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 17, 2024
4e2b506
fix test runner args test
Ethan-Arrowood Apr 17, 2024
100e40e
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 18, 2024
e93a408
fix lint issues
Ethan-Arrowood Apr 18, 2024
1ad8fba
improvements
Ethan-Arrowood Apr 18, 2024
d3a7644
fix tests
Ethan-Arrowood Apr 18, 2024
a4f42ff
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 19, 2024
bfa407d
change to install instead of install-deps
Ethan-Arrowood Apr 19, 2024
ca86512
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 22, 2024
860fa64
remove unused code and add definedConfig to withNext
Ethan-Arrowood Apr 22, 2024
593d8ad
add missing `.destroy`
Ethan-Arrowood Apr 22, 2024
aaa9478
fixes fixes fixes
Ethan-Arrowood Apr 22, 2024
03394d9
modify config values
Ethan-Arrowood Apr 22, 2024
d4d04cf
change withNext to defineConfig
Ethan-Arrowood Apr 22, 2024
5dca6ca
remove empty std out assertions
Ethan-Arrowood Apr 22, 2024
f03b956
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 22, 2024
b32e9fc
revert deep-merge change
Ethan-Arrowood Apr 22, 2024
3d0b1d3
Merge branch 'next-experimental-test' of github.com:vercel/next.js in…
Ethan-Arrowood Apr 22, 2024
9249862
fixes from review
Ethan-Arrowood Apr 22, 2024
2687eb3
more fixes, resolve type issue
Ethan-Arrowood Apr 22, 2024
1276382
oh typescript
Ethan-Arrowood Apr 22, 2024
e947996
oh typescript
Ethan-Arrowood Apr 22, 2024
adcc378
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 22, 2024
6902027
fix defineConfig one more time
Ethan-Arrowood Apr 22, 2024
c8afd70
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 22, 2024
3b7a17f
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 23, 2024
310ef22
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 23, 2024
1c93145
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 23, 2024
1b44741
update config
Ethan-Arrowood Apr 23, 2024
fa4fef1
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 23, 2024
1596290
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 23, 2024
c4a36ce
Merge branch 'canary' into next-experimental-test
Ethan-Arrowood Apr 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 53 additions & 1 deletion packages/next/src/bin/next.ts
Expand Up @@ -10,6 +10,8 @@ import { bold, cyan, italic } from '../lib/picocolors'
import { formatCliHelpOutput } from '../lib/format-cli-help-output'
import { NON_STANDARD_NODE_ENV } from '../lib/constants'
import { myParseInt } from '../server/lib/utils'
import { SUPPORTED_TEST_RUNNERS_LIST } from '../cli/next-test.js'
import { getProjectDir } from '../lib/get-project-dir'

if (
semver.lt(process.versions.node, process.env.__NEXT_REQUIRED_NODE_VERSION!)
Expand Down Expand Up @@ -343,6 +345,56 @@ program
mod.nextTelemetry(options, arg)
)
)
.usage('[options]')

program
.command('experimental-test')
.description(
`Execute \`next/experimental/testmode\` tests using a specified test runner. Any arguments or options after the optional ${italic(
'[directory]'
)} and ${italic(
'[test runner]'
)} arguments will be passed through to the ${italic('[test runner]')}.`
)
.argument(
'[directory]',
`A Next.js project directory to execute the test runner on. ${italic(
'If no directory is provided, the current directory will be used.'
)}`
)
.option(
'-t, --test-runner',
`Any supported test runner. Options: ${bold(
SUPPORTED_TEST_RUNNERS_LIST.join(', ')
)}. ${italic(
"If no test runner is provided, the Next.js config option `experimental.defaultTestRunner`, or 'playwright' will be used."
)}`
)
.option(
'-a, --test-runner-args [args...]',
'Arguments to pass through to the test runner'
)
.action((directory, options, command) =>
import('../cli/next-test.js').then((mod) => {
mod.nextTest(directory, options, command)
})
)
.usage('[directory] [options]')

program.parse(process.argv)

/*
// run default test runner in current directory
next test
// run default test runner in `my-project` directory
next test my-project
// run `playwright` test runner in current directory
next test playwright
// run `playwright` test runner in `my-project` directory
next test my-project playwright
// run command `show-report` for default test runner in current directory
next test show-report
// run command `show-report` for default test runner in `my-project` directory
next test my-project show-report
// run command `show-report` for `playwright` in `my-project` directory
next test my-project playwright show-report
*/
229 changes: 229 additions & 0 deletions packages/next/src/cli/next-test.ts
@@ -0,0 +1,229 @@
import { existsSync, 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 { warn } from '../build/output/log'
import { bold, cyan } from '../lib/picocolors'
import { findPagesDir } from '../lib/find-pages-dir'
import { verifyTypeScriptSetup } from '../lib/verify-typescript-setup'
import path from 'path'
import {
getPkgManager,
type PackageManager,
} from '../lib/helpers/get-pkg-manager'
import type { Command } from 'commander'

export interface NextTestOptions {
testRunner?: string
testRunnerArgs?: string[]
}

export const SUPPORTED_TEST_RUNNERS_LIST = ['playwright'] as const
export type SUPPORTED_TEST_RUNNERS =
Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide resolved
(typeof SUPPORTED_TEST_RUNNERS_LIST)[number]

const requiredPackagesByTestRunner: {
[k in SUPPORTED_TEST_RUNNERS]: MissingDependency[]
} = {
playwright: [
{ file: 'playwright', pkg: '@playwright/test', exportsRestrict: false },
],
}

function isSupportedTestRunner(
testRunner: string
): testRunner is SUPPORTED_TEST_RUNNERS {
return testRunner in requiredPackagesByTestRunner
}

export async function nextTest(
directory?: string,
options?: NextTestOptions,
command?: Command
) {
console.log('directory', directory, 'options', options)

process.exit(0)
// get execution directory
const baseDir = getProjectDir(directory)

// Check if the directory exists
if (!existsSync(baseDir)) {
printAndExit(`> No such directory exists as the project root: ${baseDir}`)
}

// find nextConfig
const nextConfig = await loadConfig(PHASE_PRODUCTION_BUILD, baseDir)

// set the test runner. priority is CLI option > next config > default 'playwright'
const configuredTestRunner =
options?.testRunner ??
nextConfig.experimental.defaultTestRunner ??
'playwright'

switch (configuredTestRunner) {
case 'playwright':
return runPlaywright(baseDir, nextConfig, options)
default:
return printAndExit(
`Test runner ${configuredTestRunner} is not supported.`
)
}
}

async function checkRequiredDeps(
baseDir: string,
testRunner: SUPPORTED_TEST_RUNNERS
) {
const deps = await hasNecessaryDependencies(
baseDir,
requiredPackagesByTestRunner[testRunner]
)
if (deps.missing.length > 0) {
await installDependencies(baseDir, deps.missing, true)
}
}

async function runPlaywright(
baseDir: string,
nextConfig: NextConfigComplete,
options: unknown
) {
await checkRequiredDeps(baseDir, 'playwright')

const playwrightConfigFile = await findUp(
['playwright.config.js', 'playwright.config.ts'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does playwright also support .mjs or .json files for config?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. I can only find references to these two in the docs

{
cwd: baseDir,
}
)

if (!playwrightConfigFile) {
const { pagesDir, appDir } = findPagesDir(baseDir)

const { version: typeScriptVersion } = await verifyTypeScriptSetup({
dir: baseDir,
distDir: nextConfig.distDir,
intentDirs: [pagesDir, appDir].filter(Boolean) as string[],
typeCheckPreflight: false,
tsconfigPath: nextConfig.typescript.tsconfigPath,
disableStaticImages: nextConfig.images.disableStaticImages,
hasAppDir: !!appDir,
hasPagesDir: !!pagesDir,
})

const isUsingTypeScript = !!typeScriptVersion

const playwrightConfigFilename = isUsingTypeScript
? 'playwright.config.ts'
: 'playwright.config.js'

const packageManager = getPkgManager(baseDir)

writeFileSync(
path.join(baseDir, playwrightConfigFilename),
defaultPlaywrightConfig(isUsingTypeScript, packageManager)
)
}

return
}

const defaultPlaywrightConfig = (
Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide resolved
typescript: boolean,
packageManager: PackageManager
) => `
${
typescript
? "import { defineConfig, devices } from 'next/experimental/testmode/playwright'"
: "const { defineConfig, devices } = require('next/experimental/testmode/playwright')"
};

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();

/**
* See https://playwright.dev/docs/test-configuration.
*/
${
typescript
? 'export default definedConfig({'
: 'module.exports = defineConfig({'
}
/* Match all co-located test files within app and pages directories*/
testMatch: '{app,pages}/**/*.spec.{t,j}s',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like \`await page.goto('/')\`. */
baseURL: 'http://127.0.0.1:3000',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },

/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],

/* Run your local dev server before starting the tests */
webServer: {
command: '${packageManager === 'npm' ? 'npm run' : packageManager} dev',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
});`
2 changes: 2 additions & 0 deletions packages/next/src/server/config-schema.ts
Expand Up @@ -16,6 +16,7 @@ import type {
RouteHas,
Redirect,
} from '../lib/load-custom-routes'
import { SUPPORTED_TEST_RUNNERS_LIST } from '../cli/next-test'

// A custom zod schema for the SizeLimit type
const zSizeLimit = z.custom<SizeLimit>((val) => {
Expand Down Expand Up @@ -390,6 +391,7 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
missingSuspenseWithCSRBailout: z.boolean().optional(),
useEarlyImport: z.boolean().optional(),
testProxy: z.boolean().optional(),
defaultTestRunner: z.enum(SUPPORTED_TEST_RUNNERS_LIST).optional(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking nit: I like that we're setting up all the internal plumbing to support having other test runners, but maybe until we have more than 1 supported runner it's not worth exposing this config publicly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't document these options so I think it's okay. Its unclear to me how soon we'd like to add support for another test runner, so I determined it important to at least keep the implementation open ended from the beginning.

})
.optional(),
exportPathMap: z
Expand Down
8 changes: 7 additions & 1 deletion packages/next/src/server/config-shared.ts
Expand Up @@ -11,6 +11,7 @@ import type { WEB_VITALS } from '../shared/lib/utils'
import type { NextParsedUrlQuery } from './request-meta'
import type { SizeLimit } from '../../types'
import type { SwrDelta } from './lib/revalidate'
import type { SUPPORTED_TEST_RUNNERS } from '../cli/next-test'

export type NextConfigComplete = Required<NextConfig> & {
images: Required<ImageConfigComplete>
Expand Down Expand Up @@ -440,9 +441,14 @@ export interface ExperimentalConfig {
useEarlyImport?: boolean

/**
* Enables `fetch` requests to be proxied to the experimental text proxy server
* Enables `fetch` requests to be proxied to the experimental test proxy server
Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide resolved
*/
testProxy?: boolean

/**
* Set a default test runner to be used by `next experimental-test`.
*/
defaultTestRunner?: SUPPORTED_TEST_RUNNERS
}

export type ExportPathMap = {
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/lib/utils.ts
Expand Up @@ -7,7 +7,7 @@ export function printAndExit(message: string, code = 1) {
console.error(message)
}

process.exit(code)
return process.exit(code)
}

export const getDebugPort = () => {
Expand Down