@lowdefy/server-dev 6.1.0 → 7.0.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 (623) 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 +67 -0
  99. package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
  100. package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +679 -0
  101. package/lib/docs/dataSets/describeDataSetResult.js +25 -0
  102. package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
  103. package/lib/docs/dataSets/deserializeFixture.js +34 -0
  104. package/lib/docs/dataSets/drainSessionWork.js +37 -0
  105. package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
  106. package/lib/docs/dataSets/duplicateIndexName.js +24 -0
  107. package/lib/docs/dataSets/findCollectionCollisions.js +52 -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 +44 -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 +119 -0
  119. package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +343 -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 +68 -0
  126. package/lib/docs/dataSets/readDataSet.test.mjs +220 -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/sweepOrphanDatabases.js +33 -0
  133. package/lib/docs/dataSets/test/app/lowdefy.yaml +203 -0
  134. package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
  135. package/lib/docs/dataSets/test/app/tests/data/shop.yaml +8 -0
  136. package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
  137. package/lib/docs/devEventBus.js +136 -0
  138. package/lib/docs/devEventBus.test.mjs +269 -0
  139. package/lib/docs/devToolDefinitions.js +529 -0
  140. package/lib/docs/docSources.js +21 -0
  141. package/lib/docs/docs.test.mjs +171 -15
  142. package/lib/docs/docsIndex.test.mjs +377 -0
  143. package/lib/docs/evalOperator.js +5 -3
  144. package/lib/docs/evalOperatorHeadless.js +84 -12
  145. package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
  146. package/lib/docs/evalOperatorInTab.js +2 -1
  147. package/lib/docs/findConfig.js +95 -49
  148. package/lib/docs/fitResponse.js +81 -0
  149. package/lib/docs/fitResponse.test.mjs +128 -0
  150. package/lib/docs/formatFeedback.js +26 -5
  151. package/lib/docs/formatFeedback.test.mjs +48 -0
  152. package/lib/docs/getAppMap.js +7 -1
  153. package/lib/docs/getBrowser.js +150 -80
  154. package/lib/docs/getBrowser.test.mjs +407 -16
  155. package/lib/docs/getBuildId.js +61 -0
  156. package/lib/docs/getBuildId.test.mjs +84 -0
  157. package/lib/docs/getBuildStatus.js +54 -6
  158. package/lib/docs/getBuildStatus.test.mjs +133 -0
  159. package/lib/docs/getBuildStatusAfterEdits.js +35 -0
  160. package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
  161. package/lib/docs/getCoreDoc.js +9 -1
  162. package/lib/docs/getDoc.js +100 -0
  163. package/lib/docs/getDocsIndex.js +152 -0
  164. package/lib/docs/getHazards.js +115 -0
  165. package/lib/docs/getHazards.test.mjs +175 -0
  166. package/lib/docs/getMailOutboxDirectory.js +26 -0
  167. package/lib/docs/getOverview.js +79 -13
  168. package/lib/docs/getOverviewBasePath.test.mjs +37 -0
  169. package/lib/docs/getPageBuildStatus.js +46 -0
  170. package/lib/docs/getPluginDoc.js +21 -23
  171. package/lib/docs/getSchema.js +33 -1
  172. package/lib/docs/getStaleStatus.js +36 -0
  173. package/lib/docs/getStaleStatus.test.mjs +74 -0
  174. package/lib/docs/guardJourneyOrigin.js +39 -0
  175. package/lib/docs/inspectState.js +5 -3
  176. package/lib/docs/inspectStateFromTab.js +2 -2
  177. package/lib/docs/inspectStateHeadless.js +89 -27
  178. package/lib/docs/inspectStateHeadless.test.mjs +20 -1
  179. package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
  180. package/lib/docs/installHeadlessShell.js +123 -0
  181. package/lib/docs/installHeadlessShell.test.mjs +70 -0
  182. package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
  183. package/lib/docs/isHeadlessShellIncomplete.js +34 -0
  184. package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
  185. package/lib/docs/isPageReady.js +77 -0
  186. package/lib/docs/isPageReady.test.mjs +235 -0
  187. package/lib/docs/isRunErrorEntry.js +25 -0
  188. package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
  189. package/lib/docs/journeyRunRecording.js +35 -0
  190. package/lib/docs/journeyRunRecording.test.mjs +39 -0
  191. package/lib/docs/keepEventStreamAlive.js +68 -0
  192. package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
  193. package/lib/docs/launchBrowser.js +83 -0
  194. package/lib/docs/launchBrowser.test.mjs +175 -0
  195. package/lib/docs/listModuleDocEntries.js +69 -0
  196. package/lib/docs/listPackageDocFiles.js +67 -0
  197. package/lib/docs/listPluginDocEntries.js +111 -0
  198. package/lib/docs/listPlugins.js +9 -58
  199. package/lib/docs/listTypes.js +14 -6
  200. package/lib/docs/loadFrameworkHazards.js +40 -0
  201. package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
  202. package/lib/docs/loadState.js +103 -35
  203. package/lib/docs/makeModuleManifestDoc.js +174 -0
  204. package/lib/docs/matchExercisedPages.js +35 -0
  205. package/lib/docs/matchJourneyRoute.js +69 -0
  206. package/lib/docs/matchJourneyRoute.test.mjs +178 -0
  207. package/lib/docs/mergeNetworkSnapshots.js +49 -0
  208. package/lib/docs/nextJourneySequence.js +30 -0
  209. package/lib/docs/noBrowserError.js +22 -0
  210. package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
  211. package/lib/docs/observe/claimExpectedErrors.js +67 -0
  212. package/lib/docs/observe/createJourneyAppErrors.js +236 -0
  213. package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
  214. package/lib/docs/observe/evaluateInvariants.js +183 -0
  215. package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
  216. package/lib/docs/observe/findingKey.js +29 -0
  217. package/lib/docs/observe/hasNoEffect.js +31 -0
  218. package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
  219. package/lib/docs/observe/installStepObserver.js +72 -0
  220. package/lib/docs/observe/isAppApiUrl.js +33 -0
  221. package/lib/docs/observe/parseAppApiUrl.js +37 -0
  222. package/lib/docs/observe/readStepObserver.js +42 -0
  223. package/lib/docs/observe/relativeSource.js +29 -0
  224. package/lib/docs/observe/resolveConfigKeySource.js +33 -0
  225. package/lib/docs/observe/takeJourneyEvents.js +36 -0
  226. package/lib/docs/observe/toAppErrorFailure.js +61 -0
  227. package/lib/docs/observe/usesSearchStage.js +45 -0
  228. package/lib/docs/observe/waitForClientErrorReports.js +31 -0
  229. package/lib/docs/observe/watchJourneyContext.js +75 -0
  230. package/lib/docs/observe/withAppErrors.js +31 -0
  231. package/lib/docs/observe/withAppErrors.test.mjs +101 -0
  232. package/lib/docs/openJourney.js +94 -0
  233. package/lib/docs/openJourneyEmail.js +41 -0
  234. package/lib/docs/openJourneyEmail.test.mjs +187 -0
  235. package/lib/docs/pageBlocks.js +47 -0
  236. package/lib/docs/pageBuildStatus.test.mjs +103 -0
  237. package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
  238. package/lib/docs/rails/authEngineActions.js +53 -0
  239. package/lib/docs/rails/authEngineActions.test.mjs +69 -0
  240. package/lib/docs/rails/authEngineMethods.js +52 -0
  241. package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
  242. package/lib/docs/rails/checkAuthActionsRan.js +66 -0
  243. package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
  244. package/lib/docs/rails/checkClickRails.js +56 -0
  245. package/lib/docs/rails/createDataSetRails.js +82 -0
  246. package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
  247. package/lib/docs/rails/findExternalBlocks.js +147 -0
  248. package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
  249. package/lib/docs/rails/installAuthActionRecorder.js +56 -0
  250. package/lib/docs/rails/readAuthActionRecorder.js +32 -0
  251. package/lib/docs/rails/readBuiltArtifact.js +30 -0
  252. package/lib/docs/readConnectionPlugins.js +26 -0
  253. package/lib/docs/readDevAuthMode.js +32 -0
  254. package/lib/docs/readDocEntry.js +40 -0
  255. package/lib/docs/readDocTitle.js +26 -0
  256. package/lib/docs/readJourneyEmailMatch.js +57 -0
  257. package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
  258. package/lib/docs/readJourneySession.js +56 -0
  259. package/lib/docs/readMailOutbox.js +37 -0
  260. package/lib/docs/readPagePath.js +27 -0
  261. package/lib/docs/readPluginPackages.js +99 -0
  262. package/lib/docs/readProxyBuildWait.js +38 -0
  263. package/lib/docs/readRoutineSteps.js +45 -0
  264. package/lib/docs/registerShownTextEngine.js +118 -0
  265. package/lib/docs/requestRestart.js +30 -0
  266. package/lib/docs/requestRestart.test.mjs +49 -0
  267. package/lib/docs/resolvePageInstance.js +46 -0
  268. package/lib/docs/resolvePageInstance.test.mjs +81 -0
  269. package/lib/docs/resolveSource.js +11 -7
  270. package/lib/docs/resolveSource.test.mjs +1 -1
  271. package/lib/docs/resolveToolCaller.js +62 -0
  272. package/lib/docs/reviewPage.js +51 -0
  273. package/lib/docs/reviewPageBuilds.js +56 -0
  274. package/lib/docs/reviewPageBuilds.test.mjs +207 -0
  275. package/lib/docs/runCheck.js +75 -0
  276. package/lib/docs/runCheck.test.mjs +91 -0
  277. package/lib/docs/runCheckChild.js +49 -0
  278. package/lib/docs/runEndpoint.js +185 -0
  279. package/lib/docs/runEndpoint.test.mjs +509 -0
  280. package/lib/docs/runErrorBuffers.js +99 -0
  281. package/lib/docs/runErrorBuffers.test.mjs +95 -0
  282. package/lib/docs/runJourney.js +347 -0
  283. package/lib/docs/runJourney.test.mjs +2383 -0
  284. package/lib/docs/runJourneyAsUsers.js +44 -0
  285. package/lib/docs/runJourneyDataSet.test.mjs +274 -0
  286. package/lib/docs/runJourneySteps.js +1320 -0
  287. package/lib/docs/runRequest.js +100 -59
  288. package/lib/docs/runRequest.test.mjs +190 -0
  289. package/lib/docs/saveAnnotatedScreenshot.js +1 -1
  290. package/lib/docs/scaffoldPage.js +3 -1
  291. package/lib/docs/screenshotPage.js +163 -11
  292. package/lib/docs/screenshotPage.test.mjs +263 -1
  293. package/lib/docs/screenshotPageClip.test.mjs +4 -0
  294. package/lib/docs/searchDocs.js +82 -30
  295. package/lib/docs/searchDocs.test.mjs +100 -0
  296. package/lib/docs/searchIcons.js +90 -0
  297. package/lib/docs/searchIcons.test.mjs +127 -0
  298. package/lib/docs/selectExercised.js +39 -0
  299. package/lib/docs/selectFinalState.js +66 -0
  300. package/lib/docs/selectFinalState.test.mjs +51 -0
  301. package/lib/docs/serverErrorStore.js +54 -0
  302. package/lib/docs/serverErrorStore.test.mjs +50 -0
  303. package/lib/docs/setupTestFixtures.mjs +133 -39
  304. package/lib/docs/shownTextSelector.js +28 -0
  305. package/lib/docs/snapshotState.js +13 -5
  306. package/lib/docs/splitSearchTerms.js +62 -0
  307. package/lib/docs/splitSearchTerms.test.mjs +50 -0
  308. package/lib/docs/stateCheckpoints.test.mjs +77 -0
  309. package/lib/docs/tabAvailable.js +6 -5
  310. package/lib/docs/tabChannel.js +127 -21
  311. package/lib/docs/tabChannel.test.mjs +187 -29
  312. package/lib/docs/unsettledPageNote.js +24 -0
  313. package/lib/docs/validateJourneyMail.js +36 -0
  314. package/lib/docs/validateJourneyTimeout.js +38 -0
  315. package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
  316. package/lib/docs/validateRecordingParam.js +42 -0
  317. package/lib/docs/validateStateSelection.js +33 -0
  318. package/lib/docs/validateStateSelection.test.mjs +33 -0
  319. package/lib/docs/validateViewport.js +51 -0
  320. package/lib/docs/validateViewport.test.mjs +54 -0
  321. package/lib/docs/waitForBuild.js +50 -0
  322. package/lib/docs/waitForBuild.test.mjs +75 -0
  323. package/lib/docs/waitForJourneyEmail.js +77 -0
  324. package/lib/docs/withBrowserSlot.js +45 -0
  325. package/lib/docs/withBrowserSlot.test.mjs +75 -0
  326. package/lib/docs/withDataSession.js +53 -0
  327. package/lib/server/applyDataSetRedirect.js +83 -0
  328. package/lib/server/applyDataSetRedirect.test.mjs +340 -0
  329. package/lib/server/applyDataSetRedirectTenantReach.test.mjs +204 -0
  330. package/lib/server/applyDataSetRedirectWalled.test.mjs +131 -0
  331. package/lib/server/auth/createSystemContext.js +73 -0
  332. package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
  333. package/lib/server/auth/getHeadlessUser.js +9 -8
  334. package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
  335. package/lib/server/auth/getMockUser.js +18 -14
  336. package/lib/server/auth/{session.js → getStrategies.js} +17 -16
  337. package/lib/server/auth/headlessUser.js +3 -2
  338. package/lib/server/auth/journeyActor.js +35 -0
  339. package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
  340. package/lib/server/checkPageRecord.js +44 -0
  341. package/lib/server/contextMapBudget.js +24 -0
  342. package/lib/server/contextMapBudget.test.mjs +98 -0
  343. package/lib/server/continueJitKeys.js +28 -0
  344. package/lib/server/continueJitKeys.test.mjs +43 -0
  345. package/lib/server/createDataSessionEndedError.js +33 -0
  346. package/lib/server/createDataSetRefusal.js +26 -0
  347. package/lib/server/createLowdefyContext.js +168 -41
  348. package/lib/server/createLowdefyContext.test.mjs +328 -8
  349. package/lib/server/getClientAddress.js +51 -0
  350. package/lib/server/getClientAddress.test.mjs +56 -0
  351. package/lib/server/getPageJitEnrichment.test.mjs +29 -11
  352. package/lib/server/hashConfigContent.js +26 -0
  353. package/lib/server/importAppCode.test.mjs +57 -0
  354. package/lib/server/isRoutineStep.js +27 -0
  355. package/lib/server/jitPageBuilder.js +309 -126
  356. package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
  357. package/lib/server/jitPageBuilder.test.mjs +218 -0
  358. package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
  359. package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
  360. package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
  361. package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
  362. package/lib/server/journeyCookies.js +22 -0
  363. package/lib/server/journeyCookies.test.mjs +145 -0
  364. package/lib/server/loadDynamicJsMap.js +47 -0
  365. package/lib/server/log/createHandleError.js +38 -2
  366. package/lib/server/log/createHandleError.test.mjs +125 -2
  367. package/lib/server/log/createLogger.js +3 -0
  368. package/lib/server/log/createLogger.test.mjs +15 -0
  369. package/lib/server/log/logRequest.js +2 -3
  370. package/lib/server/mutantDataSetWrappers.test.mjs +112 -0
  371. package/lib/server/mutants/applyMutant.js +39 -0
  372. package/lib/server/mutants/artifactPatterns.js +25 -0
  373. package/lib/server/mutants/buildAnchor.js +45 -0
  374. package/lib/server/mutants/collectSharedIds.js +41 -0
  375. package/lib/server/mutants/configPathSegments.js +38 -0
  376. package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
  377. package/lib/server/mutants/describeEvent.js +22 -0
  378. package/lib/server/mutants/enumerateArtifact.js +56 -0
  379. package/lib/server/mutants/findNodeByKey.js +45 -0
  380. package/lib/server/mutants/findNodeByPointer.js +49 -0
  381. package/lib/server/mutants/getArtifactKind.js +32 -0
  382. package/lib/server/mutants/groupMutantCopies.js +56 -0
  383. package/lib/server/mutants/hashArtifact.js +25 -0
  384. package/lib/server/mutants/hashMutantTarget.js +34 -0
  385. package/lib/server/mutants/listMutants.js +122 -0
  386. package/lib/server/mutants/listMutants.test.mjs +502 -0
  387. package/lib/server/mutants/mutantId.js +30 -0
  388. package/lib/server/mutants/mutantRuns.js +57 -0
  389. package/lib/server/mutants/mutantRuns.test.mjs +136 -0
  390. package/lib/server/mutants/operators/dropAction.js +47 -0
  391. package/lib/server/mutants/operators/dropBlock.js +44 -0
  392. package/lib/server/mutants/operators/dropPayload.js +52 -0
  393. package/lib/server/mutants/operators/dropStep.js +44 -0
  394. package/lib/server/mutants/operators/flipVisible.js +53 -0
  395. package/lib/server/mutants/operators/index.js +37 -0
  396. package/lib/server/mutants/operators/retargetLink.js +72 -0
  397. package/lib/server/mutants/operators/skipValidate.js +77 -0
  398. package/lib/server/mutants/operators/swapIf.js +112 -0
  399. package/lib/server/mutants/operators.test.mjs +396 -0
  400. package/lib/server/mutants/positionFreePath.js +38 -0
  401. package/lib/server/mutants/removeFromParent.js +28 -0
  402. package/lib/server/mutants/test/fixtures.mjs +173 -0
  403. package/lib/server/mutants/test/keyArtifact.mjs +63 -0
  404. package/lib/server/mutants/validateMutantParam.js +69 -0
  405. package/lib/server/mutants/validateMutantsBody.js +88 -0
  406. package/lib/server/mutants/walkArtifact.js +179 -0
  407. package/lib/server/pageBuildRecords.js +131 -0
  408. package/lib/server/pageBuildRecords.test.mjs +258 -0
  409. package/lib/server/pageCache.mjs +22 -7
  410. package/lib/server/pageCache.test.mjs +22 -7
  411. package/lib/server/pageRebuilds.test.mjs +543 -0
  412. package/lib/server/pruneJitMaps.js +36 -0
  413. package/lib/server/pruneJitMaps.test.mjs +55 -0
  414. package/lib/server/readInstanceRecord.js +33 -0
  415. package/lib/server/readInstanceRecord.test.mjs +99 -0
  416. package/lib/server/readMergedMaps.js +86 -0
  417. package/lib/server/readMergedMaps.test.mjs +79 -0
  418. package/lib/server/recording/appendRecords.js +33 -0
  419. package/lib/server/recording/errorRecordingStamp.js +34 -0
  420. package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
  421. package/lib/server/recording/isRecordingEnabled.js +31 -0
  422. package/lib/server/recording/readRecordingCookie.js +71 -0
  423. package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
  424. package/lib/server/recording/recordingCookiePayload.js +33 -0
  425. package/lib/server/recording/servedBuilds.js +46 -0
  426. package/lib/server/recording/servedBuilds.test.mjs +42 -0
  427. package/lib/server/sinkExternalConnections.js +95 -0
  428. package/lib/server/sinkExternalConnections.test.mjs +135 -0
  429. package/lib/server/skipStaleMapWrites.js +62 -0
  430. package/lib/server/skipStaleMapWrites.test.mjs +113 -0
  431. package/lib/server/startIdleGc.js +67 -0
  432. package/lib/server/startIdleGc.test.mjs +138 -0
  433. package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
  434. package/lib/server/trackSessionWork.js +30 -0
  435. package/lib/vite/createDevServerExclude.js +40 -0
  436. package/lib/vite/createDevServerExclude.test.mjs +79 -0
  437. package/manager/getContext.mjs +24 -2
  438. package/manager/processes/initialBuild.mjs +14 -0
  439. package/manager/processes/initialBuild.test.mjs +101 -0
  440. package/manager/processes/lowdefyBuild.mjs +30 -7
  441. package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +62 -0
  442. package/manager/processes/lowdefyBuild.test.mjs +136 -1
  443. package/manager/processes/optimizeDependencies.mjs +59 -0
  444. package/manager/processes/restartServer.mjs +34 -6
  445. package/manager/processes/restartServer.test.mjs +162 -0
  446. package/manager/processes/shutdownServer.mjs +11 -0
  447. package/manager/processes/shutdownServer.test.mjs +45 -0
  448. package/manager/processes/startFirstServer.mjs +46 -0
  449. package/manager/processes/startFirstServer.test.mjs +326 -0
  450. package/manager/processes/startMailSink.mjs +135 -0
  451. package/manager/processes/startMailSink.test.mjs +146 -0
  452. package/manager/processes/startProxy.mjs +129 -12
  453. package/manager/processes/startProxy.test.mjs +366 -0
  454. package/manager/processes/startServer.mjs +47 -6
  455. package/manager/processes/startServer.test.mjs +155 -0
  456. package/manager/processes/startWatchers.mjs +6 -4
  457. package/manager/processes/syncServer.mjs +69 -0
  458. package/manager/processes/syncServer.test.mjs +141 -0
  459. package/manager/processes/waitForDevServer.mjs +48 -0
  460. package/manager/processes/waitForDevServer.test.mjs +112 -0
  461. package/manager/run.mjs +128 -43
  462. package/manager/utils/BatchChanges.mjs +21 -5
  463. package/manager/utils/BatchChanges.test.mjs +77 -0
  464. package/manager/utils/acquireDevInstance.mjs +99 -0
  465. package/manager/utils/acquireDevInstance.test.mjs +160 -0
  466. package/manager/utils/createBuildActivity.mjs +67 -0
  467. package/manager/utils/createBuildActivity.test.mjs +58 -0
  468. package/manager/utils/createChangeSignal.mjs +39 -0
  469. package/manager/utils/createChangeSignal.test.mjs +62 -0
  470. package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
  471. package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
  472. package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
  473. package/manager/utils/createDotPathIgnore.mjs +44 -0
  474. package/manager/utils/createDotPathIgnore.test.mjs +65 -0
  475. package/manager/utils/createIdleGc.mjs +51 -0
  476. package/manager/utils/createIdleGc.test.mjs +77 -0
  477. package/manager/utils/createRequestActivity.mjs +100 -0
  478. package/manager/utils/createRequestActivity.test.mjs +121 -0
  479. package/manager/utils/createServerArtifactTracker.mjs +83 -0
  480. package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
  481. package/manager/utils/createServerEnv.mjs +39 -0
  482. package/manager/utils/filesHaveSameBytes.mjs +41 -0
  483. package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
  484. package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
  485. package/manager/utils/importFresh.mjs +46 -0
  486. package/manager/utils/importFresh.test.mjs +72 -0
  487. package/manager/utils/importFreshWorker.mjs +20 -0
  488. package/manager/utils/killTaggedBrowser.mjs +35 -0
  489. package/manager/utils/pagePathChanged.mjs +56 -0
  490. package/manager/utils/pruneRecordings.mjs +85 -0
  491. package/manager/utils/pruneRecordings.test.mjs +117 -0
  492. package/manager/utils/publishBuildDirectory.mjs +121 -0
  493. package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
  494. package/manager/utils/publishBuildDirectory.test.mjs +262 -0
  495. package/manager/utils/readBasePath.mjs +27 -0
  496. package/manager/utils/readBasePath.test.mjs +43 -0
  497. package/manager/utils/readBuildStatusWait.mjs +67 -0
  498. package/manager/utils/readManagerVersion.mjs +30 -0
  499. package/manager/utils/readManagerVersion.test.mjs +35 -0
  500. package/manager/utils/readPluginDefinitions.mjs +36 -0
  501. package/manager/utils/resolveDevAuthUrl.mjs +58 -0
  502. package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
  503. package/manager/utils/resolvePorts.mjs +49 -0
  504. package/manager/utils/setupWatcher.mjs +19 -11
  505. package/manager/utils/startRecordingPruner.mjs +45 -0
  506. package/manager/utils/startRecordingPruner.test.mjs +66 -0
  507. package/manager/utils/waitForChildExit.mjs +53 -0
  508. package/manager/utils/waitForChildExit.test.mjs +70 -0
  509. package/manager/utils/waitForServer.mjs +44 -0
  510. package/manager/utils/writeBuildStatus.mjs +1 -1
  511. package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
  512. package/manager/watchers/envWatcher.mjs +9 -2
  513. package/manager/watchers/envWatcher.test.mjs +87 -0
  514. package/manager/watchers/listServerSidePackages.mjs +47 -0
  515. package/manager/watchers/listServerSidePackages.test.mjs +38 -0
  516. package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
  517. package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
  518. package/manager/watchers/pluginSourceWatcher.mjs +79 -0
  519. package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
  520. package/manager/watchers/restartRequestWatcher.mjs +71 -0
  521. package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
  522. package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
  523. package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
  524. package/manager/watchers/serverPackageWatcher.mjs +38 -0
  525. package/package.json +60 -47
  526. package/package.original.json +60 -47
  527. package/src/app.js +131 -20
  528. package/src/html/renderDevPage.js +63 -11
  529. package/src/html/renderDevPage.test.mjs +138 -0
  530. package/src/lib/getRequestPath.js +29 -0
  531. package/src/middleware/createSameOriginGuard.js +37 -0
  532. package/src/middleware/createSameOriginGuard.test.mjs +102 -0
  533. package/src/middleware/errorHandler.js +51 -1
  534. package/src/middleware/errorHandler.test.mjs +173 -3
  535. package/src/middleware/isRebindingSafeHost.js +44 -0
  536. package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
  537. package/src/middleware/localDevToolsOnly.js +38 -0
  538. package/src/middleware/localDevToolsOnly.test.mjs +61 -0
  539. package/src/middleware/noStore.js +27 -0
  540. package/src/middleware/noStore.test.mjs +48 -0
  541. package/src/middleware/requestTimeout.js +38 -0
  542. package/src/middleware/staleFlag.js +76 -0
  543. package/src/middleware/staleFlag.test.mjs +127 -0
  544. package/src/middleware/stampCaller.js +38 -0
  545. package/src/middleware/stampCaller.test.mjs +64 -0
  546. package/src/routes/agent.js +15 -3
  547. package/src/routes/auth.js +29 -20
  548. package/src/routes/auth.test.mjs +30 -56
  549. package/src/routes/clientError.js +13 -13
  550. package/src/routes/cron.js +3 -1
  551. package/src/routes/cronForward.js +3 -1
  552. package/src/routes/detached.js +22 -9
  553. package/src/routes/devInspect.js +17 -8
  554. package/src/routes/devInspect.test.mjs +44 -7
  555. package/src/routes/devRecording.js +113 -0
  556. package/src/routes/devRecording.test.mjs +253 -0
  557. package/src/routes/docs/buildStatus.js +10 -2
  558. package/src/routes/docs/buildStatus.test.mjs +46 -0
  559. package/src/routes/docs/content.js +5 -3
  560. package/src/routes/docs/evalOperator.js +15 -1
  561. package/src/routes/docs/events.js +50 -0
  562. package/src/routes/docs/events.test.mjs +117 -0
  563. package/src/routes/docs/find.js +9 -1
  564. package/src/routes/docs/icons.js +30 -0
  565. package/src/routes/docs/inspectState.js +16 -1
  566. package/src/routes/docs/journey.js +194 -0
  567. package/src/routes/docs/journey.test.mjs +401 -0
  568. package/src/routes/docs/journeyRefusals.test.mjs +71 -0
  569. package/src/routes/docs/loadState.js +4 -2
  570. package/src/routes/docs/mcp.js +34 -3
  571. package/src/routes/docs/mcp.test.mjs +130 -0
  572. package/src/routes/docs/mutants.js +78 -0
  573. package/src/routes/docs/mutants.test.mjs +180 -0
  574. package/src/routes/docs/parsePathParamsParam.js +41 -0
  575. package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
  576. package/src/routes/docs/parseUserParam.js +27 -21
  577. package/src/routes/docs/parseUserParam.test.mjs +17 -5
  578. package/src/routes/docs/restart.js +35 -0
  579. package/src/routes/docs/restart.test.mjs +64 -0
  580. package/src/routes/docs/runEndpoint.js +54 -0
  581. package/src/routes/docs/runEndpoint.test.mjs +165 -0
  582. package/src/routes/docs/runRequest.js +25 -8
  583. package/src/routes/docs/runRequest.test.mjs +92 -0
  584. package/src/routes/docs/screenshot.js +50 -2
  585. package/src/routes/docs/screenshot.test.mjs +117 -0
  586. package/src/routes/docs/search.js +15 -2
  587. package/src/routes/docs/snapshotState.js +17 -3
  588. package/src/routes/endpoints.js +27 -26
  589. package/src/routes/endpointsTextAnswer.test.mjs +77 -0
  590. package/src/routes/feedback.js +10 -13
  591. package/src/routes/jitPage.js +99 -35
  592. package/src/routes/jitPage.test.mjs +248 -0
  593. package/src/routes/mcp.js +82 -5
  594. package/src/routes/mcp.test.mjs +215 -0
  595. package/src/routes/mcpMemberToken.test.mjs +125 -0
  596. package/src/routes/mcpProtectedResourceMetadata.js +34 -0
  597. package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
  598. package/src/routes/mountDevPageRoutes.js +25 -0
  599. package/src/routes/mountDevPageRoutes.test.mjs +30 -0
  600. package/src/routes/mountOauthDiscovery.js +44 -0
  601. package/src/routes/mountOauthDiscovery.test.mjs +79 -0
  602. package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
  603. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
  604. package/src/routes/reload.js +16 -0
  605. package/src/routes/request.js +6 -4
  606. package/src/routes/request.test.mjs +9 -5
  607. package/src/routes/requestBody.test.mjs +159 -0
  608. package/src/routes/usage.js +6 -2
  609. package/src/routes/user.js +27 -0
  610. package/src/routes/websocket.js +29 -8
  611. package/src/routes/websocket.test.mjs +65 -0
  612. package/src/routes/wellKnownFallback.js +30 -0
  613. package/src/routes/wellKnownFallback.test.mjs +32 -0
  614. package/src/websocket/devWebSocket.js +41 -26
  615. package/src/websocket/devWebSocket.test.mjs +157 -0
  616. package/vite.config.js +53 -23
  617. package/lib/server/auth/getDevSession.js +0 -70
  618. package/lib/server/auth/getDevSession.test.mjs +0 -138
  619. package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
  620. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
  621. package/manager/watchers/moduleBuildWatcher.mjs +0 -79
  622. package/manager/watchers/serverArtifactWatcher.mjs +0 -120
  623. package/src/routes/apiPage.js +0 -37
@@ -16,8 +16,15 @@
16
16
 
17
17
  import { jest } from '@jest/globals';
18
18
 
19
- const { listTabs, registerTab, requestFromTab, resolveTabRequest, unregisterTab, updateTabPage } =
20
- await import('./tabChannel.js');
19
+ const {
20
+ findPageInstance,
21
+ listTabs,
22
+ registerTab,
23
+ requestFromTab,
24
+ resolveTabRequest,
25
+ unregisterTab,
26
+ updateTabPage,
27
+ } = await import('./tabChannel.js');
21
28
 
22
29
  // tabChannel keeps its registries at module scope, so clear connected tabs
23
30
  // between tests to keep them independent.
@@ -25,43 +32,139 @@ afterEach(() => {
25
32
  listTabs().forEach((tab) => unregisterTab({ id: tab.id }));
26
33
  });
27
34
 
28
- test('registerTab adds a tab visible via listTabs', () => {
29
- registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
35
+ function ticket({ space = 's', ticketId }) {
36
+ return {
37
+ pageId: 'ticket',
38
+ pathParams: { space, ticket_id: ticketId },
39
+ instanceKey: `page:ticket#tickets/${space}/${ticketId}`,
40
+ };
41
+ }
42
+
43
+ function connectTab({ id, pageId, send = jest.fn(), ...origin }) {
44
+ registerTab({ id, send, ...origin });
45
+ if (pageId) {
46
+ updateTabPage({ id, pageId, pathParams: {}, instanceKey: `page:${pageId}` });
47
+ }
48
+ return send;
49
+ }
50
+
51
+ test('registerTab adds a tab visible via listTabs, on no page until its first page renders', () => {
52
+ registerTab({ id: 'tab-1', send: jest.fn() });
30
53
  const tabs = listTabs();
31
54
  expect(tabs).toHaveLength(1);
32
- expect(tabs[0]).toMatchObject({ id: 'tab-1', pageId: 'home' });
55
+ expect(tabs[0]).toMatchObject({ id: 'tab-1', pageId: null, pathParams: null, instanceKey: null });
33
56
  expect(tabs[0].connectedAt).toBeInstanceOf(Date);
34
57
  });
35
58
 
36
59
  test('registerTab throws when id is missing', () => {
37
- expect(() => registerTab({ pageId: 'home', send: jest.fn() })).toThrow('registerTab requires');
60
+ expect(() => registerTab({ send: jest.fn() })).toThrow('registerTab requires');
38
61
  });
39
62
 
40
63
  test('registerTab throws when send is not a function', () => {
41
- expect(() => registerTab({ id: 'tab-1', pageId: 'home' })).toThrow(
42
- 'registerTab requires a "send" function'
43
- );
64
+ expect(() => registerTab({ id: 'tab-1' })).toThrow('registerTab requires a "send" function');
44
65
  });
45
66
 
46
67
  test('unregisterTab removes a tab from the registry', () => {
47
- registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
68
+ connectTab({ id: 'tab-1', pageId: 'home' });
48
69
  unregisterTab({ id: 'tab-1' });
49
70
  expect(listTabs()).toHaveLength(0);
50
71
  });
51
72
 
52
- test('updateTabPage changes the pageId of a registered tab', () => {
53
- registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
54
- updateTabPage({ id: 'tab-1', pageId: 'about' });
55
- expect(listTabs()[0].pageId).toEqual('about');
73
+ test('updateTabPage records the page instance a tab shows', () => {
74
+ connectTab({ id: 'tab-1', pageId: 'home' });
75
+ updateTabPage({ id: 'tab-1', ...ticket({ ticketId: '2' }) });
76
+ expect(listTabs()[0]).toMatchObject({
77
+ id: 'tab-1',
78
+ pageId: 'ticket',
79
+ pathParams: { space: 's', ticket_id: '2' },
80
+ instanceKey: 'page:ticket#tickets/s/2',
81
+ });
56
82
  });
57
83
 
58
84
  test('updateTabPage on an unknown tab id does not throw', () => {
59
- expect(() => updateTabPage({ id: 'missing', pageId: 'about' })).not.toThrow();
85
+ expect(() =>
86
+ updateTabPage({ id: 'missing', pageId: 'about', instanceKey: 'page:about' })
87
+ ).not.toThrow();
88
+ });
89
+
90
+ test('updateTabPage throws when the instance key is missing', () => {
91
+ registerTab({ id: 'tab-1', send: jest.fn() });
92
+ expect(() => updateTabPage({ id: 'tab-1', pageId: 'about' })).toThrow(
93
+ 'updateTabPage requires "pageId" and "instanceKey" strings'
94
+ );
95
+ });
96
+
97
+ test('findPageInstance reads the instance on screen when the tab is on the page', () => {
98
+ registerTab({ id: 'tab-1', send: jest.fn() });
99
+ updateTabPage({ id: 'tab-1', ...ticket({ ticketId: '1' }) });
100
+ updateTabPage({ id: 'tab-1', ...ticket({ ticketId: '2' }) });
101
+ const { tab, ...instance } = findPageInstance({ pageId: 'ticket' });
102
+ expect(tab.id).toEqual('tab-1');
103
+ expect(instance).toEqual(ticket({ ticketId: '2' }));
104
+ });
105
+
106
+ test('findPageInstance reads the most recently rendered instance when no tab is on the page', () => {
107
+ registerTab({ id: 'tab-1', send: jest.fn() });
108
+ registerTab({ id: 'tab-2', send: jest.fn() });
109
+ updateTabPage({ id: 'tab-1', ...ticket({ ticketId: '1' }) });
110
+ updateTabPage({ id: 'tab-2', ...ticket({ ticketId: '2' }) });
111
+ updateTabPage({ id: 'tab-1', pageId: 'home', instanceKey: 'page:home' });
112
+ updateTabPage({ id: 'tab-2', pageId: 'home', instanceKey: 'page:home' });
113
+ const { tab, ...instance } = findPageInstance({ pageId: 'ticket' });
114
+ expect(tab.id).toEqual('tab-2');
115
+ expect(instance).toEqual(ticket({ ticketId: '2' }));
116
+ });
117
+
118
+ test('findPageInstance prefers a tab showing the page over a later render elsewhere', () => {
119
+ registerTab({ id: 'tab-1', send: jest.fn() });
120
+ registerTab({ id: 'tab-2', send: jest.fn() });
121
+ updateTabPage({ id: 'tab-1', ...ticket({ ticketId: '1' }) });
122
+ updateTabPage({ id: 'tab-2', ...ticket({ ticketId: '2' }) });
123
+ updateTabPage({ id: 'tab-2', pageId: 'home', instanceKey: 'page:home' });
124
+ const { tab, ...instance } = findPageInstance({ pageId: 'ticket' });
125
+ expect(tab.id).toEqual('tab-1');
126
+ expect(instance).toEqual(ticket({ ticketId: '1' }));
127
+ });
128
+
129
+ test('findPageInstance with pathParams reads that instance, comparing values as strings', () => {
130
+ registerTab({ id: 'tab-1', send: jest.fn() });
131
+ updateTabPage({ id: 'tab-1', ...ticket({ ticketId: '1' }) });
132
+ updateTabPage({ id: 'tab-1', ...ticket({ ticketId: '2' }) });
133
+ const { tab, ...instance } = findPageInstance({
134
+ pageId: 'ticket',
135
+ pathParams: { space: 's', ticket_id: 1 },
136
+ });
137
+ expect(tab.id).toEqual('tab-1');
138
+ expect(instance).toEqual(ticket({ ticketId: '1' }));
139
+ });
140
+
141
+ test('findPageInstance returns undefined for values no tab has rendered', () => {
142
+ registerTab({ id: 'tab-1', send: jest.fn() });
143
+ updateTabPage({ id: 'tab-1', ...ticket({ ticketId: '1' }) });
144
+ expect(
145
+ findPageInstance({ pageId: 'ticket', pathParams: { space: 's', ticket_id: '3' } })
146
+ ).toBeUndefined();
147
+ expect(findPageInstance({ pageId: 'ticket', pathParams: { space: 's' } })).toBeUndefined();
148
+ });
149
+
150
+ test('findPageInstance without a pageId reads the most recently connected tab on screen', () => {
151
+ connectTab({ id: 'tab-1', pageId: 'home' });
152
+ connectTab({ id: 'tab-2', pageId: 'about' });
153
+ registerTab({ id: 'tab-3', send: jest.fn() });
154
+ const { tab, ...instance } = findPageInstance();
155
+ expect(tab.id).toEqual('tab-2');
156
+ expect(instance).toEqual({ pageId: 'about', pathParams: {}, instanceKey: 'page:about' });
157
+ });
158
+
159
+ test('findPageInstance forgets the instances of a tab that disconnected', () => {
160
+ registerTab({ id: 'tab-1', send: jest.fn() });
161
+ updateTabPage({ id: 'tab-1', ...ticket({ ticketId: '1' }) });
162
+ unregisterTab({ id: 'tab-1' });
163
+ expect(findPageInstance({ pageId: 'ticket' })).toBeUndefined();
60
164
  });
61
165
 
62
166
  test('requestFromTab sends the event to the matching tab and resolves via resolveTabRequest', async () => {
63
- const send = jest.fn();
64
- registerTab({ id: 'tab-1', pageId: 'home', send });
167
+ const send = connectTab({ id: 'tab-1', pageId: 'home' });
65
168
  const promise = requestFromTab({ pageId: 'home', event: 'inspect-request' });
66
169
 
67
170
  expect(send).toHaveBeenCalledTimes(1);
@@ -74,11 +177,28 @@ test('requestFromTab sends the event to the matching tab and resolves via resolv
74
177
  await expect(promise).resolves.toEqual('snapshot');
75
178
  });
76
179
 
77
- test('requestFromTab targets the tab registered for the given pageId', async () => {
78
- const sendHome = jest.fn();
79
- const sendAbout = jest.fn();
80
- registerTab({ id: 'tab-home', pageId: 'home', send: sendHome });
81
- registerTab({ id: 'tab-about', pageId: 'about', send: sendAbout });
180
+ test('requestFromTab names the instance to read in the event it sends', async () => {
181
+ const send = jest.fn();
182
+ registerTab({ id: 'tab-1', send });
183
+ updateTabPage({ id: 'tab-1', ...ticket({ ticketId: '1' }) });
184
+ updateTabPage({ id: 'tab-1', ...ticket({ ticketId: '2' }) });
185
+ requestFromTab({
186
+ pageId: 'ticket',
187
+ pathParams: { space: 's', ticket_id: '1' },
188
+ event: 'eval-request',
189
+ payload: { expression: { _state: 'a' } },
190
+ timeout: 10,
191
+ });
192
+ expect(send.mock.calls[0][1]).toEqual({
193
+ requestId: expect.any(String),
194
+ ...ticket({ ticketId: '1' }),
195
+ expression: { _state: 'a' },
196
+ });
197
+ });
198
+
199
+ test('requestFromTab targets the tab on the given pageId', async () => {
200
+ const sendHome = connectTab({ id: 'tab-home', pageId: 'home' });
201
+ const sendAbout = connectTab({ id: 'tab-about', pageId: 'about' });
82
202
 
83
203
  const promise = requestFromTab({ pageId: 'about', event: 'eval-request' });
84
204
  expect(sendAbout).toHaveBeenCalledTimes(1);
@@ -90,9 +210,8 @@ test('requestFromTab targets the tab registered for the given pageId', async ()
90
210
  });
91
211
 
92
212
  test('requestFromTab picks the most recently registered tab when pageId is omitted', async () => {
93
- registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
94
- const sendLatest = jest.fn();
95
- registerTab({ id: 'tab-2', pageId: 'about', send: sendLatest });
213
+ connectTab({ id: 'tab-1', pageId: 'home' });
214
+ const sendLatest = connectTab({ id: 'tab-2', pageId: 'about' });
96
215
 
97
216
  const promise = requestFromTab({ event: 'inspect-request' });
98
217
  expect(sendLatest).toHaveBeenCalledTimes(1);
@@ -103,13 +222,25 @@ test('requestFromTab picks the most recently registered tab when pageId is omitt
103
222
  });
104
223
 
105
224
  test('requestFromTab resolves with an error when no tab matches the pageId', async () => {
106
- registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
225
+ connectTab({ id: 'tab-1', pageId: 'home' });
107
226
  const response = await requestFromTab({ pageId: 'missing-page', event: 'inspect-request' });
108
227
  expect(response.error).toContain('No browser tab connected');
109
228
  });
110
229
 
230
+ test('requestFromTab names the pathParams in its error when no tab has that instance', async () => {
231
+ connectTab({ id: 'tab-1', pageId: 'home' });
232
+ const response = await requestFromTab({
233
+ pageId: 'ticket',
234
+ pathParams: { space: 's', ticket_id: '1' },
235
+ event: 'inspect-request',
236
+ });
237
+ expect(response.error).toEqual(
238
+ 'No browser tab connected on page "ticket" with pathParams {"space":"s","ticket_id":"1"}. Ask the developer to open the page, or use source: "headless".'
239
+ );
240
+ });
241
+
111
242
  test('requestFromTab resolves with a timeout error when no response arrives in time', async () => {
112
- registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
243
+ connectTab({ id: 'tab-1', pageId: 'home' });
113
244
  const response = await requestFromTab({ pageId: 'home', event: 'inspect-request', timeout: 10 });
114
245
  expect(response.error).toContain('Timed out');
115
246
  });
@@ -119,8 +250,7 @@ test('resolveTabRequest returns false for an unknown requestId', () => {
119
250
  });
120
251
 
121
252
  test('resolveTabRequest returns false when called again for an already-settled requestId', async () => {
122
- const send = jest.fn();
123
- registerTab({ id: 'tab-1', pageId: 'home', send });
253
+ const send = connectTab({ id: 'tab-1', pageId: 'home' });
124
254
  const promise = requestFromTab({ pageId: 'home', event: 'inspect-request' });
125
255
  const requestId = send.mock.calls[0][1].requestId;
126
256
 
@@ -128,3 +258,31 @@ test('resolveTabRequest returns false when called again for an already-settled r
128
258
  await promise;
129
259
  expect(resolveTabRequest({ requestId, result: 'second' })).toBe(false);
130
260
  });
261
+
262
+ test('requestFromTab skips an automated journey tab for the developer tab on the same page', async () => {
263
+ const developerSend = connectTab({ id: 'developer', pageId: 'tickets' });
264
+ const walkSend = connectTab({
265
+ id: 'walk',
266
+ pageId: 'tickets',
267
+ source: 'journey',
268
+ automated: true,
269
+ });
270
+ const promise = requestFromTab({ pageId: 'tickets', event: 'inspect-request', timeout: 50 });
271
+ expect(developerSend).toHaveBeenCalledTimes(1);
272
+ expect(walkSend).not.toHaveBeenCalled();
273
+ const [, { requestId }] = developerSend.mock.calls[0];
274
+ resolveTabRequest({ requestId, result: { state: {} } });
275
+ await expect(promise).resolves.toEqual({ state: {} });
276
+ expect(listTabs().map(({ id, source, automated }) => ({ id, source, automated }))).toEqual([
277
+ { id: 'developer', source: 'dev', automated: false },
278
+ { id: 'walk', source: 'journey', automated: true },
279
+ ]);
280
+ });
281
+
282
+ test('requestFromTab finds no tab when only an automated tab is on the page', async () => {
283
+ connectTab({ id: 'walk', pageId: 'tickets', source: 'journey', automated: true });
284
+ await expect(requestFromTab({ pageId: 'tickets', event: 'inspect-request' })).resolves.toEqual({
285
+ error:
286
+ 'No browser tab connected on page "tickets". Ask the developer to open the page, or use source: "headless".',
287
+ });
288
+ });
@@ -0,0 +1,24 @@
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
+ // The note every headless tool attaches alongside `ready: false`, so an agent
18
+ // reading the result knows it is looking at a page mid-flight rather than the
19
+ // page's settled truth.
20
+ function unsettledPageNote({ timeout }) {
21
+ return `Page did not settle within ${timeout}ms — onMountAsync, a request or a subscription is still running. The result below is a snapshot of an unsettled page.`;
22
+ }
23
+
24
+ export default unsettledPageNote;
@@ -0,0 +1,36 @@
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 { type } from '@lowdefy/helpers';
18
+ import { getStepKey } from '@lowdefy/node-utils';
19
+
20
+ function readsMail(step) {
21
+ const key = getStepKey(step);
22
+ return key === 'email' || (key === 'fill' && !type.isUndefined(step.fill.fromEmail));
23
+ }
24
+
25
+ // Steps that read email need the dev server's mail sink. Without it they would
26
+ // each wait out their timeout for mail that can never arrive, so the journey is
27
+ // refused up front. Returns an error message, or undefined.
28
+ function validateJourneyMail({ steps }) {
29
+ const capturesMail = process.env.LOWDEFY_SERVER_DEV_MAIL_SINK === 'true';
30
+ if (steps.some(readsMail) && !capturesMail) {
31
+ return 'The steps read email (an "email" step or a "fill" with "fromEmail"), but this dev server captures no mail. Start (or restart) it with LOWDEFY_DEV_SMTP_PORT set to a free port, and point the app\'s SMTP connection at 127.0.0.1 on that port.';
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ export default validateJourneyMail;
@@ -0,0 +1,38 @@
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 { type } from '@lowdefy/helpers';
18
+
19
+ // A step that needs longer than a minute is a hung page, and a failing journey
20
+ // should report in minutes, not hours.
21
+ const MAX_JOURNEY_TIMEOUT = 60000;
22
+
23
+ // The journey's `timeout`: how long each step may wait, in milliseconds.
24
+ // Returns an error message, or undefined when it is omitted or well-formed.
25
+ function validateJourneyTimeout({ timeout }) {
26
+ if (type.isUndefined(timeout)) {
27
+ return undefined;
28
+ }
29
+ if (!type.isInt(timeout) || timeout < 1 || timeout > MAX_JOURNEY_TIMEOUT) {
30
+ return `The journey "timeout" must be a whole number of milliseconds from 1 to ${MAX_JOURNEY_TIMEOUT}. Received ${JSON.stringify(
31
+ timeout
32
+ )}.`;
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ export { MAX_JOURNEY_TIMEOUT };
38
+ export default validateJourneyTimeout;
@@ -0,0 +1,29 @@
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 validateJourneyTimeout from './validateJourneyTimeout.js';
18
+
19
+ test.each([undefined, 1, 5000, 60000])('validateJourneyTimeout accepts %j', (timeout) => {
20
+ expect(validateJourneyTimeout({ timeout })).toBeUndefined();
21
+ });
22
+
23
+ test.each([0, -1, 60001, 1500.5, '5000', null])('validateJourneyTimeout rejects %j', (timeout) => {
24
+ expect(validateJourneyTimeout({ timeout })).toEqual(
25
+ `The journey "timeout" must be a whole number of milliseconds from 1 to 60000. Received ${JSON.stringify(
26
+ timeout
27
+ )}.`
28
+ );
29
+ });
@@ -0,0 +1,42 @@
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 { isTraceId, type } from '@lowdefy/helpers';
18
+
19
+ // The journey route's optional `recording` param, { run, journey }: the test
20
+ // run (a trace id) this journey records into, and the journey's name in it.
21
+ // Returns an error message, or undefined when the param is absent or valid.
22
+ function validateRecordingParam(recording) {
23
+ if (type.isNone(recording)) return undefined;
24
+ if (!type.isObject(recording)) {
25
+ return `The "recording" param must be an object { run, journey }. Received ${JSON.stringify(
26
+ recording
27
+ )}.`;
28
+ }
29
+ if (!isTraceId(recording.run)) {
30
+ return `The "recording.run" param must be a trace id like "20261003T151200Z-p0d4rm". Received ${JSON.stringify(
31
+ recording.run
32
+ )}.`;
33
+ }
34
+ if (!type.isNone(recording.journey) && !type.isString(recording.journey)) {
35
+ return `The "recording.journey" param must be a string or null. Received ${JSON.stringify(
36
+ recording.journey
37
+ )}.`;
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ export default validateRecordingParam;
@@ -0,0 +1,33 @@
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 { type } from '@lowdefy/helpers';
18
+
19
+ // A journey's `state` option: true, false, or an array of state paths.
20
+ // Returns an error message, or undefined when it is valid or not given.
21
+ function validateStateSelection({ state }) {
22
+ if (type.isUndefined(state) || type.isBoolean(state)) {
23
+ return undefined;
24
+ }
25
+ if (type.isArray(state) && state.every((path) => type.isString(path) && path !== '')) {
26
+ return undefined;
27
+ }
28
+ return `The "state" option must be true, false or an array of state paths such as ["form.name"]. Received ${JSON.stringify(
29
+ state
30
+ )}.`;
31
+ }
32
+
33
+ export default validateStateSelection;
@@ -0,0 +1,33 @@
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 validateStateSelection from './validateStateSelection.js';
18
+
19
+ test.each([undefined, true, false, [], ['form.name', 'rows']])(
20
+ 'validateStateSelection accepts %j',
21
+ (state) => {
22
+ expect(validateStateSelection({ state })).toBeUndefined();
23
+ }
24
+ );
25
+
26
+ test.each(['all', null, 1, [''], ['a', 2], { path: 'a' }])(
27
+ 'validateStateSelection rejects %j',
28
+ (state) => {
29
+ expect(validateStateSelection({ state })).toMatch(
30
+ /^The "state" option must be true, false or an array of state paths/
31
+ );
32
+ }
33
+ );
@@ -0,0 +1,51 @@
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 { type } from '@lowdefy/helpers';
18
+
19
+ const COLOR_SCHEMES = ['light', 'dark'];
20
+
21
+ // Wide enough for a 4K screen. Chromium renders far larger viewports only
22
+ // slowly, and fails some of them after tying up the browser for many seconds;
23
+ // a full-page screenshot, not a tall viewport, is the way to see a long page.
24
+ const MAX_VIEWPORT_SIZE = 4096;
25
+
26
+ function isViewportSize(value) {
27
+ return type.isInt(value) && value > 0 && value <= MAX_VIEWPORT_SIZE;
28
+ }
29
+
30
+ // Checks the viewport a headless page is opened with: width and height in CSS
31
+ // pixels, and the colour scheme the page's `prefers-color-scheme` reports.
32
+ // Returns an error message, or undefined when every given option is valid.
33
+ function validateViewport({ width, height, colorScheme }) {
34
+ if (!type.isUndefined(width) && !isViewportSize(width)) {
35
+ return `Viewport width must be a positive integer of at most ${MAX_VIEWPORT_SIZE} (CSS pixels). Received ${JSON.stringify(
36
+ width
37
+ )}.`;
38
+ }
39
+ if (!type.isUndefined(height) && !isViewportSize(height)) {
40
+ return `Viewport height must be a positive integer of at most ${MAX_VIEWPORT_SIZE} (CSS pixels). Received ${JSON.stringify(
41
+ height
42
+ )}.`;
43
+ }
44
+ if (!type.isUndefined(colorScheme) && !COLOR_SCHEMES.includes(colorScheme)) {
45
+ return `Color scheme must be "light" or "dark". Received ${JSON.stringify(colorScheme)}.`;
46
+ }
47
+ return undefined;
48
+ }
49
+
50
+ export { COLOR_SCHEMES, MAX_VIEWPORT_SIZE };
51
+ export default validateViewport;
@@ -0,0 +1,54 @@
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 validateViewport from './validateViewport.js';
18
+
19
+ test('validateViewport accepts no options and every valid option', () => {
20
+ expect(validateViewport({})).toBeUndefined();
21
+ expect(validateViewport({ width: 390, height: 844, colorScheme: 'dark' })).toBeUndefined();
22
+ expect(validateViewport({ colorScheme: 'light' })).toBeUndefined();
23
+ expect(validateViewport({ width: 4096, height: 4096 })).toBeUndefined();
24
+ });
25
+
26
+ test.each([
27
+ [
28
+ { width: 0 },
29
+ 'Viewport width must be a positive integer of at most 4096 (CSS pixels). Received 0.',
30
+ ],
31
+ [
32
+ { width: 390.5 },
33
+ 'Viewport width must be a positive integer of at most 4096 (CSS pixels). Received 390.5.',
34
+ ],
35
+ [
36
+ { height: -1 },
37
+ 'Viewport height must be a positive integer of at most 4096 (CSS pixels). Received -1.',
38
+ ],
39
+ [
40
+ { height: '800' },
41
+ 'Viewport height must be a positive integer of at most 4096 (CSS pixels). Received "800".',
42
+ ],
43
+ [
44
+ { width: 40000 },
45
+ 'Viewport width must be a positive integer of at most 4096 (CSS pixels). Received 40000.',
46
+ ],
47
+ [
48
+ { height: 4097 },
49
+ 'Viewport height must be a positive integer of at most 4096 (CSS pixels). Received 4097.',
50
+ ],
51
+ [{ colorScheme: 'dim' }, 'Color scheme must be "light" or "dark". Received "dim".'],
52
+ ])('validateViewport rejects %j', (options, message) => {
53
+ expect(validateViewport(options)).toEqual(message);
54
+ });