Skip to content

Commit c10e23c

Browse files
test: improve nut coverage, standardize helpers, add docs
- Add 4 tests: auto-select, dev.port manifest, empty manifest, --port+--url - Add comments explaining what each test verifies - Extract timeout constants (SUITE_TIMEOUT, SPAWN_TIMEOUT, SPAWN_FAIL_TIMEOUT) - Add optional proxyPort to createProjectWithDevServer - Simplify manifests to dev-only props (name/label/version unused by command) - Add webappPath() helper, standardize webapps path usage - Use closeServer for blocker cleanup in devPort.nut.ts - Document port ranges and TIME_WAIT rationale in devServerUtils Made-with: Cursor
1 parent e75e84a commit c10e23c

5 files changed

Lines changed: 227 additions & 107 deletions

File tree

test/commands/webapp/dev.nut.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@ import {
2323
createEmptyWebappsDir,
2424
createWebappDirWithoutMeta,
2525
writeManifest,
26+
webappPath,
2627
ensureSfCli,
2728
authOrgViaUrl,
2829
} from './helpers/webappProjectUtils.js';
2930

3031
/* ------------------------------------------------------------------ *
3132
* Tier 1 — No Auth *
32-
* Uses devhubAuthStrategy: 'NONE'. Always runs (CI + local). *
33+
* *
34+
* Validates flag-level parse errors that fire before any org or *
35+
* filesystem interaction. No credentials needed; always runs. *
3336
* ------------------------------------------------------------------ */
3437
describe('webapp dev NUTs — Tier 1 (no auth)', () => {
3538
let session: TestSession;
@@ -42,6 +45,8 @@ describe('webapp dev NUTs — Tier 1 (no auth)', () => {
4245
await session?.clean();
4346
});
4447

48+
// --target-org is declared as Flags.requiredOrg(). Running without it
49+
// must fail at parse time with NoDefaultEnvError before any other logic.
4550
it('should require --target-org', () => {
4651
const result = execCmd('webapp dev --json', {
4752
ensureExitCode: 1,
@@ -55,9 +60,12 @@ describe('webapp dev NUTs — Tier 1 (no auth)', () => {
5560

5661
/* ------------------------------------------------------------------ *
5762
* Tier 2 — CLI Validation (with auth) *
58-
* Requires TESTKIT_AUTH_URL or JWT credentials. *
59-
* Skips gracefully when no credentials are present. *
60-
* These tests exercise local validation only — no live org calls. *
63+
* *
64+
* Validates webapp discovery errors and URL resolution errors. *
65+
* Auth is only needed so --target-org passes parsing; these tests *
66+
* exercise local filesystem/network checks — no live org calls. *
67+
* *
68+
* Requires TESTKIT_AUTH_URL. Skips gracefully when absent. *
6169
* ------------------------------------------------------------------ */
6270
describe('webapp dev NUTs — Tier 2 CLI validation', () => {
6371
let session: TestSession;
@@ -80,6 +88,7 @@ describe('webapp dev NUTs — Tier 2 CLI validation', () => {
8088

8189
// ── Discovery errors ──────────────────────────────────────────
8290

91+
// Project has no webapplications folder at all → WebappNotFoundError.
8392
it('should error when no webapp found (project only, no webapps)', () => {
8493
const projectDir = createProject(session, 'noWebappProject');
8594

@@ -91,6 +100,7 @@ describe('webapp dev NUTs — Tier 2 CLI validation', () => {
91100
expect(result.jsonOutput?.name).to.equal('WebappNotFoundError');
92101
});
93102

103+
// Project has webapp "realApp" but --name asks for "NonExistent" → WebappNameNotFoundError.
94104
it('should error when --name does not match any webapp', () => {
95105
const projectDir = createProjectWithWebapp(session, 'nameNotFound', 'realApp');
96106

@@ -102,11 +112,13 @@ describe('webapp dev NUTs — Tier 2 CLI validation', () => {
102112
expect(result.jsonOutput?.name).to.equal('WebappNameNotFoundError');
103113
});
104114

115+
// cwd is inside webapp "appA" but --name asks for "appB" → WebappNameConflictError.
116+
// Discovery treats this as ambiguous intent and rejects it.
105117
it('should error on --name conflict when inside a different webapp', () => {
106118
const projectDir = createProjectWithWebapp(session, 'nameConflict', 'appA');
107119
execSync('sf webapp generate --name appB', { cwd: projectDir, stdio: 'pipe' });
108120

109-
const cwdInsideAppA = `${projectDir}/force-app/main/default/webapplications/appA`;
121+
const cwdInsideAppA = webappPath(projectDir, 'appA');
110122

111123
const result = execCmd(`webapp dev --name appB --target-org ${targetOrg} --json`, {
112124
ensureExitCode: 1,
@@ -116,6 +128,7 @@ describe('webapp dev NUTs — Tier 2 CLI validation', () => {
116128
expect(result.jsonOutput?.name).to.equal('WebappNameConflictError');
117129
});
118130

131+
// webapplications/ folder exists but is empty → WebappNotFoundError.
119132
it('should error when webapplications folder is empty', () => {
120133
const projectDir = createProject(session, 'emptyWebapps');
121134
createEmptyWebappsDir(projectDir);
@@ -128,6 +141,7 @@ describe('webapp dev NUTs — Tier 2 CLI validation', () => {
128141
expect(result.jsonOutput?.name).to.equal('WebappNotFoundError');
129142
});
130143

144+
// webapplications/orphanApp/ exists but has no .webapplication-meta.xml → not a valid webapp.
131145
it('should error when webapp dir has no .webapplication-meta.xml', () => {
132146
const projectDir = createProject(session, 'noMeta');
133147
createWebappDirWithoutMeta(projectDir, 'orphanApp');
@@ -140,8 +154,35 @@ describe('webapp dev NUTs — Tier 2 CLI validation', () => {
140154
expect(result.jsonOutput?.name).to.equal('WebappNotFoundError');
141155
});
142156

157+
// ── Auto-selection ────────────────────────────────────────────
158+
159+
// When cwd is inside webapplications/myApp/, discovery auto-selects that
160+
// webapp without --name. The command proceeds past discovery and fails at
161+
// URL resolution (no dev server running) — confirming auto-select worked.
162+
it('should auto-select webapp when run from inside its directory', () => {
163+
const projectDir = createProjectWithWebapp(session, 'autoSelect', 'myApp');
164+
165+
writeManifest(projectDir, 'myApp', {
166+
dev: { url: 'http://localhost:5179' },
167+
});
168+
169+
const cwdInsideApp = webappPath(projectDir, 'myApp');
170+
171+
// No --name flag; cwd is inside the webapp directory.
172+
// Discovery auto-selects myApp, then the command fails at URL check
173+
// (nothing running on 5179). DevServerUrlError proves discovery succeeded.
174+
const result = execCmd(`webapp dev --target-org ${targetOrg} --json`, {
175+
ensureExitCode: 1,
176+
cwd: cwdInsideApp,
177+
});
178+
179+
expect(result.jsonOutput?.name).to.equal('DevServerUrlError');
180+
});
181+
143182
// ── URL / dev server errors ───────────────────────────────────
144183

184+
// --url explicitly provided but nothing is listening → DevServerUrlError.
185+
// The command refuses to start a dev server when --url is given.
145186
it('should error when --url is unreachable', () => {
146187
const projectDir = createProjectWithWebapp(session, 'urlUnreachable', 'myApp');
147188

@@ -153,17 +194,13 @@ describe('webapp dev NUTs — Tier 2 CLI validation', () => {
153194
expect(result.jsonOutput?.name).to.equal('DevServerUrlError');
154195
});
155196

197+
// Manifest has dev.url but no dev.command → command can't start the server
198+
// itself and the URL is unreachable → DevServerUrlError.
156199
it('should error when dev.url is unreachable and no dev.command', () => {
157200
const projectDir = createProjectWithWebapp(session, 'urlNoCmd', 'myApp');
158201

159202
writeManifest(projectDir, 'myApp', {
160-
name: 'myApp',
161-
label: 'My App',
162-
version: '1.0.0',
163-
outputDir: 'dist',
164-
dev: {
165-
url: 'http://localhost:5179',
166-
},
203+
dev: { url: 'http://localhost:5179' },
167204
});
168205

169206
const result = execCmd(`webapp dev --name myApp --target-org ${targetOrg} --json`, {

test/commands/webapp/devPort.nut.ts

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,24 @@
1717
import type { Server } from 'node:net';
1818
import { TestSession } from '@salesforce/cli-plugins-testkit';
1919
import { expect } from 'chai';
20-
import { createProjectWithDevServer, writeManifest, ensureSfCli, authOrgViaUrl } from './helpers/webappProjectUtils.js';
21-
import { occupyPort, spawnWebappDev, type WebappDevHandle } from './helpers/devServerUtils.js';
20+
import { createProjectWithDevServer, ensureSfCli, authOrgViaUrl } from './helpers/webappProjectUtils.js';
21+
import {
22+
occupyPort,
23+
spawnWebappDev,
24+
closeServer,
25+
SUITE_TIMEOUT,
26+
SPAWN_TIMEOUT,
27+
SPAWN_FAIL_TIMEOUT,
28+
type WebappDevHandle,
29+
} from './helpers/devServerUtils.js';
2230

2331
/* ------------------------------------------------------------------ *
2432
* Tier 2 — Port Handling *
2533
* *
26-
* Tests proxy port conflict (PortInUseError) and auto-increment. *
34+
* Validates proxy port resolution logic: *
35+
* - Explicit --port or dev.port occupied → PortInUseError *
36+
* - Default port occupied → auto-increment to next available *
37+
* - Explicit --port or dev.port available → proxy binds to it *
2738
* *
2839
* Requires TESTKIT_AUTH_URL. Skips otherwise. *
2940
* ------------------------------------------------------------------ */
@@ -32,7 +43,7 @@ const DEV_PORT = 18_910;
3243
const PROXY_PORT = 18_920;
3344

3445
describe('webapp dev NUTs — Tier 2 port handling', function () {
35-
this.timeout(180_000);
46+
this.timeout(SUITE_TIMEOUT);
3647

3748
let session: TestSession;
3849
let targetOrg: string;
@@ -55,18 +66,16 @@ describe('webapp dev NUTs — Tier 2 port handling', function () {
5566
await handle.kill();
5667
handle = null;
5768
}
58-
if (blocker) {
59-
await new Promise<void>((resolve) => {
60-
blocker!.close(() => resolve());
61-
});
62-
blocker = null;
63-
}
69+
await closeServer(blocker);
70+
blocker = null;
6471
});
6572

6673
after(async () => {
6774
await session?.clean();
6875
});
6976

77+
// When --port is explicitly provided and that port is already occupied,
78+
// the command must fail with PortInUseError (no auto-increment).
7079
it('should throw PortInUseError when explicit --port is occupied', async () => {
7180
blocker = await occupyPort(PROXY_PORT);
7281

@@ -75,66 +84,74 @@ describe('webapp dev NUTs — Tier 2 port handling', function () {
7584
try {
7685
handle = await spawnWebappDev(['--name', 'myApp', '--port', String(PROXY_PORT), '--target-org', targetOrg], {
7786
cwd: projectDir,
78-
timeout: 60_000,
87+
timeout: SPAWN_FAIL_TIMEOUT,
7988
});
8089
expect.fail('Expected command to fail with PortInUseError');
8190
} catch (err) {
8291
expect((err as Error).message).to.include('PortInUseError');
8392
}
8493
});
8594

95+
// When dev.port is set in the manifest and that port is occupied,
96+
// it is treated as an explicit configuration → PortInUseError (no auto-increment).
8697
it('should throw PortInUseError when dev.port in manifest is occupied', async () => {
87-
const { projectDir } = createProjectWithDevServer(session, 'manifestPort', 'myApp', DEV_PORT + 1);
88-
89-
writeManifest(projectDir, 'myApp', {
90-
name: 'myApp',
91-
label: 'My App',
92-
version: '1.0.0',
93-
outputDir: 'dist',
94-
dev: {
95-
url: `http://localhost:${DEV_PORT + 1}`,
96-
command: 'node dev-server.cjs',
97-
port: PROXY_PORT + 1,
98-
},
99-
});
98+
const { projectDir } = createProjectWithDevServer(session, 'manifestPort', 'myApp', DEV_PORT + 1, PROXY_PORT + 1);
10099

101100
blocker = await occupyPort(PROXY_PORT + 1);
102101

103102
try {
104103
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
105104
cwd: projectDir,
106-
timeout: 60_000,
105+
timeout: SPAWN_FAIL_TIMEOUT,
107106
});
108107
expect.fail('Expected command to fail with PortInUseError');
109108
} catch (err) {
110109
expect((err as Error).message).to.include('PortInUseError');
111110
}
112111
});
113112

113+
// When no --port or dev.port is configured, the default (4545) is used.
114+
// If 4545 is occupied, the command silently tries the next port.
114115
it('should auto-increment port when default port (4545) is occupied', async () => {
115116
blocker = await occupyPort(4545);
116117

117118
const { projectDir } = createProjectWithDevServer(session, 'portAutoInc', 'myApp', DEV_PORT + 2);
118119

119120
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
120121
cwd: projectDir,
121-
timeout: 120_000,
122+
timeout: SPAWN_TIMEOUT,
122123
});
123124

124125
const proxyPort = new URL(handle.proxyUrl).port;
125126
expect(Number(proxyPort)).to.be.greaterThan(4545);
126127
});
127128

129+
// --port flag with an available port → proxy binds exactly to that port.
128130
it('should use custom --port when specified and available', async () => {
129131
const customPort = PROXY_PORT + 5;
130132

131133
const { projectDir } = createProjectWithDevServer(session, 'customPort', 'myApp', DEV_PORT + 3);
132134

133135
handle = await spawnWebappDev(['--name', 'myApp', '--port', String(customPort), '--target-org', targetOrg], {
134136
cwd: projectDir,
135-
timeout: 120_000,
137+
timeout: SPAWN_TIMEOUT,
136138
});
137139

138140
expect(handle.proxyUrl).to.equal(`http://localhost:${customPort}`);
139141
});
142+
143+
// dev.port in manifest with an available port (no --port flag) →
144+
// proxy binds to the manifest-configured port.
145+
it('should use dev.port from manifest when available', async () => {
146+
const manifestPort = PROXY_PORT + 6;
147+
148+
const { projectDir } = createProjectWithDevServer(session, 'manifestPortOk', 'myApp', DEV_PORT + 4, manifestPort);
149+
150+
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
151+
cwd: projectDir,
152+
timeout: SPAWN_TIMEOUT,
153+
});
154+
155+
expect(handle.proxyUrl).to.equal(`http://localhost:${manifestPort}`);
156+
});
140157
});

0 commit comments

Comments
 (0)