Skip to content

Commit e24fb2b

Browse files
feat: add E2E NUT tests for webapp dev command @W-21111429@ (#32)
* feat: add E2E NUT tests for webapp dev command @W-21111429@ - Add Tier 1 (no auth) and Tier 2 (CLI validation) tests for dev.nut.ts - Add devPort.nut.ts for --port and --proxy-port flag tests - Add devWithUrl.nut.ts for --url, proxy detection, and Vite integration tests - Add shared helpers: devServerUtils (spawn, port reservation) and webappProjectUtils - Add _cleanup.nut.ts for test session cleanup - Add .env.template for NUT credentials (TESTKIT_AUTH_URL, etc.) - Update GitHub Actions to run NUTs on ubuntu-latest and windows-latest - Fix Windows CI: use spawnWebappDev, taskkill, --sfdx-url-file auth - Extend dev.ts to support OPEN_BROWSER=false for headless test runs
1 parent fbc4da7 commit e24fb2b

14 files changed

Lines changed: 1263 additions & 46 deletions

File tree

.env.template

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# ── NUT Credentials ──────────────────────────────────────────────
2+
# Only ONE of the two options below is needed.
3+
#
4+
# Option A: AUTH_URL (simplest — recommended for getting started)
5+
# 1. sf org login web --alias nut-org
6+
# 2. sf org display -o nut-org --json | jq -r .result.sfdxAuthUrl
7+
# 3. Paste the value below
8+
# TESTKIT_AUTH_URL=
9+
10+
# Option B: JWT (no token expiration — recommended for CI)
11+
# TESTKIT_JWT_KEY=
12+
# TESTKIT_JWT_CLIENT_ID=
13+
# TESTKIT_HUB_USERNAME=
14+
# TESTKIT_HUB_INSTANCE=https://login.salesforce.com
15+
16+
# ── Optional ─────────────────────────────────────────────────────
17+
# Path to sf CLI executable (defaults to global sf in PATH)
18+
# TESTKIT_EXECUTABLE_PATH=
19+
20+
# Prevent browser from opening during tests
21+
OPEN_BROWSER=false

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ jobs:
2424
fail-fast: false
2525
with:
2626
os: ${{ matrix.os }}
27+
retries: 3

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ node_modules
4444
# --
4545
# put files here you don't want cleaned with sf-clean
4646

47+
# local NUT credentials
48+
.env
49+
4750
# os specific files
4851
.DS_Store
4952
.idea

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@salesforce/plugin-command-reference": "^3.1.77",
2525
"@types/http-proxy": "^1.17.14",
2626
"@types/micromatch": "^4.0.10",
27+
"dotenv": "^17.3.1",
2728
"eslint-plugin-sf-plugin": "^1.20.33",
2829
"oclif": "^4.22.68",
2930
"ts-node": "^10.9.2",
@@ -82,7 +83,9 @@
8283
"prepack": "sf-prepack",
8384
"prepare": "sf-install",
8485
"test": "wireit",
85-
"test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel",
86+
"test:nuts": "mocha \"**/*.nut.ts\" --slow 4500 --timeout 300000 --parallel=false",
87+
"test:nuts:local": "node -r dotenv/config ./node_modules/.bin/nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 300000 --parallel=false",
88+
"test:nut:local": "node -r dotenv/config ./node_modules/.bin/nyc mocha --slow 4500 --timeout 300000",
8689
"test:only": "wireit",
8790
"version": "oclif readme"
8891
},

src/commands/webapp/dev.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,14 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
291291
} else if (flags.url) {
292292
// User explicitly passed --url; assume server is already running at that URL
293293
// Fail immediately if unreachable (don't start dev server)
294-
throw new SfError(messages.getMessage('error.dev-url-unreachable-with-flag', [resolvedUrl]), 'DevServerUrlError', [
295-
`Ensure your dev server is running at ${resolvedUrl}`,
296-
'Remove --url to use dev.command to start the server automatically',
297-
]);
294+
throw new SfError(
295+
messages.getMessage('error.dev-url-unreachable-with-flag', [resolvedUrl]),
296+
'DevServerUrlError',
297+
[
298+
`Ensure your dev server is running at ${resolvedUrl}`,
299+
'Remove --url to use dev.command to start the server automatically',
300+
]
301+
);
298302
} else if (manifest?.dev?.url && !manifest?.dev?.command?.trim()) {
299303
// dev.url in manifest but no dev.command - don't start (we can't control the port)
300304
throw new SfError(messages.getMessage('error.dev-url-unreachable', [resolvedUrl]), 'DevServerUrlError', [
@@ -439,7 +443,9 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
439443
await this.proxyServer.start();
440444
} catch (error) {
441445
const err = error as NodeJS.ErrnoException;
442-
if (err.code === 'EADDRINUSE') {
446+
const isAddrInUse =
447+
err.code === 'EADDRINUSE' || (error instanceof SfError && error.name === 'PortInUseError');
448+
if (isAddrInUse) {
443449
if (portExplicitlyConfigured) {
444450
throw new SfError(messages.getMessage('error.port-in-use', [String(port)]), 'PortInUseError');
445451
}

src/error/DevServerErrorParser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,11 @@ const ERROR_PATTERNS: ErrorPattern[] = [
132132

133133
// Command not found - dependencies not installed
134134
{
135-
pattern: /command not found|not recognized as.*command/i,
135+
pattern: /command not found|not recognized as.*command|:\s+not found/i,
136136
type: 'missing-module',
137137
title: 'Dependencies Not Installed',
138138
getMessage: (stderr): string => {
139-
const cmdMatch = stderr.match(/(?:sh:|bash:)\s*(\S+):\s*command not found/i);
139+
const cmdMatch = stderr.match(/(?:sh:|bash:)\s*(?:\d+:\s*)?(\S+):\s*(?:command )?not found/i);
140140
const command = cmdMatch?.[1] ?? 'required command';
141141
return `Command '${command}' not found. Project dependencies may not be installed.`;
142142
},
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2026, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { readdirSync, rmSync } from 'node:fs';
18+
import { join } from 'node:path';
19+
20+
/**
21+
* Root-level cleanup: remove any test_session_* directories left behind.
22+
*
23+
* TestSession.clean() normally deletes these, but they can persist when:
24+
* - rm fails (e.g. Windows file locks from spawned processes)
25+
* - Process is killed before after() runs (timeout, SIGKILL)
26+
* - TESTKIT_SAVE_ARTIFACTS is set
27+
*
28+
* This hook runs after all NUTs complete as a fallback.
29+
*/
30+
after(() => {
31+
const cwd = process.cwd();
32+
try {
33+
for (const name of readdirSync(cwd)) {
34+
if (name.startsWith('test_session_')) {
35+
try {
36+
rmSync(join(cwd, name), { recursive: true, force: true });
37+
} catch {
38+
/* ignore per-dir failures */
39+
}
40+
}
41+
}
42+
} catch {
43+
/* ignore */
44+
}
45+
});

0 commit comments

Comments
 (0)