1414 * limitations under the License.
1515 */
1616
17+ import type { Server as HttpServer } from 'node:http' ;
1718import { TestSession } from '@salesforce/cli-plugins-testkit' ;
1819import { 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 ( / ^ h t t p : \/ \/ l o c a l h o s t : \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 ( / ^ h t t p : \/ \/ l o c a l h o s t : \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 ( / ^ h t t p : \/ \/ l o c a l h o s t : \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 ( / ^ h t t p : \/ \/ l o c a l h o s t : \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 ( / ^ h t t p : \/ \/ l o c a l h o s t : \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 ( / ^ h t t p : \/ \/ l o c a l h o s t : \d + $ / ) ;
111274 } ) ;
112- expect ( jsonLine ) . to . be . a ( 'string' ) ;
113275 } ) ;
114276} ) ;
0 commit comments