@@ -6,26 +6,16 @@ import {
66 FORM_FIELDS_URL ,
77 BULK_CREATE_URL ,
88 TC_DETAILS_MAX_BATCH ,
9- BULK_CREATE_MAX_BATCH ,
10- MAX_SCENARIOS_PER_DOCUMENT ,
119} from "./config.js" ;
1210import {
1311 DefaultFieldMaps ,
1412 Scenario ,
1513 CreateTestCasesFromFileArgs ,
1614} from "./types.js" ;
17- import {
18- createTestCasePayload ,
19- chunkArray ,
20- canAcceptScenario ,
21- } from "./helpers.js" ;
15+ import { createTestCasePayload } from "./helpers.js" ;
2216import { getBrowserStackAuth } from "../../../lib/get-auth.js" ;
2317import { BrowserStackConfig } from "../../../lib/types.js" ;
2418import { getTMBaseURL } from "../../../lib/tm-base-url.js" ;
25- import logger from "../../../logger.js" ;
26-
27- const POLL_INTERVAL_MS = 10000 ;
28- const MAX_POLL_DURATION_MS = 8 * 60 * 1000 ;
2919
3020/**
3121 * Fetch default and custom form fields for a project.
@@ -143,7 +133,6 @@ export async function fetchTestCaseDetails(
143133export async function pollTestCaseDetails (
144134 traceRequestId : string ,
145135 config : BrowserStackConfig ,
146- deadline : number = Date . now ( ) + MAX_POLL_DURATION_MS ,
147136) : Promise < Record < string , any > > {
148137 const detailMap : Record < string , any > = { } ;
149138 let done = false ;
@@ -152,27 +141,18 @@ export async function pollTestCaseDetails(
152141
153142 while ( ! done ) {
154143 // add a bit of jitter to avoid synchronized polling storms
155- await new Promise ( ( r ) =>
156- setTimeout ( r , POLL_INTERVAL_MS + Math . random ( ) * 5000 ) ,
157- ) ;
158-
159- // Give up before the backend key TTL expires; return whatever we collected.
160- if ( Date . now ( ) > deadline ) break ;
144+ await new Promise ( ( r ) => setTimeout ( r , 10000 + Math . random ( ) * 5000 ) ) ;
161145
162146 const poll = await apiClient . post ( {
163147 url : `${ TCG_POLL_URL_VALUE } ?x-bstack-traceRequestId=${ encodeURIComponent ( traceRequestId ) } ` ,
164148 headers : {
165149 "API-TOKEN" : getBrowserStackAuth ( config ) ,
166150 } ,
167151 body : { } ,
168- // Don't throw on a non-2xx: an expired request key returns 400
169- // ("Request ids does not exists") and simply means there is nothing more
170- // to fetch — stop gracefully instead of failing the whole run.
171- raise_error : false ,
172152 } ) ;
173153
174- if ( poll . status !== 200 || ! poll . data ? .data ? .success ) {
175- break ;
154+ if ( ! poll . data . data . success ) {
155+ throw new Error ( `Polling failed: ${ poll . data . data . message } ` ) ;
176156 }
177157
178158 for ( const msg of poll . data . data . message ) {
@@ -210,55 +190,29 @@ export async function pollScenariosTestDetails(
210190 let iteratorCount = 0 ;
211191 const tmBaseUrl = await getTMBaseURL ( config ) ;
212192 const TCG_POLL_URL_VALUE = TCG_POLL_URL ( tmBaseUrl ) ;
213- const deadline = Date . now ( ) + MAX_POLL_DURATION_MS ;
214193
215194 // Promisify interval-style polling using a wrapper
216195 await new Promise < void > ( ( resolve , reject ) => {
217- let stopped = false ;
218-
219- const pollOnce = async ( ) => {
220- if ( stopped ) return ;
196+ const intervalId = setInterval ( async ( ) => {
221197 try {
222198 const poll = await apiClient . post ( {
223199 url : `${ TCG_POLL_URL_VALUE } ?x-bstack-traceRequestId=${ encodeURIComponent ( traceId ) } ` ,
224200 headers : {
225201 "API-TOKEN" : getBrowserStackAuth ( config ) ,
226202 } ,
227203 body : { } ,
228- raise_error : false ,
229204 } ) ;
230205
231206 if ( poll . status !== 200 ) {
232- stopped = true ;
233- if ( Object . keys ( scenariosMap ) . length > 0 ) {
234- resolve ( ) ;
235- } else {
236- reject (
237- new Error (
238- `Polling error: ${ poll . status } ${ typeof poll . data === "string" ? poll . data : JSON . stringify ( poll . data ) } ` ,
239- ) ,
240- ) ;
241- }
207+ clearInterval ( intervalId ) ;
208+ reject ( new Error ( `Polling error: ${ poll . statusText || poll . status } ` ) ) ;
242209 return ;
243210 }
244211
245- let terminated = false ;
246212 for ( const msg of poll . data . data . message ) {
247213 if ( msg . type === "scenario" ) {
248214 msg . data . scenarios . forEach ( ( sc : any ) => {
249- if (
250- canAcceptScenario (
251- scenariosMap ,
252- sc . id ,
253- MAX_SCENARIOS_PER_DOCUMENT ,
254- )
255- ) {
256- scenariosMap [ sc . id ] ||= {
257- id : sc . id ,
258- name : sc . name ,
259- testcases : [ ] ,
260- } ;
261- }
215+ scenariosMap [ sc . id ] = { id : sc . id , name : sc . name , testcases : [ ] } ;
262216 } ) ;
263217 const count = Object . keys ( scenariosMap ) . length ;
264218 await context . sendNotification ( {
@@ -274,32 +228,25 @@ export async function pollScenariosTestDetails(
274228
275229 if ( msg . type === "testcase" ) {
276230 const sc = msg . data . scenario ;
277- if (
278- sc &&
279- canAcceptScenario ( scenariosMap , sc . id , MAX_SCENARIOS_PER_DOCUMENT )
280- ) {
281- const array = Array . isArray ( msg . data . testcases )
282- ? msg . data . testcases
283- : msg . data . testcases
284- ? [ msg . data . testcases ]
285- : [ ] ;
286- const ids : string [ ] = array . map (
287- ( tc : any ) => tc . id || tc . test_case_id ,
231+ if ( sc ) {
232+ const array = (
233+ Array . isArray ( msg . data . testcases )
234+ ? msg . data . testcases
235+ : msg . data . testcases
236+ ? [ msg . data . testcases ]
237+ : [ ]
238+ ) . slice ( 0 , TC_DETAILS_MAX_BATCH ) ;
239+ const ids = array . map ( ( tc : any ) => tc . id || tc . test_case_id ) ;
240+
241+ const reqId = await fetchTestCaseDetails (
242+ documentId ,
243+ folderId ,
244+ projectReferenceId ,
245+ ids ,
246+ source ,
247+ config ,
288248 ) ;
289-
290- for ( const idChunk of chunkArray ( ids , TC_DETAILS_MAX_BATCH ) ) {
291- const reqId = await fetchTestCaseDetails (
292- documentId ,
293- folderId ,
294- projectReferenceId ,
295- idChunk ,
296- source ,
297- config ,
298- ) ;
299- detailPromises . push (
300- pollTestCaseDetails ( reqId , config , deadline ) ,
301- ) ;
302- }
249+ detailPromises . push ( pollTestCaseDetails ( reqId , config ) ) ;
303250
304251 scenariosMap [ sc . id ] ||= {
305252 id : sc . id ,
@@ -323,41 +270,20 @@ export async function pollScenariosTestDetails(
323270 }
324271
325272 if ( msg . type === "termination" ) {
326- terminated = true ;
273+ clearInterval ( intervalId ) ;
274+ resolve ( ) ;
327275 }
328276 }
329-
330- if ( terminated || Date . now ( ) > deadline ) {
331- stopped = true ;
332- logger . info (
333- `TCG scenario poll stopped (${ terminated ? "termination received" : "max duration reached" } ); ${ Object . keys ( scenariosMap ) . length } scenarios, ${ detailPromises . length } detail fetches` ,
334- ) ;
335- resolve ( ) ;
336- return ;
337- }
338- setTimeout ( pollOnce , POLL_INTERVAL_MS ) ;
339277 } catch ( err ) {
340- stopped = true ;
278+ clearInterval ( intervalId ) ;
341279 reject ( err ) ;
342280 }
343- } ;
344- setTimeout ( pollOnce , POLL_INTERVAL_MS ) ;
281+ } , 10000 ) ; // 10 second interval
345282 } ) ;
346283
347- const detailsList = await Promise . allSettled ( detailPromises ) ;
348- const rejectedDetails = detailsList . filter (
349- ( r ) => r . status === "rejected" ,
350- ) . length ;
351- if ( rejectedDetails > 0 ) {
352- logger . info (
353- `TCG detail fetches: ${ detailsList . length - rejectedDetails } /${ detailsList . length } succeeded, ${ rejectedDetails } failed (degrading gracefully)` ,
354- ) ;
355- }
356- const allDetails = detailsList . reduce < Record < string , any > > (
357- ( acc , result ) =>
358- result . status === "fulfilled" ? { ...acc , ...result . value } : acc ,
359- { } ,
360- ) ;
284+ // once all detail fetches are triggered, wait for them to complete
285+ const detailsList = await Promise . all ( detailPromises ) ;
286+ const allDetails = detailsList . reduce ( ( acc , cur ) => ( { ...acc , ...cur } ) , { } ) ;
361287
362288 // attach the fetched detail objects back to each testcase
363289 for ( const scenario of Object . values ( scenariosMap ) ) {
@@ -384,65 +310,44 @@ export async function bulkCreateTestCases(
384310 documentId : number ,
385311 config : BrowserStackConfig ,
386312) : Promise < string > {
313+ const results : Record < string , any > = { } ;
387314 const total = Object . keys ( scenariosMap ) . length ;
388315 let doneCount = 0 ;
389316 let testCaseCount = 0 ;
390- const failedScenarios : string [ ] = [ ] ;
391317 const tmBaseUrl = await getTMBaseURL ( config ) ;
392318 const BULK_CREATE_URL_VALUE = BULK_CREATE_URL ( tmBaseUrl , projectId , folderId ) ;
393319
394320 for ( const { id, testcases } of Object . values ( scenariosMap ) ) {
395- if ( testcases . length === 0 ) continue ;
396-
397- const batches = chunkArray ( testcases , BULK_CREATE_MAX_BATCH ) ;
398- let createdInScenario = 0 ;
399- let scenarioFailed = false ;
400-
401- for ( const batch of batches ) {
402- const payload = {
403- test_cases : batch . map ( ( tc ) =>
404- createTestCasePayload (
405- tc ,
406- id ,
407- folderId ,
408- fieldMaps ,
409- documentId ,
410- booleanFieldId ,
411- traceId ,
412- ) ,
321+ // Cap per-scenario test cases to the backend's per-request limit so the
322+ // bulk-create call never exceeds it ("More than permitted test cases sent").
323+ const cappedTestcases = testcases . slice ( 0 , TC_DETAILS_MAX_BATCH ) ;
324+ const testCaseLength = cappedTestcases . length ;
325+ testCaseCount += testCaseLength ;
326+ if ( testCaseLength === 0 ) continue ;
327+ const payload = {
328+ test_cases : cappedTestcases . map ( ( tc ) =>
329+ createTestCasePayload (
330+ tc ,
331+ id ,
332+ folderId ,
333+ fieldMaps ,
334+ documentId ,
335+ booleanFieldId ,
336+ traceId ,
413337 ) ,
414- } ;
415-
416- try {
417- await apiClient . post ( {
418- url : BULK_CREATE_URL_VALUE ,
419- headers : {
420- "API-TOKEN" : getBrowserStackAuth ( config ) ,
421- "Content-Type" : "application/json" ,
422- } ,
423- body : payload ,
424- } ) ;
425- createdInScenario += batch . length ;
426- } catch ( error ) {
427- scenarioFailed = true ;
428- await context . sendNotification ( {
429- method : "notifications/progress" ,
430- params : {
431- progressToken : context . _meta ?. progressToken ?? traceId ,
432- message : `Creation failed for scenario ${ id } : ${ error instanceof Error ? error . message : "Unknown error" } ` ,
433- total,
434- progress : doneCount ,
435- } ,
436- } ) ;
437- }
438- }
338+ ) ,
339+ } ;
439340
440- testCaseCount += createdInScenario ;
441- if ( scenarioFailed ) {
442- failedScenarios . push ( id ) ;
443- }
444- if ( createdInScenario > 0 ) {
445- doneCount ++ ;
341+ try {
342+ const resp = await apiClient . post ( {
343+ url : BULK_CREATE_URL_VALUE ,
344+ headers : {
345+ "API-TOKEN" : getBrowserStackAuth ( config ) ,
346+ "Content-Type" : "application/json" ,
347+ } ,
348+ body : payload ,
349+ } ) ;
350+ results [ id ] = resp . data ;
446351 await context . sendNotification ( {
447352 method : "notifications/progress" ,
448353 params : {
@@ -452,12 +357,23 @@ export async function bulkCreateTestCases(
452357 progress : doneCount ,
453358 } ,
454359 } ) ;
360+ } catch ( error ) {
361+ //send notification
362+ await context . sendNotification ( {
363+ method : "notifications/progress" ,
364+ params : {
365+ progressToken : context . _meta ?. progressToken ?? traceId ,
366+ message : `Creation failed for scenario ${ id } : ${ error instanceof Error ? error . message : "Unknown error" } ` ,
367+ total,
368+ progress : doneCount ,
369+ } ,
370+ } ) ;
371+ //continue to next scenario
372+ continue ;
455373 }
374+ doneCount ++ ;
456375 }
457- let resultString = `Total of ${ testCaseCount } test cases created in ${ doneCount } of ${ total } scenarios.` ;
458- if ( failedScenarios . length > 0 ) {
459- resultString += ` Failed to create test cases for ${ failedScenarios . length } scenario(s): ${ failedScenarios . join ( ", " ) } .` ;
460- }
376+ const resultString = `Total of ${ testCaseCount } test cases created in ${ total } scenarios.` ;
461377 return resultString ;
462378}
463379
0 commit comments