@lowdefy/server-dev 6.1.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (624) hide show
  1. package/client/App.jsx +52 -24
  2. package/client/Inspector.jsx +73 -30
  3. package/client/JourneyObserver.jsx +47 -0
  4. package/client/Page.jsx +54 -22
  5. package/client/Recorder.jsx +73 -0
  6. package/client/Routing.jsx +37 -13
  7. package/client/clientBuildImports.test.mjs +2 -0
  8. package/client/devStreamConnections.test.mjs +40 -0
  9. package/client/feedback/FeedbackMount.jsx +2 -2
  10. package/client/feedback/FeedbackOverlay.jsx +7 -4
  11. package/client/feedback/feedbackStyles.js +3 -0
  12. package/client/journeyObserver/startJourneyObserver.js +101 -0
  13. package/client/journeyObserver/startJourneyObserver.test.mjs +149 -0
  14. package/client/openInEditor/OpenInEditorListener.jsx +2 -0
  15. package/client/recorder/buildEngineRecord.js +37 -0
  16. package/client/recorder/buildInteractionRecord.js +64 -0
  17. package/client/recorder/buildTraceEvent.js +57 -0
  18. package/client/recorder/buildTraceEvent.test.mjs +81 -0
  19. package/client/recorder/chunkRecords.js +69 -0
  20. package/client/recorder/chunkRecords.test.mjs +59 -0
  21. package/client/recorder/collectEventActions.js +30 -0
  22. package/client/recorder/collectEventEndpoints.js +40 -0
  23. package/client/recorder/collectEventRequests.js +39 -0
  24. package/client/recorder/createPairingBuffer.js +187 -0
  25. package/client/recorder/createPairingBuffer.test.mjs +405 -0
  26. package/client/recorder/createPasswordRedactor.js +84 -0
  27. package/client/recorder/createRecordBatcher.js +54 -0
  28. package/client/recorder/createRecordBatcher.test.mjs +76 -0
  29. package/client/recorder/createRecorder.js +216 -0
  30. package/client/recorder/createRecorder.test.mjs +318 -0
  31. package/client/recorder/createRecordingSession.js +61 -0
  32. package/client/recorder/createRecordingSession.test.mjs +72 -0
  33. package/client/recorder/diffStateWrites.js +77 -0
  34. package/client/recorder/diffStateWrites.test.mjs +66 -0
  35. package/client/recorder/isEventCall.js +30 -0
  36. package/client/recorder/jsonType.js +39 -0
  37. package/client/recorder/keyChord.js +47 -0
  38. package/client/recorder/keyChord.test.mjs +50 -0
  39. package/client/recorder/sendRecords.js +40 -0
  40. package/client/recorder/toJsonSafe.js +29 -0
  41. package/lib/client/BuildingPage.jsx +3 -1
  42. package/lib/client/ErrorBar.jsx +17 -13
  43. package/lib/client/RedirectingPage.jsx +56 -0
  44. package/lib/client/auth/Auth.jsx +11 -6
  45. package/lib/client/auth/AuthConfigured.jsx +222 -36
  46. package/lib/client/auth/AuthNotConfigured.js +45 -4
  47. package/lib/client/auth/authNotConfiguredParity.test.mjs +61 -0
  48. package/lib/client/auth/authTemplateSync.test.mjs +40 -0
  49. package/lib/client/auth/betterAuthSessionContract.test.mjs +181 -0
  50. package/lib/client/auth/createFetchCheckingBuild.js +45 -0
  51. package/lib/client/bundleBuildId.js +25 -0
  52. package/lib/client/collectVisibleBlockIds.js +51 -0
  53. package/lib/client/getCspNonce.js +25 -0
  54. package/lib/client/getPagePath.js +36 -0
  55. package/lib/client/getPagePath.test.mjs +51 -0
  56. package/lib/client/utils/formatErrorsForCopy.js +30 -0
  57. package/lib/client/utils/formatErrorsForCopy.test.mjs +39 -0
  58. package/lib/client/utils/getErrorBarColor.js +25 -0
  59. package/lib/client/utils/getErrorBarColor.test.mjs +34 -0
  60. package/lib/client/utils/request.js +8 -1
  61. package/lib/client/utils/usePageConfig.js +89 -25
  62. package/lib/client/utils/usePageConfig.test.mjs +198 -13
  63. package/lib/configTree/buildConfigTree.mjs +58 -0
  64. package/lib/configTree/buildConfigTree.test.mjs +77 -0
  65. package/lib/configTree/collectConfigText.mjs +101 -0
  66. package/lib/configTree/collectConfigText.test.mjs +116 -0
  67. package/lib/configTree/prepareScratchServer.mjs +46 -0
  68. package/lib/configTree/runConfigTreeBuild.mjs +88 -0
  69. package/lib/configTree/runConfigTreeBuild.skipPluginChecks.test.mjs +74 -0
  70. package/lib/configTree/runConfigTreeBuild.test.mjs +202 -0
  71. package/lib/docs/JourneyStepError.js +30 -0
  72. package/lib/docs/acquireBrowserSlot.js +30 -0
  73. package/lib/docs/addNestedEndpoints.js +54 -0
  74. package/lib/docs/addNestedEndpoints.test.mjs +185 -0
  75. package/lib/docs/addWriteFlags.js +77 -0
  76. package/lib/docs/buildEditedPages.js +50 -0
  77. package/lib/docs/buildWaitHeader.js +21 -0
  78. package/lib/docs/captureAnnotatedScreenshot.js +23 -12
  79. package/lib/docs/checkAgentFiles.js +148 -0
  80. package/lib/docs/checkAgentFiles.test.mjs +225 -0
  81. package/lib/docs/checkStepReferences.js +239 -0
  82. package/lib/docs/checkStepReferences.test.mjs +173 -0
  83. package/lib/docs/checkpointStore.js +3 -0
  84. package/lib/docs/clientErrorStore.js +18 -1
  85. package/lib/docs/clientErrorStore.test.mjs +46 -0
  86. package/lib/docs/collectExercised.js +52 -0
  87. package/lib/docs/createBrowserLifecycle.js +102 -0
  88. package/lib/docs/createBrowserLifecycle.test.mjs +212 -0
  89. package/lib/docs/createDocsMcpServer.js +345 -388
  90. package/lib/docs/createDocsMcpServer.test.mjs +644 -4
  91. package/lib/docs/createDocsMcpServerRunJourney.test.mjs +148 -0
  92. package/lib/docs/createJourneyActors.chromium.test.mjs +227 -0
  93. package/lib/docs/createJourneyActors.js +213 -0
  94. package/lib/docs/createJourneyActors.test.mjs +79 -0
  95. package/lib/docs/createJourneyObservations.js +80 -0
  96. package/lib/docs/createLeftOriginError.js +26 -0
  97. package/lib/docs/createNetworkCounter.js +101 -0
  98. package/lib/docs/dataSets/checkSharedCollectionIds.js +78 -0
  99. package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
  100. package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +727 -0
  101. package/lib/docs/dataSets/describeDataSetResult.js +25 -0
  102. package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
  103. package/lib/docs/dataSets/describeTargetKey.js +29 -0
  104. package/lib/docs/dataSets/deserializeFixture.js +34 -0
  105. package/lib/docs/dataSets/drainSessionWork.js +37 -0
  106. package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
  107. package/lib/docs/dataSets/duplicateIndexName.js +24 -0
  108. package/lib/docs/dataSets/getDataStore.js +106 -0
  109. package/lib/docs/dataSets/getDataStore.mongodb.test.mjs +63 -0
  110. package/lib/docs/dataSets/getDataStore.test.mjs +195 -0
  111. package/lib/docs/dataSets/groupDataSetByCollection.js +52 -0
  112. package/lib/docs/dataSets/insertFixture.js +57 -0
  113. package/lib/docs/dataSets/insertGeneratedDocuments.js +41 -0
  114. package/lib/docs/dataSets/isDataStoreGone.js +29 -0
  115. package/lib/docs/dataSets/loadDataSetCollection.js +42 -0
  116. package/lib/docs/dataSets/mergeCollectionIndexes.js +52 -0
  117. package/lib/docs/dataSets/mergeCollectionIndexes.test.mjs +59 -0
  118. package/lib/docs/dataSets/openDataSession.js +133 -0
  119. package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +493 -0
  120. package/lib/docs/dataSets/pickDataStorePort.js +58 -0
  121. package/lib/docs/dataSets/pickDataStorePort.test.mjs +63 -0
  122. package/lib/docs/dataSets/readConnectionArtifacts.js +46 -0
  123. package/lib/docs/dataSets/readDataSession.js +41 -0
  124. package/lib/docs/dataSets/readDataSession.test.mjs +57 -0
  125. package/lib/docs/dataSets/readDataSet.js +83 -0
  126. package/lib/docs/dataSets/readDataSet.test.mjs +251 -0
  127. package/lib/docs/dataSets/registerDataStoreShutdown.js +47 -0
  128. package/lib/docs/dataSets/removeDataStoreFiles.js +32 -0
  129. package/lib/docs/dataSets/resolveDataSetCollection.js +46 -0
  130. package/lib/docs/dataSets/resolveJourneyDataSet.js +106 -0
  131. package/lib/docs/dataSets/resolveJourneyDataSet.test.mjs +130 -0
  132. package/lib/docs/dataSets/sessionDatabaseName.js +32 -0
  133. package/lib/docs/dataSets/sweepOrphanDatabases.js +36 -0
  134. package/lib/docs/dataSets/test/app/lowdefy.yaml +225 -0
  135. package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
  136. package/lib/docs/dataSets/test/app/tests/data/shop.yaml +12 -0
  137. package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
  138. package/lib/docs/devEventBus.js +136 -0
  139. package/lib/docs/devEventBus.test.mjs +269 -0
  140. package/lib/docs/devToolDefinitions.js +529 -0
  141. package/lib/docs/docSources.js +21 -0
  142. package/lib/docs/docs.test.mjs +171 -15
  143. package/lib/docs/docsIndex.test.mjs +377 -0
  144. package/lib/docs/evalOperator.js +5 -3
  145. package/lib/docs/evalOperatorHeadless.js +84 -12
  146. package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
  147. package/lib/docs/evalOperatorInTab.js +2 -1
  148. package/lib/docs/findConfig.js +95 -49
  149. package/lib/docs/fitResponse.js +81 -0
  150. package/lib/docs/fitResponse.test.mjs +128 -0
  151. package/lib/docs/formatFeedback.js +26 -5
  152. package/lib/docs/formatFeedback.test.mjs +48 -0
  153. package/lib/docs/getAppMap.js +7 -1
  154. package/lib/docs/getBrowser.js +150 -80
  155. package/lib/docs/getBrowser.test.mjs +407 -16
  156. package/lib/docs/getBuildId.js +61 -0
  157. package/lib/docs/getBuildId.test.mjs +84 -0
  158. package/lib/docs/getBuildStatus.js +81 -7
  159. package/lib/docs/getBuildStatus.test.mjs +229 -0
  160. package/lib/docs/getBuildStatusAfterEdits.js +35 -0
  161. package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
  162. package/lib/docs/getCoreDoc.js +9 -1
  163. package/lib/docs/getDoc.js +100 -0
  164. package/lib/docs/getDocsIndex.js +152 -0
  165. package/lib/docs/getHazards.js +115 -0
  166. package/lib/docs/getHazards.test.mjs +175 -0
  167. package/lib/docs/getMailOutboxDirectory.js +26 -0
  168. package/lib/docs/getOverview.js +79 -13
  169. package/lib/docs/getOverviewBasePath.test.mjs +37 -0
  170. package/lib/docs/getPageBuildStatus.js +46 -0
  171. package/lib/docs/getPluginDoc.js +21 -23
  172. package/lib/docs/getSchema.js +33 -1
  173. package/lib/docs/getStaleStatus.js +36 -0
  174. package/lib/docs/getStaleStatus.test.mjs +74 -0
  175. package/lib/docs/guardJourneyOrigin.js +39 -0
  176. package/lib/docs/inspectState.js +5 -3
  177. package/lib/docs/inspectStateFromTab.js +2 -2
  178. package/lib/docs/inspectStateHeadless.js +89 -27
  179. package/lib/docs/inspectStateHeadless.test.mjs +20 -1
  180. package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
  181. package/lib/docs/installHeadlessShell.js +123 -0
  182. package/lib/docs/installHeadlessShell.test.mjs +70 -0
  183. package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
  184. package/lib/docs/isHeadlessShellIncomplete.js +34 -0
  185. package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
  186. package/lib/docs/isPageReady.js +77 -0
  187. package/lib/docs/isPageReady.test.mjs +235 -0
  188. package/lib/docs/isRunErrorEntry.js +25 -0
  189. package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
  190. package/lib/docs/journeyRunRecording.js +35 -0
  191. package/lib/docs/journeyRunRecording.test.mjs +39 -0
  192. package/lib/docs/keepEventStreamAlive.js +68 -0
  193. package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
  194. package/lib/docs/launchBrowser.js +83 -0
  195. package/lib/docs/launchBrowser.test.mjs +175 -0
  196. package/lib/docs/listModuleDocEntries.js +69 -0
  197. package/lib/docs/listPackageDocFiles.js +67 -0
  198. package/lib/docs/listPluginDocEntries.js +111 -0
  199. package/lib/docs/listPlugins.js +9 -58
  200. package/lib/docs/listTypes.js +14 -6
  201. package/lib/docs/loadFrameworkHazards.js +40 -0
  202. package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
  203. package/lib/docs/loadState.js +103 -35
  204. package/lib/docs/makeModuleManifestDoc.js +174 -0
  205. package/lib/docs/matchExercisedPages.js +35 -0
  206. package/lib/docs/matchJourneyRoute.js +69 -0
  207. package/lib/docs/matchJourneyRoute.test.mjs +178 -0
  208. package/lib/docs/mergeNetworkSnapshots.js +49 -0
  209. package/lib/docs/nextJourneySequence.js +30 -0
  210. package/lib/docs/noBrowserError.js +22 -0
  211. package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
  212. package/lib/docs/observe/claimExpectedErrors.js +67 -0
  213. package/lib/docs/observe/createJourneyAppErrors.js +236 -0
  214. package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
  215. package/lib/docs/observe/evaluateInvariants.js +183 -0
  216. package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
  217. package/lib/docs/observe/findingKey.js +29 -0
  218. package/lib/docs/observe/hasNoEffect.js +31 -0
  219. package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
  220. package/lib/docs/observe/installStepObserver.js +72 -0
  221. package/lib/docs/observe/isAppApiUrl.js +33 -0
  222. package/lib/docs/observe/parseAppApiUrl.js +37 -0
  223. package/lib/docs/observe/readStepObserver.js +42 -0
  224. package/lib/docs/observe/relativeSource.js +29 -0
  225. package/lib/docs/observe/resolveConfigKeySource.js +33 -0
  226. package/lib/docs/observe/takeJourneyEvents.js +36 -0
  227. package/lib/docs/observe/toAppErrorFailure.js +61 -0
  228. package/lib/docs/observe/usesSearchStage.js +45 -0
  229. package/lib/docs/observe/waitForClientErrorReports.js +31 -0
  230. package/lib/docs/observe/watchJourneyContext.js +75 -0
  231. package/lib/docs/observe/withAppErrors.js +31 -0
  232. package/lib/docs/observe/withAppErrors.test.mjs +101 -0
  233. package/lib/docs/openJourney.js +94 -0
  234. package/lib/docs/openJourneyEmail.js +41 -0
  235. package/lib/docs/openJourneyEmail.test.mjs +187 -0
  236. package/lib/docs/pageBlocks.js +47 -0
  237. package/lib/docs/pageBuildStatus.test.mjs +103 -0
  238. package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
  239. package/lib/docs/rails/authEngineActions.js +53 -0
  240. package/lib/docs/rails/authEngineActions.test.mjs +69 -0
  241. package/lib/docs/rails/authEngineMethods.js +52 -0
  242. package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
  243. package/lib/docs/rails/checkAuthActionsRan.js +66 -0
  244. package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
  245. package/lib/docs/rails/checkClickRails.js +56 -0
  246. package/lib/docs/rails/createDataSetRails.js +82 -0
  247. package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
  248. package/lib/docs/rails/findExternalBlocks.js +147 -0
  249. package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
  250. package/lib/docs/rails/installAuthActionRecorder.js +56 -0
  251. package/lib/docs/rails/readAuthActionRecorder.js +32 -0
  252. package/lib/docs/rails/readBuiltArtifact.js +30 -0
  253. package/lib/docs/readConnectionPlugins.js +26 -0
  254. package/lib/docs/readDevAuthMode.js +32 -0
  255. package/lib/docs/readDocEntry.js +40 -0
  256. package/lib/docs/readDocTitle.js +26 -0
  257. package/lib/docs/readJourneyEmailMatch.js +57 -0
  258. package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
  259. package/lib/docs/readJourneySession.js +56 -0
  260. package/lib/docs/readMailOutbox.js +37 -0
  261. package/lib/docs/readPagePath.js +27 -0
  262. package/lib/docs/readPluginPackages.js +99 -0
  263. package/lib/docs/readProxyBuildWait.js +38 -0
  264. package/lib/docs/readRoutineSteps.js +45 -0
  265. package/lib/docs/registerShownTextEngine.js +118 -0
  266. package/lib/docs/requestRestart.js +30 -0
  267. package/lib/docs/requestRestart.test.mjs +49 -0
  268. package/lib/docs/resolvePageInstance.js +46 -0
  269. package/lib/docs/resolvePageInstance.test.mjs +81 -0
  270. package/lib/docs/resolveSource.js +11 -7
  271. package/lib/docs/resolveSource.test.mjs +1 -1
  272. package/lib/docs/resolveToolCaller.js +62 -0
  273. package/lib/docs/reviewPage.js +51 -0
  274. package/lib/docs/reviewPageBuilds.js +56 -0
  275. package/lib/docs/reviewPageBuilds.test.mjs +207 -0
  276. package/lib/docs/runCheck.js +75 -0
  277. package/lib/docs/runCheck.test.mjs +91 -0
  278. package/lib/docs/runCheckChild.js +49 -0
  279. package/lib/docs/runEndpoint.js +185 -0
  280. package/lib/docs/runEndpoint.test.mjs +509 -0
  281. package/lib/docs/runErrorBuffers.js +99 -0
  282. package/lib/docs/runErrorBuffers.test.mjs +95 -0
  283. package/lib/docs/runJourney.js +347 -0
  284. package/lib/docs/runJourney.test.mjs +2383 -0
  285. package/lib/docs/runJourneyAsUsers.js +44 -0
  286. package/lib/docs/runJourneyDataSet.test.mjs +274 -0
  287. package/lib/docs/runJourneySteps.js +1320 -0
  288. package/lib/docs/runRequest.js +100 -59
  289. package/lib/docs/runRequest.test.mjs +190 -0
  290. package/lib/docs/saveAnnotatedScreenshot.js +1 -1
  291. package/lib/docs/scaffoldPage.js +3 -1
  292. package/lib/docs/screenshotPage.js +163 -11
  293. package/lib/docs/screenshotPage.test.mjs +263 -1
  294. package/lib/docs/screenshotPageClip.test.mjs +4 -0
  295. package/lib/docs/searchDocs.js +82 -30
  296. package/lib/docs/searchDocs.test.mjs +100 -0
  297. package/lib/docs/searchIcons.js +90 -0
  298. package/lib/docs/searchIcons.test.mjs +127 -0
  299. package/lib/docs/selectExercised.js +39 -0
  300. package/lib/docs/selectFinalState.js +66 -0
  301. package/lib/docs/selectFinalState.test.mjs +51 -0
  302. package/lib/docs/serverErrorStore.js +54 -0
  303. package/lib/docs/serverErrorStore.test.mjs +50 -0
  304. package/lib/docs/setupTestFixtures.mjs +133 -39
  305. package/lib/docs/shownTextSelector.js +28 -0
  306. package/lib/docs/snapshotState.js +13 -5
  307. package/lib/docs/splitSearchTerms.js +62 -0
  308. package/lib/docs/splitSearchTerms.test.mjs +50 -0
  309. package/lib/docs/stateCheckpoints.test.mjs +77 -0
  310. package/lib/docs/tabAvailable.js +6 -5
  311. package/lib/docs/tabChannel.js +127 -21
  312. package/lib/docs/tabChannel.test.mjs +187 -29
  313. package/lib/docs/unsettledPageNote.js +24 -0
  314. package/lib/docs/validateJourneyMail.js +36 -0
  315. package/lib/docs/validateJourneyTimeout.js +38 -0
  316. package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
  317. package/lib/docs/validateRecordingParam.js +42 -0
  318. package/lib/docs/validateStateSelection.js +33 -0
  319. package/lib/docs/validateStateSelection.test.mjs +33 -0
  320. package/lib/docs/validateViewport.js +51 -0
  321. package/lib/docs/validateViewport.test.mjs +54 -0
  322. package/lib/docs/waitForBuild.js +50 -0
  323. package/lib/docs/waitForBuild.test.mjs +75 -0
  324. package/lib/docs/waitForJourneyEmail.js +77 -0
  325. package/lib/docs/withBrowserSlot.js +45 -0
  326. package/lib/docs/withBrowserSlot.test.mjs +75 -0
  327. package/lib/docs/withDataSession.js +53 -0
  328. package/lib/server/applyDataSetRedirect.js +92 -0
  329. package/lib/server/applyDataSetRedirect.test.mjs +446 -0
  330. package/lib/server/applyDataSetRedirectTenantReach.test.mjs +207 -0
  331. package/lib/server/applyDataSetRedirectWalled.test.mjs +134 -0
  332. package/lib/server/auth/createSystemContext.js +73 -0
  333. package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
  334. package/lib/server/auth/getHeadlessUser.js +9 -8
  335. package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
  336. package/lib/server/auth/getMockUser.js +18 -14
  337. package/lib/server/auth/{session.js → getStrategies.js} +17 -16
  338. package/lib/server/auth/headlessUser.js +3 -2
  339. package/lib/server/auth/journeyActor.js +35 -0
  340. package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
  341. package/lib/server/checkPageRecord.js +44 -0
  342. package/lib/server/contextMapBudget.js +24 -0
  343. package/lib/server/contextMapBudget.test.mjs +98 -0
  344. package/lib/server/continueJitKeys.js +28 -0
  345. package/lib/server/continueJitKeys.test.mjs +43 -0
  346. package/lib/server/createDataSessionEndedError.js +33 -0
  347. package/lib/server/createDataSetRefusal.js +26 -0
  348. package/lib/server/createLowdefyContext.js +168 -41
  349. package/lib/server/createLowdefyContext.test.mjs +331 -8
  350. package/lib/server/getClientAddress.js +51 -0
  351. package/lib/server/getClientAddress.test.mjs +56 -0
  352. package/lib/server/getPageJitEnrichment.test.mjs +29 -11
  353. package/lib/server/hashConfigContent.js +26 -0
  354. package/lib/server/importAppCode.test.mjs +57 -0
  355. package/lib/server/isRoutineStep.js +27 -0
  356. package/lib/server/jitPageBuilder.js +309 -126
  357. package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
  358. package/lib/server/jitPageBuilder.test.mjs +218 -0
  359. package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
  360. package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
  361. package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
  362. package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
  363. package/lib/server/journeyCookies.js +22 -0
  364. package/lib/server/journeyCookies.test.mjs +145 -0
  365. package/lib/server/loadDynamicJsMap.js +47 -0
  366. package/lib/server/log/createHandleError.js +38 -2
  367. package/lib/server/log/createHandleError.test.mjs +125 -2
  368. package/lib/server/log/createLogger.js +3 -0
  369. package/lib/server/log/createLogger.test.mjs +15 -0
  370. package/lib/server/log/logRequest.js +2 -3
  371. package/lib/server/mutantDataSetWrappers.test.mjs +115 -0
  372. package/lib/server/mutants/applyMutant.js +39 -0
  373. package/lib/server/mutants/artifactPatterns.js +25 -0
  374. package/lib/server/mutants/buildAnchor.js +45 -0
  375. package/lib/server/mutants/collectSharedIds.js +41 -0
  376. package/lib/server/mutants/configPathSegments.js +38 -0
  377. package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
  378. package/lib/server/mutants/describeEvent.js +22 -0
  379. package/lib/server/mutants/enumerateArtifact.js +56 -0
  380. package/lib/server/mutants/findNodeByKey.js +45 -0
  381. package/lib/server/mutants/findNodeByPointer.js +49 -0
  382. package/lib/server/mutants/getArtifactKind.js +32 -0
  383. package/lib/server/mutants/groupMutantCopies.js +56 -0
  384. package/lib/server/mutants/hashArtifact.js +25 -0
  385. package/lib/server/mutants/hashMutantTarget.js +34 -0
  386. package/lib/server/mutants/listMutants.js +122 -0
  387. package/lib/server/mutants/listMutants.test.mjs +502 -0
  388. package/lib/server/mutants/mutantId.js +30 -0
  389. package/lib/server/mutants/mutantRuns.js +57 -0
  390. package/lib/server/mutants/mutantRuns.test.mjs +136 -0
  391. package/lib/server/mutants/operators/dropAction.js +47 -0
  392. package/lib/server/mutants/operators/dropBlock.js +44 -0
  393. package/lib/server/mutants/operators/dropPayload.js +52 -0
  394. package/lib/server/mutants/operators/dropStep.js +44 -0
  395. package/lib/server/mutants/operators/flipVisible.js +53 -0
  396. package/lib/server/mutants/operators/index.js +37 -0
  397. package/lib/server/mutants/operators/retargetLink.js +72 -0
  398. package/lib/server/mutants/operators/skipValidate.js +77 -0
  399. package/lib/server/mutants/operators/swapIf.js +112 -0
  400. package/lib/server/mutants/operators.test.mjs +396 -0
  401. package/lib/server/mutants/positionFreePath.js +38 -0
  402. package/lib/server/mutants/removeFromParent.js +28 -0
  403. package/lib/server/mutants/test/fixtures.mjs +173 -0
  404. package/lib/server/mutants/test/keyArtifact.mjs +63 -0
  405. package/lib/server/mutants/validateMutantParam.js +69 -0
  406. package/lib/server/mutants/validateMutantsBody.js +88 -0
  407. package/lib/server/mutants/walkArtifact.js +179 -0
  408. package/lib/server/pageBuildRecords.js +131 -0
  409. package/lib/server/pageBuildRecords.test.mjs +258 -0
  410. package/lib/server/pageCache.mjs +22 -7
  411. package/lib/server/pageCache.test.mjs +22 -7
  412. package/lib/server/pageRebuilds.test.mjs +543 -0
  413. package/lib/server/pruneJitMaps.js +36 -0
  414. package/lib/server/pruneJitMaps.test.mjs +55 -0
  415. package/lib/server/readInstanceRecord.js +33 -0
  416. package/lib/server/readInstanceRecord.test.mjs +99 -0
  417. package/lib/server/readMergedMaps.js +86 -0
  418. package/lib/server/readMergedMaps.test.mjs +79 -0
  419. package/lib/server/recording/appendRecords.js +33 -0
  420. package/lib/server/recording/errorRecordingStamp.js +34 -0
  421. package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
  422. package/lib/server/recording/isRecordingEnabled.js +31 -0
  423. package/lib/server/recording/readRecordingCookie.js +71 -0
  424. package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
  425. package/lib/server/recording/recordingCookiePayload.js +33 -0
  426. package/lib/server/recording/servedBuilds.js +46 -0
  427. package/lib/server/recording/servedBuilds.test.mjs +42 -0
  428. package/lib/server/sinkExternalConnections.js +95 -0
  429. package/lib/server/sinkExternalConnections.test.mjs +135 -0
  430. package/lib/server/skipStaleMapWrites.js +62 -0
  431. package/lib/server/skipStaleMapWrites.test.mjs +113 -0
  432. package/lib/server/startIdleGc.js +67 -0
  433. package/lib/server/startIdleGc.test.mjs +138 -0
  434. package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
  435. package/lib/server/trackSessionWork.js +30 -0
  436. package/lib/vite/createDevServerExclude.js +40 -0
  437. package/lib/vite/createDevServerExclude.test.mjs +79 -0
  438. package/manager/getContext.mjs +25 -3
  439. package/manager/processes/initialBuild.mjs +17 -1
  440. package/manager/processes/initialBuild.test.mjs +104 -0
  441. package/manager/processes/lowdefyBuild.mjs +31 -7
  442. package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +72 -0
  443. package/manager/processes/lowdefyBuild.test.mjs +136 -1
  444. package/manager/processes/optimizeDependencies.mjs +59 -0
  445. package/manager/processes/restartServer.mjs +34 -6
  446. package/manager/processes/restartServer.test.mjs +162 -0
  447. package/manager/processes/shutdownServer.mjs +11 -0
  448. package/manager/processes/shutdownServer.test.mjs +45 -0
  449. package/manager/processes/startFirstServer.mjs +46 -0
  450. package/manager/processes/startFirstServer.test.mjs +326 -0
  451. package/manager/processes/startMailSink.mjs +135 -0
  452. package/manager/processes/startMailSink.test.mjs +146 -0
  453. package/manager/processes/startProxy.mjs +129 -12
  454. package/manager/processes/startProxy.test.mjs +366 -0
  455. package/manager/processes/startServer.mjs +47 -6
  456. package/manager/processes/startServer.test.mjs +155 -0
  457. package/manager/processes/startWatchers.mjs +6 -4
  458. package/manager/processes/syncServer.mjs +69 -0
  459. package/manager/processes/syncServer.test.mjs +141 -0
  460. package/manager/processes/waitForDevServer.mjs +48 -0
  461. package/manager/processes/waitForDevServer.test.mjs +112 -0
  462. package/manager/run.mjs +128 -43
  463. package/manager/utils/BatchChanges.mjs +21 -5
  464. package/manager/utils/BatchChanges.test.mjs +77 -0
  465. package/manager/utils/acquireDevInstance.mjs +99 -0
  466. package/manager/utils/acquireDevInstance.test.mjs +160 -0
  467. package/manager/utils/createBuildActivity.mjs +67 -0
  468. package/manager/utils/createBuildActivity.test.mjs +58 -0
  469. package/manager/utils/createChangeSignal.mjs +39 -0
  470. package/manager/utils/createChangeSignal.test.mjs +62 -0
  471. package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
  472. package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
  473. package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
  474. package/manager/utils/createDotPathIgnore.mjs +44 -0
  475. package/manager/utils/createDotPathIgnore.test.mjs +65 -0
  476. package/manager/utils/createIdleGc.mjs +51 -0
  477. package/manager/utils/createIdleGc.test.mjs +77 -0
  478. package/manager/utils/createRequestActivity.mjs +100 -0
  479. package/manager/utils/createRequestActivity.test.mjs +121 -0
  480. package/manager/utils/createServerArtifactTracker.mjs +83 -0
  481. package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
  482. package/manager/utils/createServerEnv.mjs +39 -0
  483. package/manager/utils/filesHaveSameBytes.mjs +41 -0
  484. package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
  485. package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
  486. package/manager/utils/importFresh.mjs +46 -0
  487. package/manager/utils/importFresh.test.mjs +72 -0
  488. package/manager/utils/importFreshWorker.mjs +20 -0
  489. package/manager/utils/killTaggedBrowser.mjs +35 -0
  490. package/manager/utils/pagePathChanged.mjs +56 -0
  491. package/manager/utils/pruneRecordings.mjs +85 -0
  492. package/manager/utils/pruneRecordings.test.mjs +117 -0
  493. package/manager/utils/publishBuildDirectory.mjs +121 -0
  494. package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
  495. package/manager/utils/publishBuildDirectory.test.mjs +262 -0
  496. package/manager/utils/readBasePath.mjs +27 -0
  497. package/manager/utils/readBasePath.test.mjs +43 -0
  498. package/manager/utils/readBuildStatusWait.mjs +67 -0
  499. package/manager/utils/readManagerVersion.mjs +30 -0
  500. package/manager/utils/readManagerVersion.test.mjs +35 -0
  501. package/manager/utils/readPluginDefinitions.mjs +36 -0
  502. package/manager/utils/resolveDevAuthUrl.mjs +58 -0
  503. package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
  504. package/manager/utils/resolvePorts.mjs +49 -0
  505. package/manager/utils/setupWatcher.mjs +19 -11
  506. package/manager/utils/startRecordingPruner.mjs +45 -0
  507. package/manager/utils/startRecordingPruner.test.mjs +66 -0
  508. package/manager/utils/waitForChildExit.mjs +53 -0
  509. package/manager/utils/waitForChildExit.test.mjs +70 -0
  510. package/manager/utils/waitForServer.mjs +44 -0
  511. package/manager/utils/writeBuildStatus.mjs +1 -1
  512. package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
  513. package/manager/watchers/envWatcher.mjs +9 -2
  514. package/manager/watchers/envWatcher.test.mjs +87 -0
  515. package/manager/watchers/listServerSidePackages.mjs +47 -0
  516. package/manager/watchers/listServerSidePackages.test.mjs +38 -0
  517. package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
  518. package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
  519. package/manager/watchers/pluginSourceWatcher.mjs +79 -0
  520. package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
  521. package/manager/watchers/restartRequestWatcher.mjs +71 -0
  522. package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
  523. package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
  524. package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
  525. package/manager/watchers/serverPackageWatcher.mjs +38 -0
  526. package/package.json +60 -47
  527. package/package.original.json +60 -47
  528. package/src/app.js +131 -20
  529. package/src/html/renderDevPage.js +63 -11
  530. package/src/html/renderDevPage.test.mjs +138 -0
  531. package/src/lib/getRequestPath.js +29 -0
  532. package/src/middleware/createSameOriginGuard.js +37 -0
  533. package/src/middleware/createSameOriginGuard.test.mjs +102 -0
  534. package/src/middleware/errorHandler.js +51 -1
  535. package/src/middleware/errorHandler.test.mjs +173 -3
  536. package/src/middleware/isRebindingSafeHost.js +44 -0
  537. package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
  538. package/src/middleware/localDevToolsOnly.js +38 -0
  539. package/src/middleware/localDevToolsOnly.test.mjs +61 -0
  540. package/src/middleware/noStore.js +27 -0
  541. package/src/middleware/noStore.test.mjs +48 -0
  542. package/src/middleware/requestTimeout.js +38 -0
  543. package/src/middleware/staleFlag.js +76 -0
  544. package/src/middleware/staleFlag.test.mjs +127 -0
  545. package/src/middleware/stampCaller.js +38 -0
  546. package/src/middleware/stampCaller.test.mjs +64 -0
  547. package/src/routes/agent.js +15 -3
  548. package/src/routes/auth.js +29 -20
  549. package/src/routes/auth.test.mjs +30 -56
  550. package/src/routes/clientError.js +13 -13
  551. package/src/routes/cron.js +3 -1
  552. package/src/routes/cronForward.js +3 -1
  553. package/src/routes/detached.js +22 -9
  554. package/src/routes/devInspect.js +17 -8
  555. package/src/routes/devInspect.test.mjs +44 -7
  556. package/src/routes/devRecording.js +113 -0
  557. package/src/routes/devRecording.test.mjs +253 -0
  558. package/src/routes/docs/buildStatus.js +10 -2
  559. package/src/routes/docs/buildStatus.test.mjs +46 -0
  560. package/src/routes/docs/content.js +5 -3
  561. package/src/routes/docs/evalOperator.js +15 -1
  562. package/src/routes/docs/events.js +50 -0
  563. package/src/routes/docs/events.test.mjs +117 -0
  564. package/src/routes/docs/find.js +9 -1
  565. package/src/routes/docs/icons.js +30 -0
  566. package/src/routes/docs/inspectState.js +16 -1
  567. package/src/routes/docs/journey.js +194 -0
  568. package/src/routes/docs/journey.test.mjs +401 -0
  569. package/src/routes/docs/journeyRefusals.test.mjs +71 -0
  570. package/src/routes/docs/loadState.js +4 -2
  571. package/src/routes/docs/mcp.js +34 -3
  572. package/src/routes/docs/mcp.test.mjs +130 -0
  573. package/src/routes/docs/mutants.js +78 -0
  574. package/src/routes/docs/mutants.test.mjs +180 -0
  575. package/src/routes/docs/parsePathParamsParam.js +41 -0
  576. package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
  577. package/src/routes/docs/parseUserParam.js +27 -21
  578. package/src/routes/docs/parseUserParam.test.mjs +17 -5
  579. package/src/routes/docs/restart.js +35 -0
  580. package/src/routes/docs/restart.test.mjs +64 -0
  581. package/src/routes/docs/runEndpoint.js +54 -0
  582. package/src/routes/docs/runEndpoint.test.mjs +165 -0
  583. package/src/routes/docs/runRequest.js +25 -8
  584. package/src/routes/docs/runRequest.test.mjs +92 -0
  585. package/src/routes/docs/screenshot.js +50 -2
  586. package/src/routes/docs/screenshot.test.mjs +117 -0
  587. package/src/routes/docs/search.js +15 -2
  588. package/src/routes/docs/snapshotState.js +17 -3
  589. package/src/routes/endpoints.js +27 -26
  590. package/src/routes/endpointsTextAnswer.test.mjs +77 -0
  591. package/src/routes/feedback.js +10 -13
  592. package/src/routes/jitPage.js +99 -35
  593. package/src/routes/jitPage.test.mjs +248 -0
  594. package/src/routes/mcp.js +82 -5
  595. package/src/routes/mcp.test.mjs +215 -0
  596. package/src/routes/mcpMemberToken.test.mjs +125 -0
  597. package/src/routes/mcpProtectedResourceMetadata.js +34 -0
  598. package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
  599. package/src/routes/mountDevPageRoutes.js +25 -0
  600. package/src/routes/mountDevPageRoutes.test.mjs +30 -0
  601. package/src/routes/mountOauthDiscovery.js +44 -0
  602. package/src/routes/mountOauthDiscovery.test.mjs +79 -0
  603. package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
  604. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
  605. package/src/routes/reload.js +16 -0
  606. package/src/routes/request.js +6 -4
  607. package/src/routes/request.test.mjs +9 -5
  608. package/src/routes/requestBody.test.mjs +159 -0
  609. package/src/routes/usage.js +6 -2
  610. package/src/routes/user.js +27 -0
  611. package/src/routes/websocket.js +29 -8
  612. package/src/routes/websocket.test.mjs +65 -0
  613. package/src/routes/wellKnownFallback.js +30 -0
  614. package/src/routes/wellKnownFallback.test.mjs +32 -0
  615. package/src/websocket/devWebSocket.js +41 -26
  616. package/src/websocket/devWebSocket.test.mjs +157 -0
  617. package/vite.config.js +53 -23
  618. package/lib/server/auth/getDevSession.js +0 -70
  619. package/lib/server/auth/getDevSession.test.mjs +0 -138
  620. package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
  621. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
  622. package/manager/watchers/moduleBuildWatcher.mjs +0 -79
  623. package/manager/watchers/serverArtifactWatcher.mjs +0 -120
  624. package/src/routes/apiPage.js +0 -37
@@ -0,0 +1,1320 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, 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 { get, journeyTargetSelectors, type } from '@lowdefy/helpers';
18
+ import {
19
+ getBlock,
20
+ getRequestState,
21
+ getShortcutModifier,
22
+ getState,
23
+ } from '@lowdefy/e2e-utils/runtime';
24
+ import { getStepKey } from '@lowdefy/node-utils';
25
+
26
+ import checkAuthActionsRan from './rails/checkAuthActionsRan.js';
27
+ import checkClickRails from './rails/checkClickRails.js';
28
+ import checkStepReferences from './checkStepReferences.js';
29
+ import createLeftOriginError from './createLeftOriginError.js';
30
+ import { buildPageUrl } from './getBrowser.js';
31
+ import hasNoEffect from './observe/hasNoEffect.js';
32
+ import isPageReady from './isPageReady.js';
33
+ import JourneyStepError from './JourneyStepError.js';
34
+ import nextJourneySequence from './nextJourneySequence.js';
35
+ import openJourneyEmail from './openJourneyEmail.js';
36
+ import readJourneyEmailMatch from './readJourneyEmailMatch.js';
37
+ import readPagePath from './readPagePath.js';
38
+ import shownTextSelector from './shownTextSelector.js';
39
+
40
+ // Structural equality over values that have already been through the JSON
41
+ // round-trip getState performs in the page (no undefined, no Dates, no
42
+ // functions) — so key order is the only thing that must not matter.
43
+ function isDeepEqual(a, b) {
44
+ if (a === b) {
45
+ return true;
46
+ }
47
+ if (type.isArray(a) && type.isArray(b)) {
48
+ return a.length === b.length && a.every((item, index) => isDeepEqual(item, b[index]));
49
+ }
50
+ if (type.isObject(a) && type.isObject(b)) {
51
+ const keysA = Object.keys(a);
52
+ const keysB = Object.keys(b);
53
+ return keysA.length === keysB.length && keysA.every((key) => isDeepEqual(a[key], b[key]));
54
+ }
55
+ return false;
56
+ }
57
+
58
+ // Playwright colours its call log with ANSI escapes; the result is read by an
59
+ // agent as JSON, where they are noise.
60
+ function cleanMessage(error) {
61
+ // eslint-disable-next-line no-control-regex
62
+ return error.message.replace(/\u001b\[[0-9;]*m/g, '');
63
+ }
64
+
65
+ // Wraps a Playwright interaction so a locator that never became actionable
66
+ // (missing block, hidden, disabled, detached) reads as "expected the block
67
+ // to be actionable, actual: <Playwright's message>" instead of a bare error.
68
+ async function actOnTarget({ target, action }) {
69
+ try {
70
+ await action();
71
+ } catch (error) {
72
+ if (error instanceof JourneyStepError) {
73
+ throw error;
74
+ }
75
+ const actual = cleanMessage(error);
76
+ const description = describeTarget(target);
77
+ throw new JourneyStepError(`${capitalise(description)} was not actionable: ${actual}`, {
78
+ expected: `${description} to be actionable`,
79
+ actual,
80
+ });
81
+ }
82
+ }
83
+
84
+ function capitalise(text) {
85
+ return text.charAt(0).toUpperCase() + text.slice(1);
86
+ }
87
+
88
+ // The #bl-<id> wrapper spans the full row while the control inside it (an
89
+ // antd button, a link, a checkbox) is usually narrower, so a click at the
90
+ // wrapper's centre can land beside the control. The block's own e2e helpers
91
+ // target the inner control for the same reason; a block with no interactive
92
+ // descendant (a Box with its own onClick) is clicked directly.
93
+ //
94
+ // A radio or checkbox inside a <label> is reached through the label: the label
95
+ // carries the option's text and is what a person clicks, while the input may
96
+ // have no size at all (antd Segmented hides it at zero width and height), so
97
+ // Playwright would never find it visible. The selectors live in
98
+ // @lowdefy/helpers, where the engine's describe functions read them too.
99
+ const {
100
+ cellAttribute: CELL_ATTRIBUTE,
101
+ checkboxOption: CHECKBOX_OPTION,
102
+ dropdownOption: DROPDOWN_OPTION,
103
+ interactiveControl: INTERACTIVE_CONTROL,
104
+ layers: LAYERS,
105
+ radioOption: RADIO_OPTION,
106
+ rowAttribute: ROW_ATTRIBUTE,
107
+ } = journeyTargetSelectors;
108
+
109
+ // A lazy block (createLazyBlock in @lowdefy/block-utils) shows its wrapper and
110
+ // a fallback before its implementation, and counts itself in
111
+ // window.__lowdefyLazyLoads from mount until the implementation has rendered.
112
+ // A table that has loaded may still hold its loading skeleton for a minimum
113
+ // time (useSkeletonTiming in @lowdefy/blocks-antd), counted in
114
+ // window.__lowdefySkeletonHolds; a pointer press ends the hold, so a click
115
+ // started on the skeleton lands on rows that are no longer there. Waits for
116
+ // both counts to reach 0, as isPageReady does, so a block's controls are
117
+ // counted once they exist; with neither on screen it passes at once.
118
+ async function waitForLoadingBlocks({ page, timeout }) {
119
+ try {
120
+ await page.waitForFunction(
121
+ () => (window.__lowdefyLazyLoads ?? 0) === 0 && (window.__lowdefySkeletonHolds ?? 0) === 0,
122
+ undefined,
123
+ { timeout }
124
+ );
125
+ } catch (error) {
126
+ throw new JourneyStepError(`A block was still loading after ${timeout} ms.`, {
127
+ expected:
128
+ 'every lazy block on the page to have loaded and every loading skeleton to have cleared',
129
+ actual: cleanMessage(error),
130
+ });
131
+ }
132
+ }
133
+
134
+ // Waits for the block (or its control) to show, and for a lazy block's
135
+ // implementation, before counting, so a block that renders a moment after the
136
+ // step starts is clicked through its control, not at its wrapper's centre or
137
+ // on its fallback. A block that never shows fails the wait.
138
+ async function resolveClickTarget({ scope, timeout }) {
139
+ const control = scope.locator(INTERACTIVE_CONTROL).first();
140
+ await control.or(scope).first().waitFor({ state: 'visible', timeout });
141
+ await waitForLoadingBlocks({ page: scope.page(), timeout });
142
+ if ((await control.count()) > 0) {
143
+ return control;
144
+ }
145
+ return scope;
146
+ }
147
+
148
+ // Exact match on a control's or option's text: a regex anchored at both ends,
149
+ // so "Cat" does not pick "Category".
150
+ function exactText(value) {
151
+ return new RegExp(`^\\s*${value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`);
152
+ }
153
+
154
+ // A step's target is a blockId string or an object narrowing the search (see
155
+ // validateJourneySteps). The string form is the object form with only blockId.
156
+ function normaliseTarget(target) {
157
+ if (type.isString(target)) {
158
+ return { blockId: target };
159
+ }
160
+ return target;
161
+ }
162
+
163
+ // The block, row and column a target is narrowed to, as describeTarget reads them.
164
+ function describeScopeParts(target) {
165
+ const parts = [];
166
+ if (!type.isUndefined(target.blockId)) {
167
+ parts.push(`block "${target.blockId}"`);
168
+ }
169
+ if (!type.isUndefined(target.row)) {
170
+ parts.push(`row ${target.row}`);
171
+ }
172
+ if (!type.isUndefined(target.column)) {
173
+ parts.push(`column "${target.column}"`);
174
+ }
175
+ return parts;
176
+ }
177
+
178
+ // Reads back the way an author thinks of it — `block "grid" row 1 control
179
+ // "Edit"` — for the expected/actual pair of a failed step.
180
+ function describeTarget(target) {
181
+ const parts = describeScopeParts(target);
182
+ if (!type.isUndefined(target.text)) {
183
+ parts.push(`control "${target.text}"`);
184
+ }
185
+ if (!type.isUndefined(target.containing)) {
186
+ parts.push(`text containing "${target.containing}"`);
187
+ }
188
+ if (!type.isUndefined(target.nth)) {
189
+ parts.push(`nth ${target.nth}`);
190
+ }
191
+ return parts.join(' ');
192
+ }
193
+
194
+ function attributeValue(value) {
195
+ return `"${String(value).replace(/["\\]/g, '\\$&')}"`;
196
+ }
197
+
198
+ // The element the target's scope keys name: the block wrapper, narrowed to a
199
+ // grid row (`.ag-row[row-index]`, the row as displayed, which ag-grid renders
200
+ // once per pinned/centre column container) and to a cell (`.ag-cell[col-id]`,
201
+ // the column's field or colId). Undefined for a page-wide `text` target,
202
+ // which has no scope narrower than the page.
203
+ function resolveScope({ page, target }) {
204
+ if (type.isUndefined(target.blockId)) {
205
+ return undefined;
206
+ }
207
+ let scope = getBlock(page, target.blockId);
208
+ if (!type.isUndefined(target.row)) {
209
+ scope = scope.locator(`.ag-row[${ROW_ATTRIBUTE}=${attributeValue(target.row)}]`);
210
+ }
211
+ if (!type.isUndefined(target.column)) {
212
+ scope = scope.locator(`.ag-cell[${CELL_ATTRIBUTE}=${attributeValue(target.column)}]`);
213
+ }
214
+ return scope;
215
+ }
216
+
217
+ // The scope a `text` or `containing` target searches. A Modal or Drawer renders its content and
218
+ // footer in a portal outside its #bl- wrapper, marked with a class naming the block, so a target
219
+ // scoped to the block alone (no row or column) searches the wrapper and that portal's root.
220
+ function resolveMatchScope({ page, target }) {
221
+ const scope = resolveScope({ page, target });
222
+ if (
223
+ type.isUndefined(scope) ||
224
+ !type.isUndefined(target.row) ||
225
+ !type.isUndefined(target.column)
226
+ ) {
227
+ return scope;
228
+ }
229
+ return scope.or(page.locator(`.${journeyTargetSelectors.portalClass(target.blockId)}`));
230
+ }
231
+
232
+ function controlsWithText({ root, text }) {
233
+ return root
234
+ .locator(INTERACTIVE_CONTROL)
235
+ .filter({ hasText: exactText(text) })
236
+ .filter({ visible: true });
237
+ }
238
+
239
+ // LAYERS are the portal layers, front-most first. A control found by text alone is looked
240
+ // for in the front-most open layer before the page, the way a person reads a
241
+ // screen: an open dropdown menu covers a dialog, a dialog covers the page. A
242
+ // confirm dialog's "Delete" is then found over the grid's "Delete" cell
243
+ // buttons behind its mask, without the author counting buttons.
244
+ async function locatePageWideText({ page, target }) {
245
+ for (const layer of LAYERS) {
246
+ const open = page.locator(layer).filter({ visible: true });
247
+ if ((await open.count()) > 0) {
248
+ const inLayer = controlsWithText({ root: open.last(), text: target.text });
249
+ if ((await inLayer.count()) > 0) {
250
+ return { matches: inLayer, where: 'the front-most open layer' };
251
+ }
252
+ }
253
+ }
254
+ return { matches: controlsWithText({ root: page, text: target.text }), where: 'the page' };
255
+ }
256
+
257
+ function namesMatches(target) {
258
+ return !type.isUndefined(target.text) || !type.isUndefined(target.containing);
259
+ }
260
+
261
+ // Every visible element a `text` or `containing` target matches, before `nth`
262
+ // picks one, and where they were looked for. With `containing` they are the
263
+ // innermost elements whose shown text contains the string, however the markup
264
+ // splits it (registerShownTextEngine.js), inside the scope or on the page: a
265
+ // list row a person picks by the name or address it shows, which is neither a
266
+ // block nor an interactive control. With `text` they are the interactive
267
+ // controls with exactly that text (a cell button, a confirm dialog's OK, a
268
+ // menu item) inside the scope, or in the front-most open layer of the page
269
+ // when there is no blockId — portal-rendered controls live outside every
270
+ // block.
271
+ async function locateMatches({ page, target }) {
272
+ const scope = resolveMatchScope({ page, target });
273
+ const where = type.isUndefined(scope) ? 'the page' : describeScopeParts(target).join(' ');
274
+ if (!type.isUndefined(target.containing)) {
275
+ return {
276
+ matches: (scope ?? page)
277
+ .locator(shownTextSelector(target.containing))
278
+ .filter({ visible: true }),
279
+ where,
280
+ };
281
+ }
282
+ if (type.isUndefined(scope)) {
283
+ return locatePageWideText({ page, target });
284
+ }
285
+ return { matches: controlsWithText({ root: scope, text: target.text }), where };
286
+ }
287
+
288
+ // The element an expectation asserts about, or an action acts on when its
289
+ // target needs no single-match check (it gives `nth`, or names no `text` or
290
+ // `containing`). A `text` or `containing` target is its nth match, the first
291
+ // when no `nth` is given: an expectation holds for any match.
292
+ // With `nth` alone it is the nth interactive control in the scope. Otherwise
293
+ // it is the scope itself, and a click resolves its inner control the way a
294
+ // plain blockId click does.
295
+ async function resolveTarget({ page, target }) {
296
+ if (namesMatches(target)) {
297
+ const { matches } = await locateMatches({ page, target });
298
+ return matches.nth(target.nth ?? 0);
299
+ }
300
+ const scope = resolveScope({ page, target });
301
+ if (!type.isUndefined(target.nth)) {
302
+ return scope.locator(INTERACTIVE_CONTROL).nth(target.nth);
303
+ }
304
+ return scope;
305
+ }
306
+
307
+ function plural({ count, word }) {
308
+ return `${count} ${word}${count === 1 ? '' : 's'}`;
309
+ }
310
+
311
+ // The element an action step (click, open, fill, select) acts on. A `text` or
312
+ // `containing` target with no `nth` must name exactly one visible element:
313
+ // acting on the first of several is a guess, and a guess that happens to pass
314
+ // hides a journey that clicks the wrong row. Waits for a first match to show,
315
+ // then looks again (a dialog may have opened over the page in the meantime) and
316
+ // counts. When none shows, the wait's own error reports it.
317
+ //
318
+ // A target with `nth` also waits for a first match before it is resolved, so
319
+ // the front-most open layer is chosen (locatePageWideText) once the control is
320
+ // showing, not before a dialog the previous step opened has rendered.
321
+ async function resolveActionTarget({ page, target, timeout }) {
322
+ if (!namesMatches(target)) {
323
+ return resolveTarget({ page, target });
324
+ }
325
+ const first = (await locateMatches({ page, target })).matches.first();
326
+ await first.waitFor({ state: 'visible', timeout });
327
+ if (!type.isUndefined(target.nth)) {
328
+ return resolveTarget({ page, target });
329
+ }
330
+ const { matches, where } = await locateMatches({ page, target });
331
+ const count = await matches.count();
332
+ if (count > 1) {
333
+ const noun = type.isUndefined(target.text) ? 'element' : 'control';
334
+ const named = type.isUndefined(target.text)
335
+ ? `containing "${target.containing}"`
336
+ : `with text "${target.text}"`;
337
+ throw new JourneyStepError(
338
+ `Matched ${plural({ count, word: noun })} ${named} in ${where}; add nth: 0..${
339
+ count - 1
340
+ }, or a blockId/row to narrow it.`,
341
+ {
342
+ expected: `exactly one ${noun} ${named} in ${where}`,
343
+ actual: plural({ count, word: noun }),
344
+ }
345
+ );
346
+ }
347
+ return matches.first();
348
+ }
349
+
350
+ // A target that names a control (`text`, `nth`) or the words shown
351
+ // (`containing`) is clicked as is - a click on a list row's text reaches the
352
+ // row's own click handler, the way a person clicks the row; a target that
353
+ // names a container (block, row, cell) is clicked on the first control inside
354
+ // it, or on itself when it has none.
355
+ async function resolveClickLocator({ located, target, timeout }) {
356
+ if (namesMatches(target) || !type.isUndefined(target.nth)) {
357
+ return located;
358
+ }
359
+ return resolveClickTarget({ scope: located, timeout });
360
+ }
361
+
362
+ // The element a person clicks to open an input's popup: the trigger antd draws
363
+ // inside the block. Every popup input - Select, MultipleSelector, AutoComplete,
364
+ // TreeSelector, Cascader (all `.ant-select`, whose clickable box antd 6 renders
365
+ // as `.ant-select-content`), the date and time pickers (`.ant-picker`), a
366
+ // colour picker, a dropdown button - has one.
367
+ const POPUP_TRIGGER = [
368
+ '.ant-select-content',
369
+ '.ant-picker',
370
+ '.ant-color-picker-trigger',
371
+ '.ant-dropdown-trigger',
372
+ '.ant-mentions',
373
+ ].join(', ');
374
+
375
+ // The popups those triggers open. antd mounts them in a portal at the end of
376
+ // <body>, so they are looked for page-wide; the hidden class marks a popup
377
+ // that has closed but is still in the DOM.
378
+ const POPUP = [
379
+ '.ant-select-dropdown:not(.ant-select-dropdown-hidden)',
380
+ '.ant-picker-dropdown:not(.ant-picker-dropdown-hidden)',
381
+ '.ant-cascader-dropdown:not(.ant-select-dropdown-hidden)',
382
+ '.ant-color-picker-inner',
383
+ '.ant-dropdown:not(.ant-dropdown-hidden)',
384
+ '.ant-mentions-dropdown:not(.ant-mentions-dropdown-hidden)',
385
+ ];
386
+
387
+ // Marks the popups already showing before an `open` step clicks, so the step
388
+ // waits for a popup its own click opened: the dropdown a previous step opened
389
+ // stays visible while it fades out, and would otherwise pass for the new one.
390
+ const SHOWING_BEFORE = 'data-lowdefy-journey-showing-before';
391
+
392
+ const NEW_POPUP = POPUP.map((selector) => `${selector}:not([${SHOWING_BEFORE}])`).join(', ');
393
+
394
+ async function markShowingPopups({ page }) {
395
+ await page
396
+ .locator(POPUP.join(', '))
397
+ .evaluateAll(
398
+ (popups, attribute) =>
399
+ popups
400
+ .filter((popup) => popup.checkVisibility())
401
+ .forEach((popup) => popup.setAttribute(attribute, '')),
402
+ SHOWING_BEFORE
403
+ );
404
+ }
405
+
406
+ // The marks come off once the step is done, so a later `open` of the same
407
+ // input finds its popup again.
408
+ async function unmarkPopups({ page }) {
409
+ await page
410
+ .locator(`[${SHOWING_BEFORE}]`)
411
+ .evaluateAll(
412
+ (popups, attribute) => popups.forEach((popup) => popup.removeAttribute(attribute)),
413
+ SHOWING_BEFORE
414
+ );
415
+ }
416
+
417
+ // Opens the popup of the input the target names and waits until it shows, so
418
+ // a screenshot or a `select`-less inspection sees the options. A block with no
419
+ // popup trigger (a button) is clicked as `click` does; a block whose click
420
+ // opens nothing fails the step, since `open` promises a popup.
421
+ async function runOpen({ page, step, timeout }) {
422
+ const target = normaliseTarget(step.open);
423
+ await markShowingPopups({ page });
424
+ try {
425
+ await actOnTarget({
426
+ target,
427
+ action: async () => {
428
+ const located = await resolveActionTarget({ page, target, timeout });
429
+ const trigger = located.locator(POPUP_TRIGGER).first();
430
+ // The input renders its trigger with the block (a lazy block once its
431
+ // implementation has rendered); counted once either shows.
432
+ await trigger.or(located).first().waitFor({ state: 'visible', timeout });
433
+ await waitForLoadingBlocks({ page, timeout });
434
+ if ((await trigger.count()) > 0) {
435
+ await trigger.click({ timeout });
436
+ } else {
437
+ await (await resolveClickLocator({ located, target, timeout })).click({ timeout });
438
+ }
439
+ },
440
+ });
441
+ const popup = page.locator(NEW_POPUP).filter({ visible: true }).first();
442
+ try {
443
+ await popup.waitFor({ state: 'visible', timeout });
444
+ } catch (error) {
445
+ const description = describeTarget(target);
446
+ throw new JourneyStepError(`Opening ${description} showed no dropdown or popup.`, {
447
+ expected: `a popup to open from ${description}`,
448
+ actual: cleanMessage(error),
449
+ });
450
+ }
451
+ } finally {
452
+ await unmarkPopups({ page });
453
+ }
454
+ // Popups animate in; a capture mid-animation is faded or offset.
455
+ await page.waitForTimeout(250);
456
+ }
457
+
458
+ // `count` clicks in quick succession through one Playwright click, as a
459
+ // person's double click: the runner's settle comes after all of them, so a
460
+ // double click is never turned into a second submit after the first settled.
461
+ async function runClick({ journey, page, step, timeout }) {
462
+ const { count = 1, ...target } = normaliseTarget(step.click);
463
+ let blockId = null;
464
+ await actOnTarget({
465
+ target,
466
+ action: async () => {
467
+ const located = await resolveActionTarget({ page, target, timeout });
468
+ const locator = await resolveClickLocator({ located, target, timeout });
469
+ blockId = await checkClickRails({ journey, page, locator });
470
+ await locator.click({ timeout, clickCount: count });
471
+ },
472
+ });
473
+ return { blockId };
474
+ }
475
+
476
+ // Where a fill looks for the input. A block that renders its input in a portal (the Search
477
+ // modal) marks the portal root with the block's portal class, so a block-scoped fill also
478
+ // searches that portal. The block's own input comes first in document order, and a closed
479
+ // portal is not in the DOM, so a block with an input of its own is unaffected.
480
+ function fillScope({ page, target, located }) {
481
+ const isBlockScope =
482
+ !type.isUndefined(target.blockId) &&
483
+ type.isUndefined(target.row) &&
484
+ type.isUndefined(target.column) &&
485
+ !namesMatches(target) &&
486
+ type.isUndefined(target.nth);
487
+ if (!isBlockScope) return located;
488
+ return located.or(page.locator(`.${journeyTargetSelectors.portalClass(target.blockId)}`));
489
+ }
490
+
491
+ // A fill types `value`, or text read from an email (`fromEmail`) - the way a
492
+ // person types a one-time code from their inbox into the tab they started in.
493
+ async function runFill({ journey, page, step, timeout }) {
494
+ const { value: literal, fromEmail, ...target } = step.fill;
495
+ let value = literal;
496
+ if (!type.isUndefined(fromEmail)) {
497
+ value = await readJourneyEmailMatch({
498
+ page,
499
+ params: fromEmail,
500
+ since: journey.startedAt,
501
+ configDirectory: journey.configDirectory,
502
+ timeout,
503
+ });
504
+ }
505
+ await actOnTarget({
506
+ target,
507
+ action: async () => {
508
+ const located = await resolveActionTarget({ page, target, timeout });
509
+ const input = fillScope({ page, target, located }).locator('input, textarea').first();
510
+ const colorTrigger = located.locator(COLOR_PICKER_TRIGGER).first();
511
+ // A colour picker shows no input until its popup opens, so its trigger counts too.
512
+ try {
513
+ await input.or(colorTrigger).first().waitFor({ state: 'visible', timeout });
514
+ } catch (error) {
515
+ // A block that never showed fails as not actionable, as any other step on it does.
516
+ if (!(await located.first().isVisible())) throw error;
517
+ throw await createNoInputError({ page, target, error });
518
+ }
519
+ if ((await input.count()) === 0 && (await colorTrigger.count()) > 0) {
520
+ await fillColorPicker({ page, target, trigger: colorTrigger, value, timeout });
521
+ return;
522
+ }
523
+ await input.fill(String(value), { timeout });
524
+ },
525
+ });
526
+ }
527
+
528
+ // The engine's type for the block a fill names, or null when the shown page does not
529
+ // hold it or the tab is between pages.
530
+ async function readBlockType({ page, blockId }) {
531
+ try {
532
+ return await page.evaluate((id) => {
533
+ const lowdefy = window.lowdefy;
534
+ if (!lowdefy?.pageId || lowdefy._devNavigating === true) return null;
535
+ const shownKey = lowdefy.pageInstances?.[lowdefy.pageId]?.at(-1);
536
+ return lowdefy.contexts?.[shownKey]?._internal?.RootSlots?.map?.[id]?.type ?? null;
537
+ }, blockId);
538
+ } catch {
539
+ return null;
540
+ }
541
+ }
542
+
543
+ async function createNoInputError({ page, target, error }) {
544
+ const description = describeTarget(target);
545
+ const blockType = await readBlockType({ page, blockId: target.blockId });
546
+ const named = blockType === null ? description : `${description} (${blockType})`;
547
+ return new JourneyStepError(
548
+ `Fill found no input in ${named} to type into: fill types into a text input, textarea or colour picker.`,
549
+ {
550
+ expected: `a text input or colour picker in ${description}`,
551
+ actual: cleanMessage(error),
552
+ }
553
+ );
554
+ }
555
+
556
+ // The antd colour picker (the ColorSelector block): its trigger opens a popup whose hex
557
+ // input sets the colour once it holds a full 6 or 8 digit hex value.
558
+ const COLOR_PICKER_TRIGGER = '.ant-color-picker-trigger';
559
+ const COLOR_PICKER_TRIGGER_ACTIVE = 'ant-color-picker-trigger-active';
560
+ const COLOR_PICKER_TRIGGER_DISABLED = 'ant-color-picker-trigger-disabled';
561
+ const COLOR_PICKER_PANEL = '.ant-popover:not(.ant-popover-hidden) .ant-color-picker-inner';
562
+ const COLOR_PICKER_HEX_INPUT =
563
+ '.ant-color-picker-hex-input input, input.ant-color-picker-hex-input';
564
+ const HEX_COLOR = /^#?([\da-f]{6}|[\da-f]{8})$/i;
565
+
566
+ // The hex input shows its "#" as a prefix, so only the digits are typed. Escape closes the
567
+ // picker, leaving the page as a person who picked a colour leaves it. The close is waited
568
+ // for briefly and never fails the fill: a picker whose `open` the config holds stays open.
569
+ async function fillColorPicker({ page, target, trigger, value, timeout }) {
570
+ const description = describeTarget(target);
571
+ const hex = String(value);
572
+ if (!HEX_COLOR.test(hex)) {
573
+ throw new JourneyStepError(
574
+ `Fill of the colour picker in ${description} takes a 6 or 8 digit hex colour, such as #1677ff; got "${hex}".`,
575
+ { expected: 'a 6 or 8 digit hex colour', actual: hex }
576
+ );
577
+ }
578
+ const triggerClass = (await trigger.getAttribute('class', { timeout })) ?? '';
579
+ if (triggerClass.split(' ').includes(COLOR_PICKER_TRIGGER_DISABLED)) {
580
+ throw new JourneyStepError(`The colour picker in ${description} is disabled.`, {
581
+ expected: `an enabled colour picker in ${description}`,
582
+ actual: 'disabled',
583
+ });
584
+ }
585
+ // The picker's panel is not tied to its trigger in the DOM, so another picker's open
586
+ // panel is closed first: then the one panel showing is this picker's.
587
+ const panel = page.locator(COLOR_PICKER_PANEL).filter({ visible: true });
588
+ if (!triggerClass.split(' ').includes(COLOR_PICKER_TRIGGER_ACTIVE)) {
589
+ if ((await panel.count()) > 0) {
590
+ await page.keyboard.press('Escape');
591
+ await panel.first().waitFor({ state: 'hidden', timeout });
592
+ }
593
+ await trigger.click({ timeout });
594
+ }
595
+ const hexInput = panel.locator(COLOR_PICKER_HEX_INPUT).first();
596
+ try {
597
+ await hexInput.waitFor({ state: 'visible', timeout });
598
+ } catch (error) {
599
+ throw new JourneyStepError(
600
+ `The colour picker in ${description} showed no hex input: fill types a hex colour, and the picker shows another format.`,
601
+ {
602
+ expected: `a hex input in the colour picker of ${description}`,
603
+ actual: cleanMessage(error),
604
+ }
605
+ );
606
+ }
607
+ await hexInput.fill(hex.replace(/^#/, ''), { timeout });
608
+ await page.keyboard.press('Escape');
609
+ await panel
610
+ .first()
611
+ .waitFor({ state: 'hidden', timeout: Math.min(timeout, 1000) })
612
+ .catch(() => {});
613
+ }
614
+
615
+ // The options of a radio group, a button selector or a segmented control are
616
+ // all on the page already: the one labelled `text` is clicked, no dropdown.
617
+ async function selectRadioOption({ options, target, text, timeout }) {
618
+ const option = options
619
+ .filter({ hasText: exactText(text) })
620
+ .filter({ visible: true })
621
+ .first();
622
+ try {
623
+ await option.click({ timeout });
624
+ } catch (error) {
625
+ const description = describeTarget(target);
626
+ throw new JourneyStepError(`No option with text "${text}" in ${description}.`, {
627
+ expected: `option "${text}" in ${description}`,
628
+ actual: cleanMessage(error),
629
+ });
630
+ }
631
+ }
632
+
633
+ // A checkbox option is ticked, never toggled: clicking one already ticked would
634
+ // untick it, so a step that selects it reads the same on a rerun.
635
+ async function tickCheckboxOption({ options, target, text, timeout }) {
636
+ const option = options
637
+ .filter({ hasText: exactText(text) })
638
+ .filter({ visible: true })
639
+ .first();
640
+ try {
641
+ await option.waitFor({ state: 'visible', timeout });
642
+ } catch (error) {
643
+ const description = describeTarget(target);
644
+ throw new JourneyStepError(`No option with text "${text}" in ${description}.`, {
645
+ expected: `option "${text}" in ${description}`,
646
+ actual: cleanMessage(error),
647
+ });
648
+ }
649
+ if (await option.locator('input[type="checkbox"]').isChecked()) {
650
+ return;
651
+ }
652
+ await actOnTarget({ target, action: () => option.click({ timeout }) });
653
+ }
654
+
655
+ // A native <select> inside the block is preferred when present (Playwright's
656
+ // selectOption is exact and needs no open dropdown), then radio and checkbox
657
+ // options labelled in the block. Otherwise the block is clicked to open its dropdown
658
+ // and the option with exactly `value` as text is clicked — Ant Design renders
659
+ // options into a portal, so they are searched page-wide, restricted to visible
660
+ // ones so the hidden accessibility list is never matched.
661
+ async function runSelect({ page, step, timeout }) {
662
+ const { value, ...target } = step.select;
663
+ const text = String(value);
664
+ let scope;
665
+ await actOnTarget({
666
+ target,
667
+ action: async () => {
668
+ scope = await resolveActionTarget({ page, target, timeout });
669
+ // A native select or the radio and checkbox options render with the
670
+ // block; counted once it shows, so a late block does not fall through to
671
+ // the dropdown.
672
+ await scope.first().waitFor({ state: 'visible', timeout });
673
+ },
674
+ });
675
+ const native = scope.locator('select');
676
+ if ((await native.count()) > 0) {
677
+ await actOnTarget({
678
+ target,
679
+ action: () => native.first().selectOption({ label: text }, { timeout }),
680
+ });
681
+ return;
682
+ }
683
+ const radioOptions = scope.locator(RADIO_OPTION);
684
+ if ((await radioOptions.count()) > 0) {
685
+ await selectRadioOption({ options: radioOptions, target, text, timeout });
686
+ return;
687
+ }
688
+ const checkboxOptions = scope.locator(CHECKBOX_OPTION);
689
+ if ((await checkboxOptions.count()) > 0) {
690
+ await tickCheckboxOption({ options: checkboxOptions, target, text, timeout });
691
+ return;
692
+ }
693
+ await actOnTarget({
694
+ target,
695
+ action: async () => {
696
+ const locator = await resolveClickLocator({ located: scope, target, timeout });
697
+ await locator.click({ timeout });
698
+ },
699
+ });
700
+ const option = page
701
+ .locator(DROPDOWN_OPTION)
702
+ .filter({ hasText: exactText(text) })
703
+ .filter({ visible: true })
704
+ .first();
705
+ try {
706
+ await option.click({ timeout });
707
+ } catch (error) {
708
+ const description = describeTarget(target);
709
+ throw new JourneyStepError(
710
+ `No option with text "${text}" appeared in the dropdown of ${description}.`,
711
+ {
712
+ expected: `option "${text}" in the dropdown of ${description}`,
713
+ actual: cleanMessage(error),
714
+ }
715
+ );
716
+ }
717
+ // The dropdown fades out for a few hundred ms after a pick; a screenshot or
718
+ // click taken during the fade would still see it covering the rows below.
719
+ // Tolerant and short: a multi-select dropdown stays open by design.
720
+ await page
721
+ .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)')
722
+ .first()
723
+ .waitFor({ state: 'hidden', timeout: Math.min(timeout, 1000) })
724
+ .catch(() => {});
725
+ }
726
+
727
+ // `Mod` in a chord resolves to Meta or Control from the platform the page
728
+ // reports, the way the app's own shortcut handling (tinykeys) does.
729
+ async function runPress({ page, step }) {
730
+ const modifier = await getShortcutModifier(page);
731
+ const key = step.press
732
+ .split('+')
733
+ .map((part) => (part === 'Mod' ? modifier : part))
734
+ .join('+');
735
+ try {
736
+ await page.keyboard.press(key);
737
+ } catch (error) {
738
+ throw new JourneyStepError(`Could not press "${key}": ${cleanMessage(error)}`, {
739
+ expected: `key "${key}" to be pressed`,
740
+ actual: cleanMessage(error),
741
+ });
742
+ }
743
+ }
744
+
745
+ // Goes back one entry in the browser history, the way the Back button does.
746
+ // The headless tab opens on about:blank before the journey's page, which the
747
+ // Navigation API leaves out of the app's entries, so an entry index of 0 means
748
+ // the journey has not navigated anywhere it could go back from.
749
+ async function runBack({ page, timeout }) {
750
+ const index = await page.evaluate(() => window.navigation.currentEntry.index);
751
+ if (index === 0) {
752
+ throw new JourneyStepError(
753
+ 'There is no earlier page in this journey to go back to: back needs a page the journey navigated from.',
754
+ { expected: 'the browser to go back one page', actual: `no earlier page than ${page.url()}` }
755
+ );
756
+ }
757
+ try {
758
+ await page.goBack({ timeout });
759
+ } catch (error) {
760
+ throw new JourneyStepError(`Could not go back: ${cleanMessage(error)}`, {
761
+ expected: 'the browser to go back one page',
762
+ actual: cleanMessage(error),
763
+ });
764
+ }
765
+ }
766
+
767
+ // Loads an app page the way a typed URL does. The page shown may not be the
768
+ // one asked for - a protected page redirects a signed-out actor to sign in -
769
+ // so the runner settles whichever page the app mounts (isPageReady reads the
770
+ // instance on screen, as in openPage) and lets the next step assert where it
771
+ // landed.
772
+ async function runGoto({ page, step, origin, timeout }) {
773
+ const { pageId, pathParams, urlQuery } = type.isString(step.goto)
774
+ ? { pageId: step.goto }
775
+ : step.goto;
776
+ let url;
777
+ try {
778
+ url = buildPageUrl({
779
+ origin,
780
+ pageId,
781
+ path: readPagePath({ pageId }),
782
+ pathParams,
783
+ urlQuery,
784
+ });
785
+ } catch (error) {
786
+ throw new JourneyStepError(`Could not open page "${pageId}": ${error.message}`, {
787
+ expected: `page "${pageId}" to load`,
788
+ actual: error.message,
789
+ });
790
+ }
791
+ try {
792
+ await page.goto(url, { waitUntil: 'load', timeout });
793
+ } catch (error) {
794
+ throw new JourneyStepError(`Could not open page "${pageId}": ${cleanMessage(error)}`, {
795
+ expected: `page "${pageId}" to load`,
796
+ actual: cleanMessage(error),
797
+ });
798
+ }
799
+ await page.waitForFunction(isPageReady, undefined, { timeout }).catch(() => {});
800
+ }
801
+
802
+ // Polls a page read until it satisfies `check`, or fails once `timeout` has
803
+ // elapsed. Playwright's own waitForFunction cannot be used here because the
804
+ // reads go through the e2e-utils helpers, which run in Node.
805
+ async function pollUntil({ page, read, check, timeout, expected }) {
806
+ const deadline = Date.now() + timeout;
807
+ let value = await read();
808
+ while (!check(value)) {
809
+ if (Date.now() >= deadline) {
810
+ throw new JourneyStepError(`Timed out after ${timeout}ms waiting for ${expected}.`, {
811
+ expected,
812
+ actual: value,
813
+ });
814
+ }
815
+ await page.waitForTimeout(50);
816
+ value = await read();
817
+ }
818
+ }
819
+
820
+ // A request the page made before the step's window (the call on page load,
821
+ // when a click was meant to call it again) does not satisfy wait.request: the
822
+ // actor's network counter must show a call started inside the window, and the
823
+ // page must have that request no longer loading.
824
+ async function readRequestWait({ journey, page, requestId, since }) {
825
+ const state = await getRequestState(page, requestId);
826
+ const pageId = await page.evaluate(() => window.lowdefy?.pageId);
827
+ return {
828
+ calledSinceLastInteraction: journey.actors.calledSince({ request: requestId, pageId, since }),
829
+ state: state ?? null,
830
+ };
831
+ }
832
+
833
+ async function runWait({ journey, page, step, timeout, requestWindowStart }) {
834
+ const wait = step.wait;
835
+ switch (getStepKey(wait)) {
836
+ case 'ms':
837
+ await page.waitForTimeout(wait.ms);
838
+ return;
839
+ case 'request':
840
+ await pollUntil({
841
+ page,
842
+ read: () =>
843
+ readRequestWait({ journey, page, requestId: wait.request, since: requestWindowStart }),
844
+ check: ({ calledSinceLastInteraction, state }) =>
845
+ calledSinceLastInteraction && !type.isNone(state) && state.loading !== true,
846
+ timeout,
847
+ expected: `request "${wait.request}" to have been called since the last interaction and finished loading`,
848
+ });
849
+ return;
850
+ case 'state':
851
+ await pollUntil({
852
+ page,
853
+ read: async () => get((await getState(page)) ?? {}, wait.state),
854
+ check: (value) => !type.isUndefined(value),
855
+ timeout,
856
+ expected: `state "${wait.state}" to be defined`,
857
+ });
858
+ return;
859
+ default:
860
+ return;
861
+ }
862
+ }
863
+
864
+ async function runScreenshot({ page, step, index, screenshots }) {
865
+ const name = type.isString(step.screenshot) ? step.screenshot : `step-${index}`;
866
+ const buffer = await page.screenshot({ type: 'png' });
867
+ screenshots.push({ name, data: buffer.toString('base64'), mimeType: 'image/png' });
868
+ }
869
+
870
+ // A path that does not exist reads as null: a journey is JSON, where null is
871
+ // the only way to say "absent", and the failure report already shows a
872
+ // missing value as null - so `equals: null` asserts the value is not there.
873
+ // A read while the page navigates fails and is retried.
874
+ async function readStateValue({ page, path }) {
875
+ try {
876
+ return { value: get((await getState(page)) ?? {}, path) ?? null };
877
+ } catch (error) {
878
+ return { error };
879
+ }
880
+ }
881
+
882
+ // Polled like every other expectation: the value a click leads to often
883
+ // lands once the request or endpoint it called has answered, which can be
884
+ // after the page has settled on a busy machine.
885
+ async function expectState({ page, params, timeout }) {
886
+ const { path, equals } = params;
887
+ const deadline = Date.now() + timeout;
888
+ let read = await readStateValue({ page, path });
889
+ while (
890
+ (!type.isUndefined(read.error) || !isDeepEqual(read.value, equals)) &&
891
+ Date.now() < deadline
892
+ ) {
893
+ await page.waitForTimeout(50);
894
+ read = await readStateValue({ page, path });
895
+ }
896
+ if (!type.isUndefined(read.error)) {
897
+ throw read.error;
898
+ }
899
+ if (!isDeepEqual(read.value, equals)) {
900
+ throw new JourneyStepError(
901
+ `Expected state "${path}" to equal ${JSON.stringify(equals)} but found ${JSON.stringify(
902
+ read.value
903
+ )}.`,
904
+ { expected: equals, actual: read.value }
905
+ );
906
+ }
907
+ }
908
+
909
+ async function expectVisible({ page, params, timeout }) {
910
+ const target = normaliseTarget(params);
911
+ const description = describeTarget(target);
912
+ try {
913
+ const located = await resolveTarget({ page, target });
914
+ await located.waitFor({ state: 'visible', timeout });
915
+ } catch (error) {
916
+ throw new JourneyStepError(`Expected ${description} to be visible.`, {
917
+ expected: `${description} to be visible`,
918
+ actual: cleanMessage(error),
919
+ });
920
+ }
921
+ }
922
+
923
+ // Passes once no element the target names is visible: nothing matches, or
924
+ // every match is hidden. Resolves at once when nothing matches yet, so a
925
+ // journey pairs it with something that must be present first.
926
+ async function expectHidden({ page, params, timeout }) {
927
+ const target = normaliseTarget(params);
928
+ const description = describeTarget(target);
929
+ try {
930
+ const located = await resolveTarget({ page, target });
931
+ await located.filter({ visible: true }).first().waitFor({ state: 'hidden', timeout });
932
+ } catch (error) {
933
+ throw new JourneyStepError(`Expected ${description} to be hidden.`, {
934
+ expected: `${description} to be hidden`,
935
+ actual: cleanMessage(error),
936
+ });
937
+ }
938
+ }
939
+
940
+ // Counts the calls the current actor's browser made to one request or
941
+ // endpoint since the journey started, from the network counter, which keeps
942
+ // its counts across full page loads. Compared once, after the page settles,
943
+ // without polling: "not called" can only be judged once the moment has passed.
944
+ // A request with no pageId is counted on the page the actor is on; when no
945
+ // Lowdefy page is showing (it crashed or left the app) the step fails, since a
946
+ // count of 0 against no page would pass without proving anything.
947
+ async function expectCalls({ journey, page, params }) {
948
+ await settlePage({ page, timeout: Math.min(journey.stepTimeout, SETTLE_TIMEOUT_MS) });
949
+ let query;
950
+ let description;
951
+ if (type.isUndefined(params.endpoint)) {
952
+ const pageId = params.pageId ?? (await page.evaluate(() => window.lowdefy?.pageId));
953
+ if (type.isNone(pageId)) {
954
+ throw new JourneyStepError(
955
+ `Expected request "${params.request}" to be counted on the current page, but no Lowdefy page is showing.`,
956
+ { expected: `a Lowdefy page to count request "${params.request}" on`, actual: null }
957
+ );
958
+ }
959
+ query = { request: params.request, pageId };
960
+ description = `request "${params.request}" on page "${pageId}"`;
961
+ } else {
962
+ query = { endpoint: params.endpoint };
963
+ description = `endpoint "${params.endpoint}"`;
964
+ }
965
+ const actual = journey.actors.countCalls(query);
966
+ if (actual !== params.count) {
967
+ throw new JourneyStepError(
968
+ `Expected ${description} to have been called ${plural({
969
+ count: params.count,
970
+ word: 'time',
971
+ })} but it was called ${plural({ count: actual, word: 'time' })}.`,
972
+ { expected: params.count, actual }
973
+ );
974
+ }
975
+ }
976
+
977
+ // Waits for the target to be in the page, then reads the text of every element
978
+ // it matches. A grid row is rendered once per column container (pinned left,
979
+ // centre, pinned right), so a row target legitimately matches more than one
980
+ // element; joining them reads the whole row.
981
+ async function readTargetText({ page, target, timeout }) {
982
+ // A block's text includes its Modal or Drawer portal's (see resolveMatchScope).
983
+ const located =
984
+ !namesMatches(target) && type.isUndefined(target.nth) && !type.isUndefined(target.blockId)
985
+ ? resolveMatchScope({ page, target })
986
+ : await resolveTarget({ page, target });
987
+ await located.first().waitFor({ state: 'attached', timeout });
988
+ const texts = await located.allInnerTexts();
989
+ return texts.join('\n');
990
+ }
991
+
992
+ // One read of the target's text: { text } or, when the target is not in the
993
+ // page yet or the page navigated mid-read, { error }.
994
+ async function tryReadTargetText({ page, target, timeout }) {
995
+ try {
996
+ return { text: await readTargetText({ page, target, timeout }) };
997
+ } catch (error) {
998
+ return { error };
999
+ }
1000
+ }
1001
+
1002
+ // Polled rather than read once, like the url and the title: a block often
1003
+ // renders its text only once the request it shows has answered (a list hidden
1004
+ // until its data arrives reads as ""), and a sign-in or sign-out may still be
1005
+ // reloading the page when the step starts.
1006
+ async function expectText({ page, params, timeout }) {
1007
+ const { contains, ...target } = params;
1008
+ const description = describeTarget(target);
1009
+ const expected = `${description} text to contain "${contains}"`;
1010
+ const deadline = Date.now() + timeout;
1011
+ let read = await tryReadTargetText({ page, target, timeout });
1012
+ // The last text actually read. The final poll runs with whatever is left of
1013
+ // the timeout (1ms at the end), which can fail on its own latency: that
1014
+ // failure must not hide the text the earlier reads found.
1015
+ let lastText = read.text;
1016
+ while (read.text?.includes(contains) !== true && Date.now() < deadline) {
1017
+ await page.waitForTimeout(50);
1018
+ read = await tryReadTargetText({
1019
+ page,
1020
+ target,
1021
+ timeout: Math.max(deadline - Date.now(), 1),
1022
+ });
1023
+ lastText = read.text ?? lastText;
1024
+ }
1025
+ if (!type.isUndefined(read.error) && type.isUndefined(lastText)) {
1026
+ throw new JourneyStepError(`Expected ${description} to contain text "${contains}".`, {
1027
+ expected,
1028
+ actual: cleanMessage(read.error),
1029
+ });
1030
+ }
1031
+ if (!lastText.includes(contains)) {
1032
+ // innerText is the text as rendered, so CSS text-transform applies to it.
1033
+ const hint = lastText.toLowerCase().includes(String(contains).toLowerCase())
1034
+ ? ' It matches ignoring case: the block may render its text transformed (text-transform).'
1035
+ : '';
1036
+ throw new JourneyStepError(
1037
+ `Expected ${description} text to contain "${contains}" but found ${JSON.stringify(
1038
+ lastText
1039
+ )}.${hint}`,
1040
+ { expected, actual: lastText }
1041
+ );
1042
+ }
1043
+ }
1044
+
1045
+ // The document title is set by the page after it renders, so it is polled
1046
+ // rather than read once.
1047
+ async function expectTitle({ page, params, timeout }) {
1048
+ if (!type.isUndefined(params.equals)) {
1049
+ await pollUntil({
1050
+ page,
1051
+ read: () => page.title(),
1052
+ check: (title) => title === params.equals,
1053
+ timeout,
1054
+ expected: `title to equal "${params.equals}"`,
1055
+ });
1056
+ return;
1057
+ }
1058
+ await pollUntil({
1059
+ page,
1060
+ read: () => page.title(),
1061
+ check: (title) => title.includes(params.contains),
1062
+ timeout,
1063
+ expected: `title to contain "${params.contains}"`,
1064
+ });
1065
+ }
1066
+
1067
+ // Waits for the URL rather than reading it once, because a click that
1068
+ // navigates resolves before the new route is committed — page.waitForURL is
1069
+ // Playwright's own wait for exactly this.
1070
+ async function expectUrl({ page, params, timeout }) {
1071
+ const { contains } = params;
1072
+ try {
1073
+ await page.waitForURL((url) => url.href.includes(contains), { timeout });
1074
+ } catch {
1075
+ throw new JourneyStepError(
1076
+ `Expected url to contain "${contains}" but found ${JSON.stringify(page.url())}.`,
1077
+ { expected: `url to contain "${contains}"`, actual: page.url() }
1078
+ );
1079
+ }
1080
+ }
1081
+
1082
+ // Fails when the interaction just before it did nothing, the dead-click
1083
+ // invariant inverted (see observe/hasNoEffect). It reads that
1084
+ // interaction's window, which the runner closed once the page settled, so it
1085
+ // does not wait. Only a run that watches its steps' windows (a journey run,
1086
+ // not a screenshot's steps) has one to read.
1087
+ function expectEffect({ journey }) {
1088
+ const window = journey.appErrors?.previousWindow();
1089
+ if (type.isNone(window)) {
1090
+ throw new JourneyStepError(
1091
+ 'expect.effect can only be checked in a journey run, which watches what each step does.',
1092
+ { expected: 'the previous step to have an effect', actual: null }
1093
+ );
1094
+ }
1095
+ if (hasNoEffect({ window })) {
1096
+ throw new JourneyStepError(
1097
+ 'Expected the previous step to have an effect, but it ran no event, changed nothing on the page, sent no app request and left the URL unchanged.',
1098
+ {
1099
+ expected: 'the previous step to have an effect',
1100
+ actual: 'no event, no DOM change, no app request, URL unchanged',
1101
+ }
1102
+ );
1103
+ }
1104
+ }
1105
+
1106
+ async function runExpect({ journey, page, step, timeout }) {
1107
+ const expectation = step.expect;
1108
+ const key = getStepKey(expectation);
1109
+ const params = expectation[key];
1110
+ switch (key) {
1111
+ case 'state':
1112
+ await expectState({ page, params, timeout });
1113
+ return;
1114
+ case 'visible':
1115
+ await expectVisible({ page, params, timeout });
1116
+ return;
1117
+ case 'hidden':
1118
+ await expectHidden({ page, params, timeout });
1119
+ return;
1120
+ case 'calls':
1121
+ await expectCalls({ journey, page, params });
1122
+ return;
1123
+ case 'text':
1124
+ await expectText({ page, params, timeout });
1125
+ return;
1126
+ case 'url':
1127
+ await expectUrl({ page, params, timeout });
1128
+ return;
1129
+ case 'title':
1130
+ await expectTitle({ page, params, timeout });
1131
+ return;
1132
+ case 'error':
1133
+ // Claimed when the step's window closes (see observe/claimExpectedErrors).
1134
+ return;
1135
+ case 'effect':
1136
+ expectEffect({ journey });
1137
+ return;
1138
+ default:
1139
+ return;
1140
+ }
1141
+ }
1142
+
1143
+ // After an interaction, waits for the page's own load chain — the event the
1144
+ // interaction fired, the requests it called — to settle, using the same
1145
+ // readiness check openPage uses, so the next step asserts against the
1146
+ // outcome rather than racing it. Tolerant: a page that never settles (a
1147
+ // hung request) simply moves on and lets the next expect report what it
1148
+ // finds. Waits for the instance on screen, since a click may have navigated
1149
+ // to another page or instance.
1150
+ //
1151
+ // Never longer than SETTLE_TIMEOUT_MS, however long the steps may wait: an
1152
+ // event that ends in a Wait (a sign-in link's resend cooldown) keeps the page
1153
+ // unsettled for as long as it waits, and every step after it waits for what it
1154
+ // needs on its own.
1155
+ async function settlePage({ page, timeout }) {
1156
+ const pageId = await page.evaluate(() => window.lowdefy?.pageId);
1157
+ if (type.isNone(pageId)) {
1158
+ return;
1159
+ }
1160
+ await page.waitForFunction(isPageReady, undefined, { timeout }).catch(() => {});
1161
+ }
1162
+
1163
+ const INTERACTION_STEPS = ['click', 'open', 'fill', 'select', 'press', 'back'];
1164
+
1165
+ // The steps that open a new window for wait.request: an interaction, or a step
1166
+ // that loads or switches to a page. Before the first of them the window is the
1167
+ // page open.
1168
+ const REQUEST_WINDOW_STEPS = [...INTERACTION_STEPS, 'goto', 'as'];
1169
+
1170
+ const SETTLE_TIMEOUT_MS = 5000;
1171
+
1172
+ async function runStep({ journey, step, index, screenshots, requestWindowStart }) {
1173
+ const page = journey.actors.current().page;
1174
+ const timeout = journey.stepTimeout;
1175
+ await checkStepReferences({ journey, page, step });
1176
+ switch (getStepKey(step)) {
1177
+ case 'click':
1178
+ return runClick({ journey, page, step, timeout });
1179
+ case 'open':
1180
+ await runOpen({ page, step, timeout });
1181
+ return;
1182
+ case 'fill':
1183
+ await runFill({ journey, page, step, timeout });
1184
+ return;
1185
+ case 'select':
1186
+ await runSelect({ page, step, timeout });
1187
+ return;
1188
+ case 'press':
1189
+ await runPress({ page, step, timeout });
1190
+ return;
1191
+ case 'back':
1192
+ await runBack({ page, timeout });
1193
+ return;
1194
+ case 'goto':
1195
+ await runGoto({ page, step, origin: journey.origin, timeout: journey.openTimeout });
1196
+ return;
1197
+ case 'email':
1198
+ await openJourneyEmail({
1199
+ page,
1200
+ params: step.email,
1201
+ since: journey.startedAt,
1202
+ configDirectory: journey.configDirectory,
1203
+ timeout,
1204
+ });
1205
+ return;
1206
+ case 'as':
1207
+ await journey.actors.switchTo(step.as);
1208
+ return;
1209
+ case 'wait':
1210
+ await runWait({ journey, page, step, timeout, requestWindowStart });
1211
+ return;
1212
+ case 'screenshot':
1213
+ await runScreenshot({ page, step, index, screenshots });
1214
+ return;
1215
+ case 'expect':
1216
+ await runExpect({ journey, page, step, timeout });
1217
+ return;
1218
+ default:
1219
+ return;
1220
+ }
1221
+ }
1222
+
1223
+ function toFailure({ error, index, step }) {
1224
+ if (error instanceof JourneyStepError) {
1225
+ // `actual` is null rather than undefined so the key survives JSON — an
1226
+ // agent reading the failure sees "found nothing", not a missing field.
1227
+ return {
1228
+ index,
1229
+ step,
1230
+ expected: error.expected,
1231
+ actual: type.isUndefined(error.actual) ? null : error.actual,
1232
+ message: error.message,
1233
+ };
1234
+ }
1235
+ return {
1236
+ index,
1237
+ step,
1238
+ expected: `step ${index} (${getStepKey(step)}) to complete`,
1239
+ actual: cleanMessage(error),
1240
+ message: cleanMessage(error),
1241
+ };
1242
+ }
1243
+
1244
+ // Runs the steps in order, stopping at the first failure. Returns the step
1245
+ // log, the failure (if any) and the screenshots taken — never throws for a
1246
+ // step that fails, because a failed journey is a result an agent reads, not
1247
+ // an error it recovers from. A journey run that watches for app errors
1248
+ // (journey.appErrors, see createJourneyAppErrors) also fails at the step
1249
+ // whose window held one, or on open when opening the page caused one.
1250
+ async function runJourneySteps({ journey, steps }) {
1251
+ const results = [];
1252
+ const screenshots = [];
1253
+ const { appErrors } = journey;
1254
+ let failure;
1255
+ // In nextJourneySequence's order; 0 takes in every call since the page opened.
1256
+ let requestWindowStart = 0;
1257
+ if (!type.isUndefined(appErrors)) {
1258
+ failure = await appErrors.judgeOpen({ page: journey.actors.current().page });
1259
+ }
1260
+ for (let index = 0; index < steps.length; index += 1) {
1261
+ const step = steps[index];
1262
+ if (!type.isUndefined(failure)) {
1263
+ results.push({ index, step, status: 'skipped', durationMs: 0 });
1264
+ continue;
1265
+ }
1266
+ const started = Date.now();
1267
+ if (REQUEST_WINDOW_STEPS.includes(getStepKey(step))) {
1268
+ requestWindowStart = nextJourneySequence();
1269
+ }
1270
+ await appErrors?.openWindow({ page: journey.actors.current().page });
1271
+ try {
1272
+ const ran = await runStep({ journey, step, index, screenshots, requestWindowStart });
1273
+ if (INTERACTION_STEPS.includes(getStepKey(step))) {
1274
+ await settlePage({
1275
+ page: journey.actors.current().page,
1276
+ timeout: Math.min(journey.stepTimeout, SETTLE_TIMEOUT_MS),
1277
+ });
1278
+ }
1279
+ if (getStepKey(step) === 'click') {
1280
+ await checkAuthActionsRan({
1281
+ journey,
1282
+ page: journey.actors.current().page,
1283
+ blockId: ran?.blockId,
1284
+ });
1285
+ }
1286
+ await journey.actors.sampleRendered();
1287
+ results.push({ index, step, status: 'ok', durationMs: Date.now() - started });
1288
+ } catch (error) {
1289
+ failure = toFailure({ error, index, step });
1290
+ results.push({ index, step, status: 'failed', durationMs: Date.now() - started });
1291
+ }
1292
+ // A request this step caused to another host of the dev server was aborted, so whatever the
1293
+ // step saw afterwards is not what the app does: that request is the failure.
1294
+ const departure = journey.actors.leftOrigin();
1295
+ if (!type.isUndefined(departure)) {
1296
+ failure = toFailure({
1297
+ error: createLeftOriginError({ origin: journey.origin, departure }),
1298
+ index,
1299
+ step,
1300
+ });
1301
+ results[results.length - 1].status = 'failed';
1302
+ }
1303
+ if (!type.isUndefined(appErrors)) {
1304
+ failure = await appErrors.judgeStep({
1305
+ journey,
1306
+ steps,
1307
+ index,
1308
+ results,
1309
+ failure,
1310
+ leftOrigin: !type.isUndefined(departure),
1311
+ });
1312
+ }
1313
+ }
1314
+ if (!type.isUndefined(appErrors) && type.isUndefined(failure) && steps.length > 0) {
1315
+ failure = await appErrors.judgeDrain({ journey, steps, results });
1316
+ }
1317
+ return { results, screenshots, failure };
1318
+ }
1319
+
1320
+ export default runJourneySteps;