@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
@@ -25,6 +25,18 @@ import { jest } from '@jest/globals';
25
25
  // browser flow is untouched everywhere else.
26
26
  jest.unstable_mockModule('playwright-core', () => ({
27
27
  chromium: { launch: jest.fn().mockRejectedValue(new Error("Executable doesn't exist")) },
28
+ selectors: { register: jest.fn(async () => {}) },
29
+ }));
30
+ // A missing shell would otherwise start a real download.
31
+ jest.unstable_mockModule('./installHeadlessShell.js', () => ({ default: () => null }));
32
+ // A data set caller loads a data set into the dev server's memory store; both
33
+ // are mocked so the test checks who the page opens as and on which database.
34
+ const mockReadDataSet = jest.fn();
35
+ const mockOpenDataSession = jest.fn();
36
+ jest.unstable_mockModule('./dataSets/readDataSet.js', () => ({ default: mockReadDataSet }));
37
+ jest.unstable_mockModule('./dataSets/getDataStore.js', () => ({ default: async () => ({}) }));
38
+ jest.unstable_mockModule('./dataSets/openDataSession.js', () => ({
39
+ default: mockOpenDataSession,
28
40
  }));
29
41
 
30
42
  // lib/build/config.js reads build/config.json from process.cwd() at import
@@ -34,6 +46,10 @@ const originalCwd = process.cwd();
34
46
  const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-screenshot-test-'));
35
47
  fs.mkdirSync(path.join(fixtureDir, 'build'), { recursive: true });
36
48
  fs.writeFileSync(path.join(fixtureDir, 'build', 'config.json'), JSON.stringify({ basePath: '' }));
49
+ fs.writeFileSync(
50
+ path.join(fixtureDir, 'build', 'routes.json'),
51
+ JSON.stringify([{ pageId: 'ticket', path: 'tickets/{space}/{ticket_id}' }])
52
+ );
37
53
  process.chdir(fixtureDir);
38
54
 
39
55
  const { default: screenshotPage } = await import('./screenshotPage.js');
@@ -55,5 +71,251 @@ test('screenshotPage returns an error when pageId is missing', async () => {
55
71
 
56
72
  test('screenshotPage returns an actionable error when no browser is available', async () => {
57
73
  const result = await screenshotPage({ origin: 'http://localhost:3001', pageId: 'home' });
58
- expect(result.error).toMatch(/No Chromium available. Run: npx playwright install chromium/);
74
+ expect(result.error).toMatch(
75
+ /No Chromium available. Run: npx playwright install chromium-headless-shell/
76
+ );
77
+ });
78
+
79
+ test('screenshotPage returns an error for an invalid viewport before launching a browser', async () => {
80
+ const { chromium } = await import('playwright-core');
81
+ chromium.launch.mockClear();
82
+ const width = await screenshotPage({ origin: 'http://localhost:3001', pageId: 'home', width: 0 });
83
+ expect(width.error).toEqual(
84
+ 'Viewport width must be a positive integer of at most 4096 (CSS pixels). Received 0.'
85
+ );
86
+ const scheme = await screenshotPage({
87
+ origin: 'http://localhost:3001',
88
+ pageId: 'home',
89
+ colorScheme: 'sepia',
90
+ });
91
+ expect(scheme.error).toEqual('Color scheme must be "light" or "dark". Received "sepia".');
92
+ expect(chromium.launch).not.toHaveBeenCalled();
93
+ });
94
+
95
+ test('screenshotPage rejects a bad urlQuery or malformed steps before launching a browser', async () => {
96
+ const { chromium } = await import('playwright-core');
97
+ chromium.launch.mockClear();
98
+ const query = await screenshotPage({
99
+ origin: 'http://localhost:3001',
100
+ pageId: 'home',
101
+ urlQuery: 'framework=popia',
102
+ });
103
+ expect(query.error).toMatch(/requires "urlQuery" to be an object/);
104
+ const steps = await screenshotPage({
105
+ origin: 'http://localhost:3001',
106
+ pageId: 'home',
107
+ steps: [{ hover: 'x' }],
108
+ });
109
+ expect(steps.error).toMatch(/Unknown journey step "hover"/);
110
+ expect(chromium.launch).not.toHaveBeenCalled();
111
+ });
112
+
113
+ test('screenshotPage opens the page at the urlQuery and captures after the steps ran', async () => {
114
+ const { chromium } = await import('playwright-core');
115
+ const calls = [];
116
+ const page = {
117
+ goto: jest.fn(async (url) => calls.push(`goto ${url}`)),
118
+ waitForFunction: jest.fn(async () => {}),
119
+ waitForTimeout: jest.fn(async () => {}),
120
+ screenshot: jest.fn(async () => {
121
+ calls.push('screenshot');
122
+ return Buffer.from('png');
123
+ }),
124
+ };
125
+ const context = {
126
+ addCookies: jest.fn(async () => {}),
127
+ newPage: jest.fn(async () => page),
128
+ close: jest.fn(async () => {}),
129
+ on: jest.fn(),
130
+ };
131
+ // getBrowser caches the browser while it reports connected; disconnecting
132
+ // it after the test makes the next test's launch take effect.
133
+ let connected = true;
134
+ chromium.launch.mockResolvedValue({
135
+ isConnected: () => connected,
136
+ newContext: async () => context,
137
+ });
138
+ try {
139
+ const result = await screenshotPage({
140
+ origin: 'http://localhost:3001',
141
+ pageId: 'gates',
142
+ urlQuery: { framework: 'popia', filter: { status: 'open' } },
143
+ });
144
+ expect(result.mimeType).toEqual('image/png');
145
+ expect(page.goto).toHaveBeenCalledTimes(1);
146
+ const url = new URL(page.goto.mock.calls[0][0]);
147
+ expect(url.pathname).toEqual('/gates');
148
+ expect(url.search).not.toEqual('');
149
+ expect(decodeURIComponent(url.search)).toContain('popia');
150
+ expect(context.close).toHaveBeenCalled();
151
+ } finally {
152
+ connected = false;
153
+ chromium.launch.mockRejectedValue(new Error("Executable doesn't exist"));
154
+ }
155
+ });
156
+
157
+ test('screenshotPage refuses steps that read email when the dev server captures no mail', async () => {
158
+ const { chromium } = await import('playwright-core');
159
+ chromium.launch.mockClear();
160
+ const result = await screenshotPage({
161
+ origin: 'http://localhost:3001',
162
+ pageId: 'home',
163
+ steps: [{ email: { to: 'a@example.com' } }],
164
+ });
165
+ expect(result.error).toMatch(/captures no mail/);
166
+ expect(chromium.launch).not.toHaveBeenCalled();
167
+ });
168
+
169
+ test('screenshotPage keeps the colour scheme and returns screenshot step captures when steps run', async () => {
170
+ const { chromium } = await import('playwright-core');
171
+ let captures = 0;
172
+ const page = {
173
+ goto: jest.fn(async () => {}),
174
+ waitForFunction: jest.fn(async () => {}),
175
+ waitForTimeout: jest.fn(async () => {}),
176
+ screenshot: jest.fn(async () => {
177
+ captures += 1;
178
+ return Buffer.from(`png-${captures}`);
179
+ }),
180
+ };
181
+ const context = {
182
+ addCookies: jest.fn(async () => {}),
183
+ // A journey's network counter listens for the context's requests.
184
+ on: jest.fn(),
185
+ // Its pages report what they ran through the journey observer binding.
186
+ exposeBinding: jest.fn(async () => {}),
187
+ newPage: jest.fn(async () => page),
188
+ close: jest.fn(async () => {}),
189
+ };
190
+ const newContext = jest.fn(async () => context);
191
+ let connected = true;
192
+ chromium.launch.mockResolvedValue({ isConnected: () => connected, newContext });
193
+ try {
194
+ const result = await screenshotPage({
195
+ origin: 'http://localhost:3001',
196
+ pageId: 'home',
197
+ colorScheme: 'dark',
198
+ steps: [{ screenshot: 'before' }],
199
+ });
200
+ expect(newContext).toHaveBeenCalledWith(expect.objectContaining({ colorScheme: 'dark' }));
201
+ expect(result.screenshots).toEqual([
202
+ { name: 'before', data: Buffer.from('png-1').toString('base64'), mimeType: 'image/png' },
203
+ ]);
204
+ expect(result.data).toEqual(Buffer.from('png-2').toString('base64'));
205
+ expect(context.close).toHaveBeenCalled();
206
+ } finally {
207
+ connected = false;
208
+ chromium.launch.mockRejectedValue(new Error("Executable doesn't exist"));
209
+ }
210
+ });
211
+
212
+ test('screenshotPage returns the error naming a missing placeholder before opening a browser', async () => {
213
+ const result = await screenshotPage({
214
+ origin: 'http://localhost:3001',
215
+ pageId: 'ticket',
216
+ pathParams: { space: 's' },
217
+ });
218
+ expect(result.invalidInput).toBe(true);
219
+ expect(result.error).toMatch(/missing a value for path placeholder "ticket_id"/);
220
+ });
221
+
222
+ test('screenshotPage refuses a data set user name without data before launching a browser', async () => {
223
+ const { chromium } = await import('playwright-core');
224
+ chromium.launch.mockClear();
225
+ const result = await screenshotPage({
226
+ origin: 'http://localhost:3001',
227
+ pageId: 'home',
228
+ user: 'member',
229
+ });
230
+ expect(result).toEqual({
231
+ error:
232
+ 'The call\'s user "member" names a data set user, but the call has no "data". Add data: <data set name>, or give user as an object.',
233
+ invalidInput: true,
234
+ });
235
+ expect(chromium.launch).not.toHaveBeenCalled();
236
+ });
237
+
238
+ test("screenshotPage renders as a data set user, on the data set's own database", async () => {
239
+ const { chromium } = await import('playwright-core');
240
+ const member = { id: 'u_member', roles: ['member'] };
241
+ mockReadDataSet.mockResolvedValue({ name: 'crm', users: { member } });
242
+ const close = jest.fn(async () => {});
243
+ mockOpenDataSession.mockResolvedValue({ cookie: 'session1', session: { id: 'session1' }, close });
244
+ const page = {
245
+ goto: jest.fn(async () => {}),
246
+ waitForFunction: jest.fn(async () => {}),
247
+ waitForTimeout: jest.fn(async () => {}),
248
+ screenshot: jest.fn(async () => Buffer.from('png')),
249
+ };
250
+ const cookies = [];
251
+ const context = {
252
+ addCookies: jest.fn(async (added) => cookies.push(...added)),
253
+ route: jest.fn(async () => {}),
254
+ newPage: jest.fn(async () => page),
255
+ close: jest.fn(async () => {}),
256
+ on: jest.fn(),
257
+ };
258
+ let connected = true;
259
+ chromium.launch.mockResolvedValue({
260
+ isConnected: () => connected,
261
+ newContext: async () => context,
262
+ });
263
+ try {
264
+ const result = await screenshotPage({
265
+ origin: 'http://localhost:3001',
266
+ pageId: 'home',
267
+ user: 'member',
268
+ data: 'crm',
269
+ });
270
+ expect(result.mimeType).toEqual('image/png');
271
+ expect(mockReadDataSet).toHaveBeenCalledWith(expect.objectContaining({ name: 'crm' }));
272
+ expect(mockOpenDataSession).toHaveBeenCalledWith({
273
+ dataSet: { name: 'crm', users: { member } },
274
+ });
275
+ const headless = cookies.find((cookie) => cookie.name === 'lowdefy_headless_user');
276
+ const caller = JSON.parse(Buffer.from(headless.value, 'base64').toString());
277
+ expect(caller.explicit).toBe(true);
278
+ expect(caller.user).toMatchObject({ id: 'u_member', roles: ['member'] });
279
+ expect(cookies.map((cookie) => cookie.name)).toContain('lowdefy_journey_data');
280
+ expect(close).toHaveBeenCalledTimes(1);
281
+ } finally {
282
+ connected = false;
283
+ chromium.launch.mockRejectedValue(new Error("Executable doesn't exist"));
284
+ }
285
+ });
286
+
287
+ test('screenshotPage opens the page signed out for user "none"', async () => {
288
+ const { chromium } = await import('playwright-core');
289
+ mockOpenDataSession.mockClear();
290
+ const page = {
291
+ goto: jest.fn(async () => {}),
292
+ waitForFunction: jest.fn(async () => {}),
293
+ waitForTimeout: jest.fn(async () => {}),
294
+ screenshot: jest.fn(async () => Buffer.from('png')),
295
+ };
296
+ const cookies = [];
297
+ const context = {
298
+ addCookies: jest.fn(async (added) => cookies.push(...added)),
299
+ newPage: jest.fn(async () => page),
300
+ close: jest.fn(async () => {}),
301
+ on: jest.fn(),
302
+ };
303
+ let connected = true;
304
+ chromium.launch.mockResolvedValue({
305
+ isConnected: () => connected,
306
+ newContext: async () => context,
307
+ });
308
+ try {
309
+ const result = await screenshotPage({
310
+ origin: 'http://localhost:3001',
311
+ pageId: 'home',
312
+ user: 'none',
313
+ });
314
+ expect(result.mimeType).toEqual('image/png');
315
+ expect(cookies.map((cookie) => cookie.name)).not.toContain('lowdefy_headless_user');
316
+ expect(mockOpenDataSession).not.toHaveBeenCalled();
317
+ } finally {
318
+ connected = false;
319
+ chromium.launch.mockRejectedValue(new Error("Executable doesn't exist"));
320
+ }
59
321
  });
@@ -37,6 +37,10 @@ const mockOpenPage = jest.fn(async () => ({
37
37
  }));
38
38
  const mockGetBrowser = jest.fn(async () => ({}));
39
39
 
40
+ // The route table is a build artifact these tests have none of: every page is served at its id.
41
+ jest.unstable_mockModule('./resolvePageInstance.js', () => ({
42
+ default: ({ pageId }) => ({ path: pageId, instanceKey: `page:${pageId}` }),
43
+ }));
40
44
  jest.unstable_mockModule('./getBrowser.js', () => ({
41
45
  getBrowser: mockGetBrowser,
42
46
  openPage: mockOpenPage,
@@ -14,54 +14,106 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import fs from 'node:fs';
18
- import path from 'node:path';
19
17
  import { type } from '@lowdefy/helpers';
20
18
 
21
- import getDocsManifest from './getDocsManifest.js';
19
+ import docSources from './docSources.js';
20
+ import getDocsIndex from './getDocsIndex.js';
21
+ import readDocEntry from './readDocEntry.js';
22
+ import splitSearchTerms from './splitSearchTerms.js';
22
23
 
23
24
  const MAX_RESULTS = 20;
24
25
  const SNIPPET_RADIUS = 120;
26
+ const TITLE_WEIGHT = 3;
27
+ const TYPE_NAME_WEIGHT = 2;
28
+ const BODY_WEIGHT = 1;
25
29
 
26
- let contentCache = null;
30
+ // Each term counts once, in the best field it appears in.
31
+ function scoreEntry({ entry, content, terms }) {
32
+ const title = entry.title.toLowerCase();
33
+ const slug = entry.slug.toLowerCase();
34
+ const typeName = (entry.typeName ?? '').toLowerCase();
35
+ const body = content.toLowerCase();
36
+ let score = 0;
37
+ let titleMatched = false;
38
+ let bodyMatch = null;
39
+ for (const term of terms) {
40
+ if (title.includes(term) || slug.includes(term)) {
41
+ score += TITLE_WEIGHT;
42
+ titleMatched = true;
43
+ continue;
44
+ }
45
+ if (typeName.includes(term)) {
46
+ score += TYPE_NAME_WEIGHT;
47
+ continue;
48
+ }
49
+ const index = body.indexOf(term);
50
+ if (index !== -1) {
51
+ score += BODY_WEIGHT;
52
+ bodyMatch = bodyMatch ?? { index, term };
53
+ }
54
+ }
55
+ return { score, titleMatched, bodyMatch };
56
+ }
27
57
 
28
- function getContents({ manifest }) {
29
- if (contentCache !== null) {
30
- return contentCache;
58
+ function makeSnippet({ content, titleMatched, bodyMatch }) {
59
+ if (titleMatched || type.isNone(bodyMatch)) {
60
+ return content.slice(0, SNIPPET_RADIUS * 2);
31
61
  }
32
- contentCache = manifest.docs.map((doc) => ({
33
- doc,
34
- content: fs.readFileSync(path.join(manifest.contentDir, doc.path), 'utf8'),
35
- }));
36
- return contentCache;
62
+ const start = Math.max(0, bodyMatch.index - SNIPPET_RADIUS);
63
+ return content.slice(start, bodyMatch.index + bodyMatch.term.length + SNIPPET_RADIUS);
37
64
  }
38
65
 
39
- function searchDocs({ query }) {
66
+ function makeHit({ entry, snippet }) {
67
+ const hit = { slug: entry.slug, title: entry.title, section: entry.section };
68
+ if (!type.isNone(entry.kind)) {
69
+ hit.kind = entry.kind;
70
+ }
71
+ if (!type.isNone(entry.typeName)) {
72
+ hit.typeName = entry.typeName;
73
+ }
74
+ hit.source = entry.source;
75
+ hit.package = entry.package;
76
+ hit.version = entry.version;
77
+ hit.path = entry.path ?? entry.filePath;
78
+ hit.snippet = snippet;
79
+ return hit;
80
+ }
81
+
82
+ function searchDocs({ query, source }) {
40
83
  if (!type.isString(query) || query.trim() === '') {
41
84
  throw new Error('searchDocs requires a "query" string.');
42
85
  }
43
- const manifest = getDocsManifest();
44
- if (type.isNone(manifest)) {
45
- return [];
86
+ if (!type.isNone(source) && !docSources.includes(source)) {
87
+ throw new Error(
88
+ `Unknown docs source. Received ${JSON.stringify(source)}. Use one of: ${docSources.join(
89
+ ', '
90
+ )}.`
91
+ );
46
92
  }
47
- const lowerQuery = query.trim().toLowerCase();
48
- const titleMatches = [];
49
- const contentMatches = [];
50
- for (const { doc, content } of getContents({ manifest })) {
51
- if (doc.title.toLowerCase().includes(lowerQuery) || doc.slug.includes(lowerQuery)) {
52
- titleMatches.push({ ...doc, snippet: content.slice(0, SNIPPET_RADIUS * 2) });
93
+ let terms = splitSearchTerms({ query });
94
+ // A query of only short or common words, such as "to a", is searched whole.
95
+ if (terms.length === 0) {
96
+ terms = [query.trim().toLowerCase()];
97
+ }
98
+ const { entries } = getDocsIndex();
99
+ const scored = [];
100
+ for (const entry of entries) {
101
+ if (!type.isNone(source) && entry.source !== source) {
53
102
  continue;
54
103
  }
55
- const index = content.toLowerCase().indexOf(lowerQuery);
56
- if (index !== -1) {
57
- const start = Math.max(0, index - SNIPPET_RADIUS);
58
- contentMatches.push({
59
- ...doc,
60
- snippet: content.slice(start, index + lowerQuery.length + SNIPPET_RADIUS),
61
- });
104
+ const content = readDocEntry({ entry });
105
+ const { score, titleMatched, bodyMatch } = scoreEntry({ entry, content, terms });
106
+ if (score === 0) {
107
+ continue;
62
108
  }
109
+ scored.push({
110
+ score,
111
+ hit: makeHit({ entry, snippet: makeSnippet({ content, titleMatched, bodyMatch }) }),
112
+ });
63
113
  }
64
- return [...titleMatches, ...contentMatches].slice(0, MAX_RESULTS);
114
+ // Array sort is stable, so entries with equal scores keep index order.
115
+ scored.sort((a, b) => b.score - a.score);
116
+ return scored.slice(0, MAX_RESULTS).map(({ hit }) => hit);
65
117
  }
66
118
 
67
119
  export default searchDocs;
@@ -0,0 +1,100 @@
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 fs from 'fs';
18
+ import path from 'path';
19
+
20
+ import getDocsManifest from './getDocsManifest.js';
21
+ import searchDocs from './searchDocs.js';
22
+
23
+ // The order the substring search gave before terms: title or slug matches,
24
+ // then body matches, each in manifest order.
25
+ function substringOrder(query) {
26
+ const manifest = getDocsManifest();
27
+ const lowerQuery = query.toLowerCase();
28
+ const titleMatches = [];
29
+ const bodyMatches = [];
30
+ for (const doc of manifest.docs) {
31
+ const content = fs.readFileSync(path.join(manifest.contentDir, doc.path), 'utf8');
32
+ if (doc.title.toLowerCase().includes(lowerQuery) || doc.slug.includes(lowerQuery)) {
33
+ titleMatches.push(doc.slug);
34
+ } else if (content.toLowerCase().includes(lowerQuery)) {
35
+ bodyMatches.push(doc.slug);
36
+ }
37
+ }
38
+ return [...titleMatches, ...bodyMatches].slice(0, 20);
39
+ }
40
+
41
+ test('searchDocs finds a page from a multi-word query', () => {
42
+ const results = searchDocs({ query: 'Link action pathParams path parameters' });
43
+ expect(results[0].slug).toEqual('actions/link');
44
+ });
45
+
46
+ test('searchDocs ranks entries matching more terms first', () => {
47
+ const results = searchDocs({ query: 'MongoDB aggregation pipeline' });
48
+ expect(results[0].slug).toEqual('connections/mongodb');
49
+ });
50
+
51
+ test('searchDocs keeps the substring order for a single-term query', () => {
52
+ for (const query of ['SetState', 'modal', 'pathParams']) {
53
+ expect(searchDocs({ query }).map((hit) => hit.slug)).toEqual(substringOrder(query));
54
+ }
55
+ });
56
+
57
+ test('searchDocs leaves out entries matching no term', () => {
58
+ expect(searchDocs({ query: 'zzqqxx wwvvyy' })).toEqual([]);
59
+ });
60
+
61
+ test('searchDocs searches the whole query when every term is dropped', () => {
62
+ const results = searchDocs({ query: 'to a' });
63
+ expect(results.length).toBeGreaterThan(0);
64
+ expect(results.map((hit) => hit.slug)).toEqual(substringOrder('to a'));
65
+ });
66
+
67
+ test('searchDocs hits name their source, package and version', () => {
68
+ const [hit] = searchDocs({ query: 'SetState' });
69
+ expect(hit).toEqual(
70
+ expect.objectContaining({
71
+ slug: 'actions/setstate',
72
+ source: 'core',
73
+ package: '@lowdefy/docs-content',
74
+ version: getDocsManifest().version,
75
+ path: 'content/actions/setstate.md',
76
+ })
77
+ );
78
+ expect(hit.snippet.startsWith('# SetState')).toBe(true);
79
+ });
80
+
81
+ test('searchDocs snippet surrounds the first body match when no title matches', () => {
82
+ const results = searchDocs({ query: 'placeholders' });
83
+ const bodyHit = results.find((hit) => !hit.title.toLowerCase().includes('placeholders'));
84
+ expect(bodyHit.snippet.toLowerCase()).toContain('placeholders');
85
+ });
86
+
87
+ test('searchDocs source filter narrows hits to that source', () => {
88
+ expect(searchDocs({ query: 'SetState', source: 'core' }).length).toBeGreaterThan(0);
89
+ expect(searchDocs({ query: 'SetState', source: 'module' })).toEqual([]);
90
+ });
91
+
92
+ test('searchDocs throws on an unknown source, naming the sources', () => {
93
+ expect(() => searchDocs({ query: 'SetState', source: 'blog' })).toThrow(
94
+ 'Unknown docs source. Received "blog". Use one of: core, plugin, local-plugin, module.'
95
+ );
96
+ });
97
+
98
+ test('searchDocs throws on an empty query', () => {
99
+ expect(() => searchDocs({ query: ' ' })).toThrow('requires a "query" string');
100
+ });
@@ -0,0 +1,90 @@
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 { defaultIconAliases, getLucideIconNames, loadIconSets } from '@lowdefy/build/dev';
19
+
20
+ import readBuildArtifact from './readBuildArtifact.js';
21
+
22
+ const DEFAULT_LIMIT = 30;
23
+
24
+ let setNamesCache = { key: null, names: [] };
25
+
26
+ // Installed icon sets are read through the loader the build uses, so search
27
+ // and the build agree. Icons a plugin adds to "lucide" are listed unqualified,
28
+ // like Lucide's own; every other set's names are listed qualified, which
29
+ // always resolve. Cached until the plugin list in the build changes.
30
+ async function getSetNames() {
31
+ const iconSets = readBuildArtifact({ name: 'customTypesMap.json' })?.iconSets ?? {};
32
+ const key = JSON.stringify(iconSets);
33
+ if (setNamesCache.key === key) {
34
+ return setNamesCache.names;
35
+ }
36
+ const sets = await loadIconSets({
37
+ context: { directories: { server: process.cwd() }, typesMap: { iconSets } },
38
+ });
39
+ const lucideNames = new Set(Object.values(getLucideIconNames()).flat());
40
+ const names = [];
41
+ Object.entries(sets).forEach(([setId, layers]) => {
42
+ const setNames = new Set(layers.flatMap((layer) => [...layer.names]));
43
+ [...setNames].sort().forEach((name) => {
44
+ if (setId !== 'lucide') {
45
+ names.push(`${setId}:${name}`);
46
+ } else if (!lucideNames.has(name)) {
47
+ names.push(name);
48
+ }
49
+ });
50
+ });
51
+ setNamesCache = { key, names };
52
+ return names;
53
+ }
54
+
55
+ function matchesWords(text, words) {
56
+ const lowerText = text.toLowerCase();
57
+ return words.every((word) => lowerText.includes(word));
58
+ }
59
+
60
+ async function searchIcons({ query, limit = DEFAULT_LIMIT }) {
61
+ if (!type.isString(query) || query.trim() === '') {
62
+ throw new Error('searchIcons requires a "query" string.');
63
+ }
64
+ const words = query
65
+ .trim()
66
+ .toLowerCase()
67
+ .split(/[\s-]+/);
68
+ // The dev build writes the full semantic map (built-in, the default icon
69
+ // set's and theme.icons.aliases). Before the first build only the built-in
70
+ // names are known.
71
+ const semantic = readBuildArtifact({ name: 'iconAliases.json' }) ?? defaultIconAliases;
72
+
73
+ const semanticResults = Object.entries(semantic)
74
+ .filter(([name, icon]) => matchesWords(`${name} ${icon}`, words))
75
+ .map(([name, icon]) => ({ name, icon }));
76
+ // Canonical Lucide names rank before alias names (Home is an alias of House).
77
+ const { canonical, aliases } = getLucideIconNames();
78
+ const iconResults = [...canonical, ...aliases, ...(await getSetNames())]
79
+ .filter((name) => matchesWords(name, words))
80
+ .slice(0, limit);
81
+
82
+ return {
83
+ semantic: semanticResults,
84
+ icons: iconResults,
85
+ usage:
86
+ 'Use a semantic name from "semantic" (icon: edit, or <i data-icon="edit"></i> in HTML). Otherwise use a Lucide name from "icons" in PascalCase (icon: Receipt). Never invent a name. Qualified names (set:Name) come from installed icon set plugins. Add app-specific semantic names under theme.icons.aliases in lowdefy.yaml.',
87
+ };
88
+ }
89
+
90
+ export default searchIcons;