Skip to content

Commit 51871e5

Browse files
committed
test(e2e): isolate E2E tests and remove destructive actions
The E2E suite has a litany of problems, including that it is fairly complex, doesn't work very well locally and destructively modifies the workspace, and doesn't isolate tests from each other particularly well. The tests rely on wrapper scripts to work properly, which makes it unnecessarily difficult to run E2E tests. This changeset makes a few changes to the E2E test to help address these issues The tl;dr is that tests are now self contained without any need for wrapper scripts and are better isolated. Other notable changes include: - Each test now gets its own isolated Verdaccio registry; the Verdaccio registry storage is no longer shared between tests and is no longer persisted between test invocations. - The CLI is now published to the (isolated) Verdaccio registry from a temporary workspace, which is a copy of the project workspace. We no longer publish to Verdaccio from the project workspace. This ensures that destructive actions that occur on publish don't alter the workspace, and ensures that one test can't modify the registry in a way that impacts another test. - The tests no longer rely on wrapper scripts (which ran `vitest` in a subprocess and didn't always work locally (depending on your `PATH`). - I also converted the tests to TypeScript while I was at it. Theoretically, these changes mean we can run E2E tests concurrently, though I haven't done that in this changeset. We could also now merge the E2E vitest configuration into the primary Vitest config (though I haven't done that here, either).
1 parent 719c17c commit 51871e5

File tree

7 files changed

+327
-286
lines changed

7 files changed

+327
-286
lines changed

e2e/install.e2e.js

-44
This file was deleted.

e2e/install.e2e.ts

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import http from 'node:http'
2+
import os from 'node:os'
3+
import events from 'node:events'
4+
import { existsSync } from 'node:fs'
5+
import { execSync } from 'node:child_process'
6+
import fs from 'node:fs/promises'
7+
import { platform } from 'node:os'
8+
import path from 'node:path'
9+
import { fileURLToPath } from 'node:url'
10+
11+
import execa from 'execa'
12+
import { runServer } from 'verdaccio'
13+
import { describe, expect, it } from 'vitest'
14+
import createDebug from 'debug'
15+
import picomatch from 'picomatch'
16+
17+
import pkg from '../package.json'
18+
19+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
20+
const projectRoot = path.resolve(__dirname, '..')
21+
const distDir = path.join(projectRoot, 'dist')
22+
const tempdirPrefix = 'netlify-cli-e2e-test--'
23+
24+
const debug = createDebug('netlify-cli:e2e')
25+
const isNodeModules = picomatch('**/node_modules/**')
26+
const isNotNodeModules = (target: string) => !isNodeModules(target)
27+
28+
const itWithMockNpmRegistry = it.extend<{ registry: { address: string; cwd: string } }>({
29+
registry: async (
30+
// Vitest requires this argument is destructured even if no properties are used
31+
// eslint-disable-next-line no-empty-pattern
32+
{},
33+
use,
34+
) => {
35+
try {
36+
if (!(await fs.stat(distDir)).isDirectory()) {
37+
throw new Error(`found unexpected non-directory at "${distDir}"`)
38+
}
39+
} catch (err) {
40+
throw new Error(
41+
'"dist" directory does not exist or is not a directory. The project must be built before running E2E tests.',
42+
{ cause: err },
43+
)
44+
}
45+
46+
const verdaccioStorageDir = await fs.mkdtemp(path.join(os.tmpdir(), `${tempdirPrefix}verdaccio-storage`))
47+
const server: http.Server = (await runServer(
48+
// @ts-expect-error(ndhoule): Verdaccio's types are incorrect
49+
{
50+
self_path: __dirname,
51+
storage: verdaccioStorageDir,
52+
web: { title: 'Test Registry' },
53+
max_body_size: '128mb',
54+
// Disable user registration
55+
max_users: -1,
56+
logs: { level: 'fatal' },
57+
uplinks: {
58+
npmjs: {
59+
url: 'https://registry.npmjs.org/',
60+
maxage: '1d',
61+
cache: true,
62+
},
63+
},
64+
packages: {
65+
'@*/*': {
66+
access: '$all',
67+
publish: 'noone',
68+
proxy: 'npmjs',
69+
},
70+
'netlify-cli': {
71+
access: '$all',
72+
publish: '$all',
73+
},
74+
'**': {
75+
access: '$all',
76+
publish: 'noone',
77+
proxy: 'npmjs',
78+
},
79+
},
80+
},
81+
)) as http.Server
82+
83+
await Promise.all([
84+
Promise.race([
85+
events.once(server, 'listening'),
86+
events.once(server, 'error').then(() => {
87+
throw new Error('Verdaccio server failed to start')
88+
}),
89+
]),
90+
server.listen(),
91+
])
92+
const address = server.address()
93+
if (address === null || typeof address === 'string') {
94+
throw new Error('Failed to open Verdaccio server')
95+
}
96+
const registryURL = new URL(
97+
`http://${
98+
address.family === 'IPv6' && address.address === '::' ? 'localhost' : address.address
99+
}:${address.port.toString()}`,
100+
)
101+
102+
// The CLI publishing process modiifes the workspace, so copy it to a temporary directory. This
103+
// lets us avoid contaminating the user's workspace when running these tests locally.
104+
const publishWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), `${tempdirPrefix}publish-workspace`))
105+
await fs.cp(projectRoot, publishWorkspace, {
106+
recursive: true,
107+
verbatimSymlinks: true,
108+
// At this point, the project is built. As long as we limit the prepublish script to built-
109+
// ins, node_modules are not be necessary to publish the package.
110+
filter: isNotNodeModules,
111+
})
112+
await fs.writeFile(
113+
path.join(publishWorkspace, '.npmrc'),
114+
`//${registryURL.hostname}:${registryURL.port}/:_authToken=dummy`,
115+
)
116+
await execa('npm', ['publish', `--registry=${registryURL.toString()}`, '--tag=testing'], {
117+
cwd: publishWorkspace,
118+
stdio: debug.enabled ? 'inherit' : 'ignore',
119+
env: {
120+
// XXX(ndhoule): Is this necessary? Unsure, doesn't --registry cover this?
121+
npm_config_registry: registryURL.toString(),
122+
},
123+
})
124+
await fs.rm(publishWorkspace, { force: true, recursive: true })
125+
126+
const testWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), tempdirPrefix))
127+
await use({
128+
address: registryURL.toString(),
129+
cwd: testWorkspace,
130+
})
131+
132+
await Promise.all([
133+
events.once(server, 'close'),
134+
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
135+
server.close().closeAllConnections(),
136+
])
137+
await fs.rm(testWorkspace, { force: true, recursive: true })
138+
await fs.rm(verdaccioStorageDir, { force: true, recursive: true })
139+
},
140+
})
141+
142+
const doesPackageManagerExist = (packageManager: string): boolean => {
143+
try {
144+
execSync(`${packageManager} --version`)
145+
return true
146+
} catch {
147+
return false
148+
}
149+
}
150+
151+
const tests: [packageManager: string, config: { install: [cmd: string, args: string[]]; lockfile: string }][] = [
152+
[
153+
'npm',
154+
{
155+
install: ['npm', ['install', 'netlify-cli@testing']],
156+
lockfile: 'package-lock.json',
157+
},
158+
],
159+
[
160+
'pnpm',
161+
{
162+
install: ['pnpm', ['add', 'netlify-cli@testing']],
163+
lockfile: 'pnpm-lock.yaml',
164+
},
165+
],
166+
[
167+
'yarn',
168+
{
169+
install: ['yarn', ['add', 'netlify-cli@testing']],
170+
lockfile: 'yarn.lock',
171+
},
172+
],
173+
]
174+
175+
describe.each(tests)('%s → installs the cli and runs the help command without error', (packageManager, config) => {
176+
itWithMockNpmRegistry.runIf(doesPackageManagerExist(packageManager))(
177+
'installs the cli and runs the help command without error',
178+
async ({ registry }) => {
179+
const cwd = registry.cwd
180+
await execa(...config.install, {
181+
cwd,
182+
env: { npm_config_registry: registry.address },
183+
stdio: debug.enabled ? 'inherit' : 'ignore',
184+
})
185+
186+
expect(
187+
existsSync(path.join(cwd, config.lockfile)),
188+
`Generated lock file ${config.lockfile} does not exist in ${cwd}`,
189+
).toBe(true)
190+
191+
const binary = path.resolve(path.join(cwd, `./node_modules/.bin/netlify${platform() === 'win32' ? '.cmd' : ''}`))
192+
const { stdout } = await execa(binary, ['help'], { cwd })
193+
194+
expect(stdout.trim(), `Help command does not start with 'VERSION':\n\n${stdout}`).toMatch(/^VERSION/)
195+
expect(stdout, `Help command does not include 'netlify-cli/${pkg.version}':\n\n${stdout}`).toContain(
196+
`netlify-cli/${pkg.version}`,
197+
)
198+
expect(stdout, `Help command does not include '$ netlify [COMMAND]':\n\n${stdout}`).toMatch('$ netlify [COMMAND]')
199+
},
200+
)
201+
})

0 commit comments

Comments
 (0)