Skip to content

Commit 58cd165

Browse files
fix: merge URL/proxy test suites into shared session and fix Windows CI
Consolidate proxy-only, Vite proxy, and full flow tests into a single devWithUrl.nut.ts sharing one TestSession (eliminates 2 redundant auth calls). Fix cross-platform authOrgViaUrl by using Node execSync input option instead of shell echo|pipe which fails on Windows cmd.exe. @W-21111429@ Made-with: Cursor
1 parent 8a805b6 commit 58cd165

File tree

3 files changed

+259
-44
lines changed

3 files changed

+259
-44
lines changed

test/commands/webapp/devWithUrl.nut.ts

Lines changed: 205 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,47 @@
1414
* limitations under the License.
1515
*/
1616

17+
import type { Server as HttpServer } from 'node:http';
1718
import { TestSession } from '@salesforce/cli-plugins-testkit';
1819
import { expect } from 'chai';
19-
import { createProjectWithDevServer, ensureSfCli, authOrgViaUrl } from './helpers/webappProjectUtils.js';
20-
import { spawnWebappDev, type WebappDevHandle } from './helpers/devServerUtils.js';
20+
import {
21+
createProjectWithDevServer,
22+
createProjectWithWebapp,
23+
writeManifest,
24+
ensureSfCli,
25+
authOrgViaUrl,
26+
} from './helpers/webappProjectUtils.js';
27+
import {
28+
spawnWebappDev,
29+
startTestHttpServer,
30+
startViteProxyServer,
31+
closeServer,
32+
type WebappDevHandle,
33+
} from './helpers/devServerUtils.js';
2134

2235
/* ------------------------------------------------------------------ *
23-
* Tier 2 — Full Flow (proxy startup via dev.command) *
36+
* Tier 2 — URL / proxy integration tests *
2437
* *
25-
* These tests let the command's own DevServerManager start a tiny *
26-
* Node HTTP server via `dev.command`, then verify the proxy boots *
27-
* and serves content. *
38+
* All suites share a single TestSession (one auth call) and test *
39+
* the three modes of dev server + proxy interaction: *
40+
* 1. Full flow: dev.command starts server, standalone proxy boots *
41+
* 2. Proxy-only: external server already running, proxy boots *
42+
* 3. Vite proxy: Vite plugin handles proxy, standalone skipped *
2843
* *
29-
* Requires TESTKIT_AUTH_URL or JWT credentials. Skips otherwise. *
44+
* Requires TESTKIT_AUTH_URL. Skips otherwise. *
3045
* ------------------------------------------------------------------ */
3146

32-
const DEV_PORT = 18_900;
47+
const FULL_FLOW_PORT = 18_900;
48+
const PROXY_ONLY_PORT = 18_930;
49+
const VITE_PORT = 18_940;
3350

34-
describe('webapp dev NUTs — Tier 2 full flow', function () {
35-
// Full flow tests may take a while (dev server + proxy startup)
51+
describe('webapp dev NUTs — Tier 2 URL/proxy integration', function () {
3652
this.timeout(180_000);
3753

3854
let session: TestSession;
3955
let targetOrg: string;
4056
let handle: WebappDevHandle | null = null;
57+
let externalServer: HttpServer | null = null;
4158

4259
before(async function () {
4360
if (!process.env.TESTKIT_AUTH_URL) {
@@ -56,59 +73,204 @@ describe('webapp dev NUTs — Tier 2 full flow', function () {
5673
await handle.kill();
5774
handle = null;
5875
}
76+
await closeServer(externalServer);
77+
externalServer = null;
5978
});
6079

6180
after(async () => {
6281
await session?.clean();
6382
});
6483

65-
it('should start proxy when dev.command starts a dev server', async () => {
66-
const { projectDir } = createProjectWithDevServer(session, 'fullFlow', 'myApp', DEV_PORT);
84+
// ── Full flow (dev.command starts dev server) ────────────────────
6785

68-
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
69-
cwd: projectDir,
70-
timeout: 120_000,
86+
describe('full flow', () => {
87+
it('should start proxy when dev.command starts a dev server', async () => {
88+
const { projectDir } = createProjectWithDevServer(session, 'fullFlow', 'myApp', FULL_FLOW_PORT);
89+
90+
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
91+
cwd: projectDir,
92+
timeout: 120_000,
93+
});
94+
95+
expect(handle.proxyUrl).to.be.a('string');
96+
expect(handle.proxyUrl).to.match(/^http:\/\/localhost:\d+$/);
97+
});
98+
99+
it('should serve proxied content from the dev server', async () => {
100+
const { projectDir } = createProjectWithDevServer(session, 'proxyContent', 'myApp', FULL_FLOW_PORT + 1);
101+
102+
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
103+
cwd: projectDir,
104+
timeout: 120_000,
105+
});
106+
107+
const response = await fetch(handle.proxyUrl);
108+
expect(response.status).to.equal(200);
109+
110+
const body = await response.text();
111+
expect(body).to.include('Test Dev Server');
71112
});
72113

73-
expect(handle.proxyUrl).to.be.a('string');
74-
expect(handle.proxyUrl).to.match(/^http:\/\/localhost:\d+$/);
114+
it('should emit JSON with proxy URL on stderr', async () => {
115+
const { projectDir } = createProjectWithDevServer(session, 'jsonOutput', 'myApp', FULL_FLOW_PORT + 2);
116+
117+
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
118+
cwd: projectDir,
119+
timeout: 120_000,
120+
});
121+
122+
expect(handle.proxyUrl).to.match(/^http:\/\/localhost:\d+$/);
123+
124+
const jsonLine = handle.stderr.split('\n').find((line) => {
125+
try {
126+
const parsed = JSON.parse(line.trim()) as Record<string, unknown>;
127+
return typeof parsed.url === 'string';
128+
} catch {
129+
return false;
130+
}
131+
});
132+
expect(jsonLine).to.be.a('string');
133+
});
75134
});
76135

77-
it('should serve proxied content from the dev server', async () => {
78-
const { projectDir } = createProjectWithDevServer(session, 'proxyContent', 'myApp', DEV_PORT + 1);
136+
// ── Proxy-only mode (external server already running) ────────────
137+
138+
describe('proxy-only mode', () => {
139+
it('should start proxy when --url points to an already-running server', async () => {
140+
externalServer = await startTestHttpServer(PROXY_ONLY_PORT);
141+
142+
const projectDir = createProjectWithWebapp(session, 'proxyOnly', 'myApp');
79143

80-
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
81-
cwd: projectDir,
82-
timeout: 120_000,
144+
handle = await spawnWebappDev(
145+
['--name', 'myApp', '--url', `http://localhost:${PROXY_ONLY_PORT}`, '--target-org', targetOrg],
146+
{ cwd: projectDir, timeout: 120_000 }
147+
);
148+
149+
expect(handle.proxyUrl).to.be.a('string');
150+
expect(handle.proxyUrl).to.match(/^http:\/\/localhost:\d+$/);
151+
const proxyPort = Number(new URL(handle.proxyUrl).port);
152+
expect(proxyPort).to.not.equal(PROXY_ONLY_PORT);
83153
});
84154

85-
const response = await fetch(handle.proxyUrl);
86-
expect(response.status).to.equal(200);
155+
it('should serve proxied content from the external server via --url', async () => {
156+
externalServer = await startTestHttpServer(PROXY_ONLY_PORT + 1);
157+
158+
const projectDir = createProjectWithWebapp(session, 'proxyOnlyContent', 'myApp');
159+
160+
handle = await spawnWebappDev(
161+
['--name', 'myApp', '--url', `http://localhost:${PROXY_ONLY_PORT + 1}`, '--target-org', targetOrg],
162+
{ cwd: projectDir, timeout: 120_000 }
163+
);
164+
165+
const response = await fetch(handle.proxyUrl);
166+
expect(response.status).to.equal(200);
167+
168+
const body = await response.text();
169+
expect(body).to.include('Manual Dev Server');
170+
});
87171

88-
const body = await response.text();
89-
expect(body).to.include('Test Dev Server');
172+
it('should start proxy when dev.url in manifest is already reachable (no dev.command needed)', async () => {
173+
externalServer = await startTestHttpServer(PROXY_ONLY_PORT + 2);
174+
175+
const projectDir = createProjectWithWebapp(session, 'proxyOnlyManifest', 'myApp');
176+
writeManifest(projectDir, 'myApp', {
177+
name: 'myApp',
178+
label: 'My App',
179+
version: '1.0.0',
180+
outputDir: 'dist',
181+
dev: {
182+
url: `http://localhost:${PROXY_ONLY_PORT + 2}`,
183+
},
184+
});
185+
186+
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
187+
cwd: projectDir,
188+
timeout: 120_000,
189+
});
190+
191+
expect(handle.proxyUrl).to.be.a('string');
192+
193+
const response = await fetch(handle.proxyUrl);
194+
expect(response.status).to.equal(200);
195+
196+
const body = await response.text();
197+
expect(body).to.include('Manual Dev Server');
198+
});
90199
});
91200

92-
it('should emit JSON with proxy URL on stderr', async () => {
93-
const { projectDir } = createProjectWithDevServer(session, 'jsonOutput', 'myApp', DEV_PORT + 2);
201+
// ── Vite proxy mode (dev server has built-in proxy) ──────────────
202+
203+
describe('Vite proxy mode', () => {
204+
it('should skip standalone proxy when Vite proxy is detected', async () => {
205+
externalServer = await startViteProxyServer(VITE_PORT);
206+
207+
const projectDir = createProjectWithWebapp(session, 'viteProxy', 'myApp');
208+
writeManifest(projectDir, 'myApp', {
209+
name: 'myApp',
210+
label: 'My App',
211+
version: '1.0.0',
212+
outputDir: 'dist',
213+
dev: {
214+
url: `http://localhost:${VITE_PORT}`,
215+
},
216+
});
217+
218+
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
219+
cwd: projectDir,
220+
timeout: 120_000,
221+
});
94222

95-
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
96-
cwd: projectDir,
97-
timeout: 120_000,
223+
// When Vite proxy is active, the emitted URL IS the dev server URL (no separate proxy)
224+
expect(handle.proxyUrl).to.equal(`http://localhost:${VITE_PORT}`);
98225
});
99226

100-
// spawnWebappDev already parsed the JSON line — verify it's well-formed
101-
expect(handle.proxyUrl).to.match(/^http:\/\/localhost:\d+$/);
102-
103-
// Double-check by scanning stderr for the raw JSON line
104-
const jsonLine = handle.stderr.split('\n').find((line) => {
105-
try {
106-
const parsed = JSON.parse(line.trim()) as Record<string, unknown>;
107-
return typeof parsed.url === 'string';
108-
} catch {
109-
return false;
110-
}
227+
it('should serve content directly from Vite server (no standalone proxy)', async () => {
228+
externalServer = await startViteProxyServer(VITE_PORT + 1);
229+
230+
const projectDir = createProjectWithWebapp(session, 'viteContent', 'myApp');
231+
writeManifest(projectDir, 'myApp', {
232+
name: 'myApp',
233+
label: 'My App',
234+
version: '1.0.0',
235+
outputDir: 'dist',
236+
dev: {
237+
url: `http://localhost:${VITE_PORT + 1}`,
238+
},
239+
});
240+
241+
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
242+
cwd: projectDir,
243+
timeout: 120_000,
244+
});
245+
246+
const response = await fetch(handle.proxyUrl);
247+
expect(response.status).to.equal(200);
248+
249+
const body = await response.text();
250+
expect(body).to.include('Vite Dev Server');
251+
});
252+
253+
it('should start standalone proxy when server lacks Vite proxy header', async () => {
254+
externalServer = await startTestHttpServer(VITE_PORT + 2);
255+
256+
const projectDir = createProjectWithWebapp(session, 'noViteProxy', 'myApp');
257+
writeManifest(projectDir, 'myApp', {
258+
name: 'myApp',
259+
label: 'My App',
260+
version: '1.0.0',
261+
outputDir: 'dist',
262+
dev: {
263+
url: `http://localhost:${VITE_PORT + 2}`,
264+
},
265+
});
266+
267+
handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], {
268+
cwd: projectDir,
269+
timeout: 120_000,
270+
});
271+
272+
expect(handle.proxyUrl).to.not.equal(`http://localhost:${VITE_PORT + 2}`);
273+
expect(handle.proxyUrl).to.match(/^http:\/\/localhost:\d+$/);
111274
});
112-
expect(jsonLine).to.be.a('string');
113275
});
114276
});

test/commands/webapp/helpers/devServerUtils.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import { spawn, type ChildProcess } from 'node:child_process';
18+
import { createServer as createHttpServer, type Server as HttpServer } from 'node:http';
1819
import { join } from 'node:path';
1920
import { createServer, type Server } from 'node:net';
2021

@@ -128,3 +129,51 @@ export function occupyPort(port: number): Promise<Server> {
128129
server.listen(port, '127.0.0.1', () => resolve(server));
129130
});
130131
}
132+
133+
/**
134+
* Start a plain HTTP server that serves static HTML content.
135+
* Used for proxy-only mode tests where the dev server is already running.
136+
*/
137+
export function startTestHttpServer(port: number): Promise<HttpServer> {
138+
return new Promise((resolve, reject) => {
139+
const server = createHttpServer((_, res) => {
140+
res.writeHead(200, { 'Content-Type': 'text/html' });
141+
res.end('<h1>Manual Dev Server</h1>');
142+
});
143+
server.on('error', reject);
144+
server.listen(port, '127.0.0.1', () => resolve(server));
145+
});
146+
}
147+
148+
/**
149+
* Start an HTTP server that mimics a Vite dev server with the
150+
* WebAppProxyHandler plugin active. Responds to health check requests
151+
* (`?sfProxyHealthCheck=true`) with `X-Salesforce-WebApp-Proxy: true`.
152+
*/
153+
export function startViteProxyServer(port: number): Promise<HttpServer> {
154+
return new Promise((resolve, reject) => {
155+
const server = createHttpServer((req, res) => {
156+
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
157+
if (url.searchParams.get('sfProxyHealthCheck') === 'true') {
158+
res.writeHead(200, {
159+
'Content-Type': 'text/plain',
160+
'X-Salesforce-WebApp-Proxy': 'true',
161+
});
162+
res.end('OK');
163+
return;
164+
}
165+
res.writeHead(200, { 'Content-Type': 'text/html' });
166+
res.end('<h1>Vite Dev Server</h1>');
167+
});
168+
server.on('error', reject);
169+
server.listen(port, '127.0.0.1', () => resolve(server));
170+
});
171+
}
172+
173+
/** Close an HTTP server and wait for it to finish. */
174+
export function closeServer(server: HttpServer | Server | null): Promise<void> {
175+
if (!server) return Promise.resolve();
176+
return new Promise((resolve) => {
177+
server.close(() => resolve());
178+
});
179+
}

test/commands/webapp/helpers/webappProjectUtils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,18 @@ export function ensureSfCli(): void {
3838
/**
3939
* Authenticate an org via TESTKIT_AUTH_URL without requiring DevHub.
4040
* Returns the authenticated username.
41+
*
42+
* Must be called once per TestSession since each session has its own
43+
* mock home directory where auth files are stored.
4144
*/
4245
export function authOrgViaUrl(): string {
4346
const authUrl = process.env.TESTKIT_AUTH_URL;
4447
if (!authUrl) {
4548
throw new Error('TESTKIT_AUTH_URL environment variable is not set.');
4649
}
4750

48-
const output = execSync(`echo "${authUrl}" | sf org login sfdx-url --sfdx-url-stdin --json`, {
51+
const output = execSync('sf org login sfdx-url --sfdx-url-stdin --json', {
52+
input: authUrl,
4953
stdio: ['pipe', 'pipe', 'pipe'],
5054
}).toString();
5155

0 commit comments

Comments
 (0)