@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.
- package/client/App.jsx +52 -24
- package/client/Inspector.jsx +73 -30
- package/client/JourneyObserver.jsx +47 -0
- package/client/Page.jsx +54 -22
- package/client/Recorder.jsx +73 -0
- package/client/Routing.jsx +37 -13
- package/client/clientBuildImports.test.mjs +2 -0
- package/client/devStreamConnections.test.mjs +40 -0
- package/client/feedback/FeedbackMount.jsx +2 -2
- package/client/feedback/FeedbackOverlay.jsx +7 -4
- package/client/feedback/feedbackStyles.js +3 -0
- package/client/journeyObserver/startJourneyObserver.js +101 -0
- package/client/journeyObserver/startJourneyObserver.test.mjs +149 -0
- package/client/openInEditor/OpenInEditorListener.jsx +2 -0
- package/client/recorder/buildEngineRecord.js +37 -0
- package/client/recorder/buildInteractionRecord.js +64 -0
- package/client/recorder/buildTraceEvent.js +57 -0
- package/client/recorder/buildTraceEvent.test.mjs +81 -0
- package/client/recorder/chunkRecords.js +69 -0
- package/client/recorder/chunkRecords.test.mjs +59 -0
- package/client/recorder/collectEventActions.js +30 -0
- package/client/recorder/collectEventEndpoints.js +40 -0
- package/client/recorder/collectEventRequests.js +39 -0
- package/client/recorder/createPairingBuffer.js +187 -0
- package/client/recorder/createPairingBuffer.test.mjs +405 -0
- package/client/recorder/createPasswordRedactor.js +84 -0
- package/client/recorder/createRecordBatcher.js +54 -0
- package/client/recorder/createRecordBatcher.test.mjs +76 -0
- package/client/recorder/createRecorder.js +216 -0
- package/client/recorder/createRecorder.test.mjs +318 -0
- package/client/recorder/createRecordingSession.js +61 -0
- package/client/recorder/createRecordingSession.test.mjs +72 -0
- package/client/recorder/diffStateWrites.js +77 -0
- package/client/recorder/diffStateWrites.test.mjs +66 -0
- package/client/recorder/isEventCall.js +30 -0
- package/client/recorder/jsonType.js +39 -0
- package/client/recorder/keyChord.js +47 -0
- package/client/recorder/keyChord.test.mjs +50 -0
- package/client/recorder/sendRecords.js +40 -0
- package/client/recorder/toJsonSafe.js +29 -0
- package/lib/client/BuildingPage.jsx +3 -1
- package/lib/client/ErrorBar.jsx +17 -13
- package/lib/client/RedirectingPage.jsx +56 -0
- package/lib/client/auth/Auth.jsx +11 -6
- package/lib/client/auth/AuthConfigured.jsx +222 -36
- package/lib/client/auth/AuthNotConfigured.js +45 -4
- package/lib/client/auth/authNotConfiguredParity.test.mjs +61 -0
- package/lib/client/auth/authTemplateSync.test.mjs +40 -0
- package/lib/client/auth/betterAuthSessionContract.test.mjs +181 -0
- package/lib/client/auth/createFetchCheckingBuild.js +45 -0
- package/lib/client/bundleBuildId.js +25 -0
- package/lib/client/collectVisibleBlockIds.js +51 -0
- package/lib/client/getCspNonce.js +25 -0
- package/lib/client/getPagePath.js +36 -0
- package/lib/client/getPagePath.test.mjs +51 -0
- package/lib/client/utils/formatErrorsForCopy.js +30 -0
- package/lib/client/utils/formatErrorsForCopy.test.mjs +39 -0
- package/lib/client/utils/getErrorBarColor.js +25 -0
- package/lib/client/utils/getErrorBarColor.test.mjs +34 -0
- package/lib/client/utils/request.js +8 -1
- package/lib/client/utils/usePageConfig.js +89 -25
- package/lib/client/utils/usePageConfig.test.mjs +198 -13
- package/lib/configTree/buildConfigTree.mjs +58 -0
- package/lib/configTree/buildConfigTree.test.mjs +77 -0
- package/lib/configTree/collectConfigText.mjs +101 -0
- package/lib/configTree/collectConfigText.test.mjs +116 -0
- package/lib/configTree/prepareScratchServer.mjs +46 -0
- package/lib/configTree/runConfigTreeBuild.mjs +88 -0
- package/lib/configTree/runConfigTreeBuild.skipPluginChecks.test.mjs +74 -0
- package/lib/configTree/runConfigTreeBuild.test.mjs +202 -0
- package/lib/docs/JourneyStepError.js +30 -0
- package/lib/docs/acquireBrowserSlot.js +30 -0
- package/lib/docs/addNestedEndpoints.js +54 -0
- package/lib/docs/addNestedEndpoints.test.mjs +185 -0
- package/lib/docs/addWriteFlags.js +77 -0
- package/lib/docs/buildEditedPages.js +50 -0
- package/lib/docs/buildWaitHeader.js +21 -0
- package/lib/docs/captureAnnotatedScreenshot.js +23 -12
- package/lib/docs/checkAgentFiles.js +148 -0
- package/lib/docs/checkAgentFiles.test.mjs +225 -0
- package/lib/docs/checkStepReferences.js +239 -0
- package/lib/docs/checkStepReferences.test.mjs +173 -0
- package/lib/docs/checkpointStore.js +3 -0
- package/lib/docs/clientErrorStore.js +18 -1
- package/lib/docs/clientErrorStore.test.mjs +46 -0
- package/lib/docs/collectExercised.js +52 -0
- package/lib/docs/createBrowserLifecycle.js +102 -0
- package/lib/docs/createBrowserLifecycle.test.mjs +212 -0
- package/lib/docs/createDocsMcpServer.js +345 -388
- package/lib/docs/createDocsMcpServer.test.mjs +644 -4
- package/lib/docs/createDocsMcpServerRunJourney.test.mjs +148 -0
- package/lib/docs/createJourneyActors.chromium.test.mjs +227 -0
- package/lib/docs/createJourneyActors.js +213 -0
- package/lib/docs/createJourneyActors.test.mjs +79 -0
- package/lib/docs/createJourneyObservations.js +80 -0
- package/lib/docs/createLeftOriginError.js +26 -0
- package/lib/docs/createNetworkCounter.js +101 -0
- package/lib/docs/dataSets/checkSharedCollectionIds.js +78 -0
- package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
- package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +727 -0
- package/lib/docs/dataSets/describeDataSetResult.js +25 -0
- package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
- package/lib/docs/dataSets/describeTargetKey.js +29 -0
- package/lib/docs/dataSets/deserializeFixture.js +34 -0
- package/lib/docs/dataSets/drainSessionWork.js +37 -0
- package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
- package/lib/docs/dataSets/duplicateIndexName.js +24 -0
- package/lib/docs/dataSets/getDataStore.js +106 -0
- package/lib/docs/dataSets/getDataStore.mongodb.test.mjs +63 -0
- package/lib/docs/dataSets/getDataStore.test.mjs +195 -0
- package/lib/docs/dataSets/groupDataSetByCollection.js +52 -0
- package/lib/docs/dataSets/insertFixture.js +57 -0
- package/lib/docs/dataSets/insertGeneratedDocuments.js +41 -0
- package/lib/docs/dataSets/isDataStoreGone.js +29 -0
- package/lib/docs/dataSets/loadDataSetCollection.js +42 -0
- package/lib/docs/dataSets/mergeCollectionIndexes.js +52 -0
- package/lib/docs/dataSets/mergeCollectionIndexes.test.mjs +59 -0
- package/lib/docs/dataSets/openDataSession.js +133 -0
- package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +493 -0
- package/lib/docs/dataSets/pickDataStorePort.js +58 -0
- package/lib/docs/dataSets/pickDataStorePort.test.mjs +63 -0
- package/lib/docs/dataSets/readConnectionArtifacts.js +46 -0
- package/lib/docs/dataSets/readDataSession.js +41 -0
- package/lib/docs/dataSets/readDataSession.test.mjs +57 -0
- package/lib/docs/dataSets/readDataSet.js +83 -0
- package/lib/docs/dataSets/readDataSet.test.mjs +251 -0
- package/lib/docs/dataSets/registerDataStoreShutdown.js +47 -0
- package/lib/docs/dataSets/removeDataStoreFiles.js +32 -0
- package/lib/docs/dataSets/resolveDataSetCollection.js +46 -0
- package/lib/docs/dataSets/resolveJourneyDataSet.js +106 -0
- package/lib/docs/dataSets/resolveJourneyDataSet.test.mjs +130 -0
- package/lib/docs/dataSets/sessionDatabaseName.js +32 -0
- package/lib/docs/dataSets/sweepOrphanDatabases.js +36 -0
- package/lib/docs/dataSets/test/app/lowdefy.yaml +225 -0
- package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
- package/lib/docs/dataSets/test/app/tests/data/shop.yaml +12 -0
- package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
- package/lib/docs/devEventBus.js +136 -0
- package/lib/docs/devEventBus.test.mjs +269 -0
- package/lib/docs/devToolDefinitions.js +529 -0
- package/lib/docs/docSources.js +21 -0
- package/lib/docs/docs.test.mjs +171 -15
- package/lib/docs/docsIndex.test.mjs +377 -0
- package/lib/docs/evalOperator.js +5 -3
- package/lib/docs/evalOperatorHeadless.js +84 -12
- package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
- package/lib/docs/evalOperatorInTab.js +2 -1
- package/lib/docs/findConfig.js +95 -49
- package/lib/docs/fitResponse.js +81 -0
- package/lib/docs/fitResponse.test.mjs +128 -0
- package/lib/docs/formatFeedback.js +26 -5
- package/lib/docs/formatFeedback.test.mjs +48 -0
- package/lib/docs/getAppMap.js +7 -1
- package/lib/docs/getBrowser.js +150 -80
- package/lib/docs/getBrowser.test.mjs +407 -16
- package/lib/docs/getBuildId.js +61 -0
- package/lib/docs/getBuildId.test.mjs +84 -0
- package/lib/docs/getBuildStatus.js +81 -7
- package/lib/docs/getBuildStatus.test.mjs +229 -0
- package/lib/docs/getBuildStatusAfterEdits.js +35 -0
- package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
- package/lib/docs/getCoreDoc.js +9 -1
- package/lib/docs/getDoc.js +100 -0
- package/lib/docs/getDocsIndex.js +152 -0
- package/lib/docs/getHazards.js +115 -0
- package/lib/docs/getHazards.test.mjs +175 -0
- package/lib/docs/getMailOutboxDirectory.js +26 -0
- package/lib/docs/getOverview.js +79 -13
- package/lib/docs/getOverviewBasePath.test.mjs +37 -0
- package/lib/docs/getPageBuildStatus.js +46 -0
- package/lib/docs/getPluginDoc.js +21 -23
- package/lib/docs/getSchema.js +33 -1
- package/lib/docs/getStaleStatus.js +36 -0
- package/lib/docs/getStaleStatus.test.mjs +74 -0
- package/lib/docs/guardJourneyOrigin.js +39 -0
- package/lib/docs/inspectState.js +5 -3
- package/lib/docs/inspectStateFromTab.js +2 -2
- package/lib/docs/inspectStateHeadless.js +89 -27
- package/lib/docs/inspectStateHeadless.test.mjs +20 -1
- package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
- package/lib/docs/installHeadlessShell.js +123 -0
- package/lib/docs/installHeadlessShell.test.mjs +70 -0
- package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
- package/lib/docs/isHeadlessShellIncomplete.js +34 -0
- package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
- package/lib/docs/isPageReady.js +77 -0
- package/lib/docs/isPageReady.test.mjs +235 -0
- package/lib/docs/isRunErrorEntry.js +25 -0
- package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
- package/lib/docs/journeyRunRecording.js +35 -0
- package/lib/docs/journeyRunRecording.test.mjs +39 -0
- package/lib/docs/keepEventStreamAlive.js +68 -0
- package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
- package/lib/docs/launchBrowser.js +83 -0
- package/lib/docs/launchBrowser.test.mjs +175 -0
- package/lib/docs/listModuleDocEntries.js +69 -0
- package/lib/docs/listPackageDocFiles.js +67 -0
- package/lib/docs/listPluginDocEntries.js +111 -0
- package/lib/docs/listPlugins.js +9 -58
- package/lib/docs/listTypes.js +14 -6
- package/lib/docs/loadFrameworkHazards.js +40 -0
- package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
- package/lib/docs/loadState.js +103 -35
- package/lib/docs/makeModuleManifestDoc.js +174 -0
- package/lib/docs/matchExercisedPages.js +35 -0
- package/lib/docs/matchJourneyRoute.js +69 -0
- package/lib/docs/matchJourneyRoute.test.mjs +178 -0
- package/lib/docs/mergeNetworkSnapshots.js +49 -0
- package/lib/docs/nextJourneySequence.js +30 -0
- package/lib/docs/noBrowserError.js +22 -0
- package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
- package/lib/docs/observe/claimExpectedErrors.js +67 -0
- package/lib/docs/observe/createJourneyAppErrors.js +236 -0
- package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
- package/lib/docs/observe/evaluateInvariants.js +183 -0
- package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
- package/lib/docs/observe/findingKey.js +29 -0
- package/lib/docs/observe/hasNoEffect.js +31 -0
- package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
- package/lib/docs/observe/installStepObserver.js +72 -0
- package/lib/docs/observe/isAppApiUrl.js +33 -0
- package/lib/docs/observe/parseAppApiUrl.js +37 -0
- package/lib/docs/observe/readStepObserver.js +42 -0
- package/lib/docs/observe/relativeSource.js +29 -0
- package/lib/docs/observe/resolveConfigKeySource.js +33 -0
- package/lib/docs/observe/takeJourneyEvents.js +36 -0
- package/lib/docs/observe/toAppErrorFailure.js +61 -0
- package/lib/docs/observe/usesSearchStage.js +45 -0
- package/lib/docs/observe/waitForClientErrorReports.js +31 -0
- package/lib/docs/observe/watchJourneyContext.js +75 -0
- package/lib/docs/observe/withAppErrors.js +31 -0
- package/lib/docs/observe/withAppErrors.test.mjs +101 -0
- package/lib/docs/openJourney.js +94 -0
- package/lib/docs/openJourneyEmail.js +41 -0
- package/lib/docs/openJourneyEmail.test.mjs +187 -0
- package/lib/docs/pageBlocks.js +47 -0
- package/lib/docs/pageBuildStatus.test.mjs +103 -0
- package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
- package/lib/docs/rails/authEngineActions.js +53 -0
- package/lib/docs/rails/authEngineActions.test.mjs +69 -0
- package/lib/docs/rails/authEngineMethods.js +52 -0
- package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
- package/lib/docs/rails/checkAuthActionsRan.js +66 -0
- package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
- package/lib/docs/rails/checkClickRails.js +56 -0
- package/lib/docs/rails/createDataSetRails.js +82 -0
- package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
- package/lib/docs/rails/findExternalBlocks.js +147 -0
- package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
- package/lib/docs/rails/installAuthActionRecorder.js +56 -0
- package/lib/docs/rails/readAuthActionRecorder.js +32 -0
- package/lib/docs/rails/readBuiltArtifact.js +30 -0
- package/lib/docs/readConnectionPlugins.js +26 -0
- package/lib/docs/readDevAuthMode.js +32 -0
- package/lib/docs/readDocEntry.js +40 -0
- package/lib/docs/readDocTitle.js +26 -0
- package/lib/docs/readJourneyEmailMatch.js +57 -0
- package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
- package/lib/docs/readJourneySession.js +56 -0
- package/lib/docs/readMailOutbox.js +37 -0
- package/lib/docs/readPagePath.js +27 -0
- package/lib/docs/readPluginPackages.js +99 -0
- package/lib/docs/readProxyBuildWait.js +38 -0
- package/lib/docs/readRoutineSteps.js +45 -0
- package/lib/docs/registerShownTextEngine.js +118 -0
- package/lib/docs/requestRestart.js +30 -0
- package/lib/docs/requestRestart.test.mjs +49 -0
- package/lib/docs/resolvePageInstance.js +46 -0
- package/lib/docs/resolvePageInstance.test.mjs +81 -0
- package/lib/docs/resolveSource.js +11 -7
- package/lib/docs/resolveSource.test.mjs +1 -1
- package/lib/docs/resolveToolCaller.js +62 -0
- package/lib/docs/reviewPage.js +51 -0
- package/lib/docs/reviewPageBuilds.js +56 -0
- package/lib/docs/reviewPageBuilds.test.mjs +207 -0
- package/lib/docs/runCheck.js +75 -0
- package/lib/docs/runCheck.test.mjs +91 -0
- package/lib/docs/runCheckChild.js +49 -0
- package/lib/docs/runEndpoint.js +185 -0
- package/lib/docs/runEndpoint.test.mjs +509 -0
- package/lib/docs/runErrorBuffers.js +99 -0
- package/lib/docs/runErrorBuffers.test.mjs +95 -0
- package/lib/docs/runJourney.js +347 -0
- package/lib/docs/runJourney.test.mjs +2383 -0
- package/lib/docs/runJourneyAsUsers.js +44 -0
- package/lib/docs/runJourneyDataSet.test.mjs +274 -0
- package/lib/docs/runJourneySteps.js +1320 -0
- package/lib/docs/runRequest.js +100 -59
- package/lib/docs/runRequest.test.mjs +190 -0
- package/lib/docs/saveAnnotatedScreenshot.js +1 -1
- package/lib/docs/scaffoldPage.js +3 -1
- package/lib/docs/screenshotPage.js +163 -11
- package/lib/docs/screenshotPage.test.mjs +263 -1
- package/lib/docs/screenshotPageClip.test.mjs +4 -0
- package/lib/docs/searchDocs.js +82 -30
- package/lib/docs/searchDocs.test.mjs +100 -0
- package/lib/docs/searchIcons.js +90 -0
- package/lib/docs/searchIcons.test.mjs +127 -0
- package/lib/docs/selectExercised.js +39 -0
- package/lib/docs/selectFinalState.js +66 -0
- package/lib/docs/selectFinalState.test.mjs +51 -0
- package/lib/docs/serverErrorStore.js +54 -0
- package/lib/docs/serverErrorStore.test.mjs +50 -0
- package/lib/docs/setupTestFixtures.mjs +133 -39
- package/lib/docs/shownTextSelector.js +28 -0
- package/lib/docs/snapshotState.js +13 -5
- package/lib/docs/splitSearchTerms.js +62 -0
- package/lib/docs/splitSearchTerms.test.mjs +50 -0
- package/lib/docs/stateCheckpoints.test.mjs +77 -0
- package/lib/docs/tabAvailable.js +6 -5
- package/lib/docs/tabChannel.js +127 -21
- package/lib/docs/tabChannel.test.mjs +187 -29
- package/lib/docs/unsettledPageNote.js +24 -0
- package/lib/docs/validateJourneyMail.js +36 -0
- package/lib/docs/validateJourneyTimeout.js +38 -0
- package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
- package/lib/docs/validateRecordingParam.js +42 -0
- package/lib/docs/validateStateSelection.js +33 -0
- package/lib/docs/validateStateSelection.test.mjs +33 -0
- package/lib/docs/validateViewport.js +51 -0
- package/lib/docs/validateViewport.test.mjs +54 -0
- package/lib/docs/waitForBuild.js +50 -0
- package/lib/docs/waitForBuild.test.mjs +75 -0
- package/lib/docs/waitForJourneyEmail.js +77 -0
- package/lib/docs/withBrowserSlot.js +45 -0
- package/lib/docs/withBrowserSlot.test.mjs +75 -0
- package/lib/docs/withDataSession.js +53 -0
- package/lib/server/applyDataSetRedirect.js +92 -0
- package/lib/server/applyDataSetRedirect.test.mjs +446 -0
- package/lib/server/applyDataSetRedirectTenantReach.test.mjs +207 -0
- package/lib/server/applyDataSetRedirectWalled.test.mjs +134 -0
- package/lib/server/auth/createSystemContext.js +73 -0
- package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
- package/lib/server/auth/getHeadlessUser.js +9 -8
- package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
- package/lib/server/auth/getMockUser.js +18 -14
- package/lib/server/auth/{session.js → getStrategies.js} +17 -16
- package/lib/server/auth/headlessUser.js +3 -2
- package/lib/server/auth/journeyActor.js +35 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
- package/lib/server/checkPageRecord.js +44 -0
- package/lib/server/contextMapBudget.js +24 -0
- package/lib/server/contextMapBudget.test.mjs +98 -0
- package/lib/server/continueJitKeys.js +28 -0
- package/lib/server/continueJitKeys.test.mjs +43 -0
- package/lib/server/createDataSessionEndedError.js +33 -0
- package/lib/server/createDataSetRefusal.js +26 -0
- package/lib/server/createLowdefyContext.js +168 -41
- package/lib/server/createLowdefyContext.test.mjs +331 -8
- package/lib/server/getClientAddress.js +51 -0
- package/lib/server/getClientAddress.test.mjs +56 -0
- package/lib/server/getPageJitEnrichment.test.mjs +29 -11
- package/lib/server/hashConfigContent.js +26 -0
- package/lib/server/importAppCode.test.mjs +57 -0
- package/lib/server/isRoutineStep.js +27 -0
- package/lib/server/jitPageBuilder.js +309 -126
- package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
- package/lib/server/jitPageBuilder.test.mjs +218 -0
- package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
- package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
- package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
- package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
- package/lib/server/journeyCookies.js +22 -0
- package/lib/server/journeyCookies.test.mjs +145 -0
- package/lib/server/loadDynamicJsMap.js +47 -0
- package/lib/server/log/createHandleError.js +38 -2
- package/lib/server/log/createHandleError.test.mjs +125 -2
- package/lib/server/log/createLogger.js +3 -0
- package/lib/server/log/createLogger.test.mjs +15 -0
- package/lib/server/log/logRequest.js +2 -3
- package/lib/server/mutantDataSetWrappers.test.mjs +115 -0
- package/lib/server/mutants/applyMutant.js +39 -0
- package/lib/server/mutants/artifactPatterns.js +25 -0
- package/lib/server/mutants/buildAnchor.js +45 -0
- package/lib/server/mutants/collectSharedIds.js +41 -0
- package/lib/server/mutants/configPathSegments.js +38 -0
- package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
- package/lib/server/mutants/describeEvent.js +22 -0
- package/lib/server/mutants/enumerateArtifact.js +56 -0
- package/lib/server/mutants/findNodeByKey.js +45 -0
- package/lib/server/mutants/findNodeByPointer.js +49 -0
- package/lib/server/mutants/getArtifactKind.js +32 -0
- package/lib/server/mutants/groupMutantCopies.js +56 -0
- package/lib/server/mutants/hashArtifact.js +25 -0
- package/lib/server/mutants/hashMutantTarget.js +34 -0
- package/lib/server/mutants/listMutants.js +122 -0
- package/lib/server/mutants/listMutants.test.mjs +502 -0
- package/lib/server/mutants/mutantId.js +30 -0
- package/lib/server/mutants/mutantRuns.js +57 -0
- package/lib/server/mutants/mutantRuns.test.mjs +136 -0
- package/lib/server/mutants/operators/dropAction.js +47 -0
- package/lib/server/mutants/operators/dropBlock.js +44 -0
- package/lib/server/mutants/operators/dropPayload.js +52 -0
- package/lib/server/mutants/operators/dropStep.js +44 -0
- package/lib/server/mutants/operators/flipVisible.js +53 -0
- package/lib/server/mutants/operators/index.js +37 -0
- package/lib/server/mutants/operators/retargetLink.js +72 -0
- package/lib/server/mutants/operators/skipValidate.js +77 -0
- package/lib/server/mutants/operators/swapIf.js +112 -0
- package/lib/server/mutants/operators.test.mjs +396 -0
- package/lib/server/mutants/positionFreePath.js +38 -0
- package/lib/server/mutants/removeFromParent.js +28 -0
- package/lib/server/mutants/test/fixtures.mjs +173 -0
- package/lib/server/mutants/test/keyArtifact.mjs +63 -0
- package/lib/server/mutants/validateMutantParam.js +69 -0
- package/lib/server/mutants/validateMutantsBody.js +88 -0
- package/lib/server/mutants/walkArtifact.js +179 -0
- package/lib/server/pageBuildRecords.js +131 -0
- package/lib/server/pageBuildRecords.test.mjs +258 -0
- package/lib/server/pageCache.mjs +22 -7
- package/lib/server/pageCache.test.mjs +22 -7
- package/lib/server/pageRebuilds.test.mjs +543 -0
- package/lib/server/pruneJitMaps.js +36 -0
- package/lib/server/pruneJitMaps.test.mjs +55 -0
- package/lib/server/readInstanceRecord.js +33 -0
- package/lib/server/readInstanceRecord.test.mjs +99 -0
- package/lib/server/readMergedMaps.js +86 -0
- package/lib/server/readMergedMaps.test.mjs +79 -0
- package/lib/server/recording/appendRecords.js +33 -0
- package/lib/server/recording/errorRecordingStamp.js +34 -0
- package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
- package/lib/server/recording/isRecordingEnabled.js +31 -0
- package/lib/server/recording/readRecordingCookie.js +71 -0
- package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
- package/lib/server/recording/recordingCookiePayload.js +33 -0
- package/lib/server/recording/servedBuilds.js +46 -0
- package/lib/server/recording/servedBuilds.test.mjs +42 -0
- package/lib/server/sinkExternalConnections.js +95 -0
- package/lib/server/sinkExternalConnections.test.mjs +135 -0
- package/lib/server/skipStaleMapWrites.js +62 -0
- package/lib/server/skipStaleMapWrites.test.mjs +113 -0
- package/lib/server/startIdleGc.js +67 -0
- package/lib/server/startIdleGc.test.mjs +138 -0
- package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
- package/lib/server/trackSessionWork.js +30 -0
- package/lib/vite/createDevServerExclude.js +40 -0
- package/lib/vite/createDevServerExclude.test.mjs +79 -0
- package/manager/getContext.mjs +25 -3
- package/manager/processes/initialBuild.mjs +17 -1
- package/manager/processes/initialBuild.test.mjs +104 -0
- package/manager/processes/lowdefyBuild.mjs +31 -7
- package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +72 -0
- package/manager/processes/lowdefyBuild.test.mjs +136 -1
- package/manager/processes/optimizeDependencies.mjs +59 -0
- package/manager/processes/restartServer.mjs +34 -6
- package/manager/processes/restartServer.test.mjs +162 -0
- package/manager/processes/shutdownServer.mjs +11 -0
- package/manager/processes/shutdownServer.test.mjs +45 -0
- package/manager/processes/startFirstServer.mjs +46 -0
- package/manager/processes/startFirstServer.test.mjs +326 -0
- package/manager/processes/startMailSink.mjs +135 -0
- package/manager/processes/startMailSink.test.mjs +146 -0
- package/manager/processes/startProxy.mjs +129 -12
- package/manager/processes/startProxy.test.mjs +366 -0
- package/manager/processes/startServer.mjs +47 -6
- package/manager/processes/startServer.test.mjs +155 -0
- package/manager/processes/startWatchers.mjs +6 -4
- package/manager/processes/syncServer.mjs +69 -0
- package/manager/processes/syncServer.test.mjs +141 -0
- package/manager/processes/waitForDevServer.mjs +48 -0
- package/manager/processes/waitForDevServer.test.mjs +112 -0
- package/manager/run.mjs +128 -43
- package/manager/utils/BatchChanges.mjs +21 -5
- package/manager/utils/BatchChanges.test.mjs +77 -0
- package/manager/utils/acquireDevInstance.mjs +99 -0
- package/manager/utils/acquireDevInstance.test.mjs +160 -0
- package/manager/utils/createBuildActivity.mjs +67 -0
- package/manager/utils/createBuildActivity.test.mjs +58 -0
- package/manager/utils/createChangeSignal.mjs +39 -0
- package/manager/utils/createChangeSignal.test.mjs +62 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
- package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
- package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
- package/manager/utils/createDotPathIgnore.mjs +44 -0
- package/manager/utils/createDotPathIgnore.test.mjs +65 -0
- package/manager/utils/createIdleGc.mjs +51 -0
- package/manager/utils/createIdleGc.test.mjs +77 -0
- package/manager/utils/createRequestActivity.mjs +100 -0
- package/manager/utils/createRequestActivity.test.mjs +121 -0
- package/manager/utils/createServerArtifactTracker.mjs +83 -0
- package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
- package/manager/utils/createServerEnv.mjs +39 -0
- package/manager/utils/filesHaveSameBytes.mjs +41 -0
- package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
- package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
- package/manager/utils/importFresh.mjs +46 -0
- package/manager/utils/importFresh.test.mjs +72 -0
- package/manager/utils/importFreshWorker.mjs +20 -0
- package/manager/utils/killTaggedBrowser.mjs +35 -0
- package/manager/utils/pagePathChanged.mjs +56 -0
- package/manager/utils/pruneRecordings.mjs +85 -0
- package/manager/utils/pruneRecordings.test.mjs +117 -0
- package/manager/utils/publishBuildDirectory.mjs +121 -0
- package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
- package/manager/utils/publishBuildDirectory.test.mjs +262 -0
- package/manager/utils/readBasePath.mjs +27 -0
- package/manager/utils/readBasePath.test.mjs +43 -0
- package/manager/utils/readBuildStatusWait.mjs +67 -0
- package/manager/utils/readManagerVersion.mjs +30 -0
- package/manager/utils/readManagerVersion.test.mjs +35 -0
- package/manager/utils/readPluginDefinitions.mjs +36 -0
- package/manager/utils/resolveDevAuthUrl.mjs +58 -0
- package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
- package/manager/utils/resolvePorts.mjs +49 -0
- package/manager/utils/setupWatcher.mjs +19 -11
- package/manager/utils/startRecordingPruner.mjs +45 -0
- package/manager/utils/startRecordingPruner.test.mjs +66 -0
- package/manager/utils/waitForChildExit.mjs +53 -0
- package/manager/utils/waitForChildExit.test.mjs +70 -0
- package/manager/utils/waitForServer.mjs +44 -0
- package/manager/utils/writeBuildStatus.mjs +1 -1
- package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
- package/manager/watchers/envWatcher.mjs +9 -2
- package/manager/watchers/envWatcher.test.mjs +87 -0
- package/manager/watchers/listServerSidePackages.mjs +47 -0
- package/manager/watchers/listServerSidePackages.test.mjs +38 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
- package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
- package/manager/watchers/pluginSourceWatcher.mjs +79 -0
- package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
- package/manager/watchers/restartRequestWatcher.mjs +71 -0
- package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
- package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
- package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
- package/manager/watchers/serverPackageWatcher.mjs +38 -0
- package/package.json +60 -47
- package/package.original.json +60 -47
- package/src/app.js +131 -20
- package/src/html/renderDevPage.js +63 -11
- package/src/html/renderDevPage.test.mjs +138 -0
- package/src/lib/getRequestPath.js +29 -0
- package/src/middleware/createSameOriginGuard.js +37 -0
- package/src/middleware/createSameOriginGuard.test.mjs +102 -0
- package/src/middleware/errorHandler.js +51 -1
- package/src/middleware/errorHandler.test.mjs +173 -3
- package/src/middleware/isRebindingSafeHost.js +44 -0
- package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
- package/src/middleware/localDevToolsOnly.js +38 -0
- package/src/middleware/localDevToolsOnly.test.mjs +61 -0
- package/src/middleware/noStore.js +27 -0
- package/src/middleware/noStore.test.mjs +48 -0
- package/src/middleware/requestTimeout.js +38 -0
- package/src/middleware/staleFlag.js +76 -0
- package/src/middleware/staleFlag.test.mjs +127 -0
- package/src/middleware/stampCaller.js +38 -0
- package/src/middleware/stampCaller.test.mjs +64 -0
- package/src/routes/agent.js +15 -3
- package/src/routes/auth.js +29 -20
- package/src/routes/auth.test.mjs +30 -56
- package/src/routes/clientError.js +13 -13
- package/src/routes/cron.js +3 -1
- package/src/routes/cronForward.js +3 -1
- package/src/routes/detached.js +22 -9
- package/src/routes/devInspect.js +17 -8
- package/src/routes/devInspect.test.mjs +44 -7
- package/src/routes/devRecording.js +113 -0
- package/src/routes/devRecording.test.mjs +253 -0
- package/src/routes/docs/buildStatus.js +10 -2
- package/src/routes/docs/buildStatus.test.mjs +46 -0
- package/src/routes/docs/content.js +5 -3
- package/src/routes/docs/evalOperator.js +15 -1
- package/src/routes/docs/events.js +50 -0
- package/src/routes/docs/events.test.mjs +117 -0
- package/src/routes/docs/find.js +9 -1
- package/src/routes/docs/icons.js +30 -0
- package/src/routes/docs/inspectState.js +16 -1
- package/src/routes/docs/journey.js +194 -0
- package/src/routes/docs/journey.test.mjs +401 -0
- package/src/routes/docs/journeyRefusals.test.mjs +71 -0
- package/src/routes/docs/loadState.js +4 -2
- package/src/routes/docs/mcp.js +34 -3
- package/src/routes/docs/mcp.test.mjs +130 -0
- package/src/routes/docs/mutants.js +78 -0
- package/src/routes/docs/mutants.test.mjs +180 -0
- package/src/routes/docs/parsePathParamsParam.js +41 -0
- package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
- package/src/routes/docs/parseUserParam.js +27 -21
- package/src/routes/docs/parseUserParam.test.mjs +17 -5
- package/src/routes/docs/restart.js +35 -0
- package/src/routes/docs/restart.test.mjs +64 -0
- package/src/routes/docs/runEndpoint.js +54 -0
- package/src/routes/docs/runEndpoint.test.mjs +165 -0
- package/src/routes/docs/runRequest.js +25 -8
- package/src/routes/docs/runRequest.test.mjs +92 -0
- package/src/routes/docs/screenshot.js +50 -2
- package/src/routes/docs/screenshot.test.mjs +117 -0
- package/src/routes/docs/search.js +15 -2
- package/src/routes/docs/snapshotState.js +17 -3
- package/src/routes/endpoints.js +27 -26
- package/src/routes/endpointsTextAnswer.test.mjs +77 -0
- package/src/routes/feedback.js +10 -13
- package/src/routes/jitPage.js +99 -35
- package/src/routes/jitPage.test.mjs +248 -0
- package/src/routes/mcp.js +82 -5
- package/src/routes/mcp.test.mjs +215 -0
- package/src/routes/mcpMemberToken.test.mjs +125 -0
- package/src/routes/mcpProtectedResourceMetadata.js +34 -0
- package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
- package/src/routes/mountDevPageRoutes.js +25 -0
- package/src/routes/mountDevPageRoutes.test.mjs +30 -0
- package/src/routes/mountOauthDiscovery.js +44 -0
- package/src/routes/mountOauthDiscovery.test.mjs +79 -0
- package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
- package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
- package/src/routes/reload.js +16 -0
- package/src/routes/request.js +6 -4
- package/src/routes/request.test.mjs +9 -5
- package/src/routes/requestBody.test.mjs +159 -0
- package/src/routes/usage.js +6 -2
- package/src/routes/user.js +27 -0
- package/src/routes/websocket.js +29 -8
- package/src/routes/websocket.test.mjs +65 -0
- package/src/routes/wellKnownFallback.js +30 -0
- package/src/routes/wellKnownFallback.test.mjs +32 -0
- package/src/websocket/devWebSocket.js +41 -26
- package/src/websocket/devWebSocket.test.mjs +157 -0
- package/vite.config.js +53 -23
- package/lib/server/auth/getDevSession.js +0 -70
- package/lib/server/auth/getDevSession.test.mjs +0 -138
- package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
- package/manager/watchers/moduleBuildWatcher.mjs +0 -79
- package/manager/watchers/serverArtifactWatcher.mjs +0 -120
- package/src/routes/apiPage.js +0 -37
|
@@ -0,0 +1,216 @@
|
|
|
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 createPairingBuffer from './createPairingBuffer.js';
|
|
18
|
+
import createPasswordRedactor from './createPasswordRedactor.js';
|
|
19
|
+
import createRecordBatcher from './createRecordBatcher.js';
|
|
20
|
+
import createRecordingSession from './createRecordingSession.js';
|
|
21
|
+
import keyChord from './keyChord.js';
|
|
22
|
+
import sendRecords from './sendRecords.js';
|
|
23
|
+
|
|
24
|
+
// Dev tools draw their own UI over the app; clicks on it are not the app.
|
|
25
|
+
const IGNORED_ROOTS = '[data-lowdefy-feedback]';
|
|
26
|
+
|
|
27
|
+
function elementOf(target) {
|
|
28
|
+
if (target === null || target === undefined) return null;
|
|
29
|
+
if (target.nodeType === 1) return target;
|
|
30
|
+
return target.parentElement ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function valueOf(element) {
|
|
34
|
+
if (element.type === 'checkbox' || element.type === 'radio') return element.checked;
|
|
35
|
+
if ('value' in element) return element.value;
|
|
36
|
+
return element.textContent ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Guards a listener: a bad payload or a broken hook must never reach the app
|
|
40
|
+
// the recorder rides on.
|
|
41
|
+
function safely(handler) {
|
|
42
|
+
return function guarded(...args) {
|
|
43
|
+
try {
|
|
44
|
+
handler(...args);
|
|
45
|
+
} catch {
|
|
46
|
+
// Recording is best effort.
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The dev recorder for one tab: subscribes to the engine's trace hook with
|
|
52
|
+
// state, listens to DOM interactions in the capture phase, pairs them through
|
|
53
|
+
// the pairing buffer and batches finished records to /api/dev-recording. It
|
|
54
|
+
// opens no connection of its own: sends are short keepalive POSTs, and the
|
|
55
|
+
// recorder flushes on the shared dev stream's reload event (attachStream).
|
|
56
|
+
// Returns null, subscribing to nothing, when recording is off for this page.
|
|
57
|
+
function createRecorder({ basePath, lowdefy, recording, window, getTrace }) {
|
|
58
|
+
if (recording?.enabled !== true) return null;
|
|
59
|
+
const { document } = window;
|
|
60
|
+
const trace = getTrace(lowdefy);
|
|
61
|
+
const session = createRecordingSession({ storage: window.sessionStorage });
|
|
62
|
+
const batcher = createRecordBatcher({
|
|
63
|
+
send: (records) => sendRecords({ basePath, records, fetch: window.fetch.bind(window) }),
|
|
64
|
+
});
|
|
65
|
+
const buffer = createPairingBuffer({
|
|
66
|
+
onRecord: (record) => batcher.add(record),
|
|
67
|
+
getSession: () => session.getId(),
|
|
68
|
+
getRoles: () => lowdefy.user?.roles ?? [],
|
|
69
|
+
getBuild: () => lowdefy._devBuildId ?? null,
|
|
70
|
+
redactor: createPasswordRedactor(),
|
|
71
|
+
});
|
|
72
|
+
let pageId = null;
|
|
73
|
+
// A pageview waits here until the page it shows is on screen, because only
|
|
74
|
+
// the fetched page says which page, and which path values, a path is: the
|
|
75
|
+
// pageview fires on the route change, while the config fetch is suspended.
|
|
76
|
+
// A path left before its page was shown names no page and is not recorded.
|
|
77
|
+
let pendingPageview = null;
|
|
78
|
+
// The page on screen and the path it was shown for.
|
|
79
|
+
let shown = null;
|
|
80
|
+
|
|
81
|
+
function currentUrl() {
|
|
82
|
+
return `${window.location.pathname}${window.location.search}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function describe(element) {
|
|
86
|
+
if (element === null || element.closest(IGNORED_ROOTS) !== null) return null;
|
|
87
|
+
return trace.describeElement(element);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const onClick = safely((event) => {
|
|
91
|
+
const element = elementOf(event.target);
|
|
92
|
+
const target = describe(element);
|
|
93
|
+
if (target === null) return;
|
|
94
|
+
buffer.addInteraction({ kind: 'click', element, target, pageId });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const onInput = safely((event) => {
|
|
98
|
+
const element = elementOf(event.target);
|
|
99
|
+
const target = describe(element);
|
|
100
|
+
if (target === null) return;
|
|
101
|
+
buffer.addInteraction({ kind: 'change', element, target, pageId, value: valueOf(element) });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const onKeyDown = safely((event) => {
|
|
105
|
+
const key = keyChord(event, { platform: window.navigator.platform });
|
|
106
|
+
if (key === null) return;
|
|
107
|
+
const element = elementOf(event.target);
|
|
108
|
+
if (element !== null && element.closest(IGNORED_ROOTS) !== null) return;
|
|
109
|
+
const target = element === null || element === document.body ? null : describe(element);
|
|
110
|
+
buffer.addInteraction({ kind: 'key', element, target, pageId, key });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const onPopState = safely(() => {
|
|
114
|
+
buffer.addInteraction({ kind: 'back', pageId });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const onTrace = safely((payload) => {
|
|
118
|
+
buffer.addEvent(payload, { urlAfter: currentUrl() });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function flush() {
|
|
122
|
+
try {
|
|
123
|
+
buffer.flushAll();
|
|
124
|
+
return batcher.flush();
|
|
125
|
+
} catch {
|
|
126
|
+
return Promise.resolve();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The pending pageview as a visit to `page`, under the build its config was
|
|
131
|
+
// served under (lowdefy._devBuildId, set as Page.jsx renders it).
|
|
132
|
+
function recordPendingPageview({ page }) {
|
|
133
|
+
const { t, path, url } = pendingPageview;
|
|
134
|
+
pendingPageview = null;
|
|
135
|
+
pageId = page.pageId;
|
|
136
|
+
shown = { path, page };
|
|
137
|
+
buffer.addInteraction({
|
|
138
|
+
t,
|
|
139
|
+
kind: 'pageview',
|
|
140
|
+
pageId: page.pageId,
|
|
141
|
+
pathParams: page.pathParams,
|
|
142
|
+
url,
|
|
143
|
+
build: lowdefy._devBuildId ?? null,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const onPageHide = safely(() => {
|
|
148
|
+
flush();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const unsubscribe = trace.subscribe(onTrace, { state: true });
|
|
152
|
+
document.addEventListener('click', onClick, true);
|
|
153
|
+
document.addEventListener('input', onInput, true);
|
|
154
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
155
|
+
window.addEventListener('popstate', onPopState);
|
|
156
|
+
window.addEventListener('pagehide', onPageHide);
|
|
157
|
+
window.__lowdefyRecorder = { flush };
|
|
158
|
+
|
|
159
|
+
// The time and url are the navigation's; the page and build wait for
|
|
160
|
+
// pageShown. `path` keys the visit, so moving between two instances of one
|
|
161
|
+
// page is a pageview. Coming back to the path of the page still on screen
|
|
162
|
+
// (the page between was never shown) brings no new page to show, so it is
|
|
163
|
+
// that page's visit at once.
|
|
164
|
+
function pageview({ path }) {
|
|
165
|
+
try {
|
|
166
|
+
pendingPageview = { t: Date.now(), path, url: currentUrl() };
|
|
167
|
+
if (shown !== null && shown.path === path) {
|
|
168
|
+
recordPendingPageview({ page: shown.page });
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// Recording is best effort.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Called with the page instance on screen, { pageId, pathParams,
|
|
176
|
+
// instanceKey }, each time it changes. It changes after the Recorder's
|
|
177
|
+
// pageview for the same route change, once Page has rendered the fetched
|
|
178
|
+
// config; a config reload keeps the instance and changes nothing.
|
|
179
|
+
function pageShown(page) {
|
|
180
|
+
try {
|
|
181
|
+
if (pendingPageview === null) return;
|
|
182
|
+
recordPendingPageview({ page });
|
|
183
|
+
} catch {
|
|
184
|
+
// Recording is best effort.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Listens on the tab's one shared dev stream (DevStreamContext), never a
|
|
189
|
+
// stream of its own. A config reload flushes so the attempt before it is
|
|
190
|
+
// sent promptly; each record already carries the build it was made under.
|
|
191
|
+
function attachStream(source) {
|
|
192
|
+
if (source === null || source === undefined) return () => {};
|
|
193
|
+
const onReload = safely(() => {
|
|
194
|
+
flush();
|
|
195
|
+
});
|
|
196
|
+
source.addEventListener('reload', onReload);
|
|
197
|
+
return () => source.removeEventListener('reload', onReload);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function stop() {
|
|
201
|
+
flush();
|
|
202
|
+
unsubscribe();
|
|
203
|
+
document.removeEventListener('click', onClick, true);
|
|
204
|
+
document.removeEventListener('input', onInput, true);
|
|
205
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
206
|
+
window.removeEventListener('popstate', onPopState);
|
|
207
|
+
window.removeEventListener('pagehide', onPageHide);
|
|
208
|
+
if (window.__lowdefyRecorder?.flush === flush) {
|
|
209
|
+
delete window.__lowdefyRecorder;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { attachStream, flush, pageShown, pageview, stop };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default createRecorder;
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
/*
|
|
5
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { jest } from '@jest/globals';
|
|
21
|
+
|
|
22
|
+
import createRecorder from './createRecorder.js';
|
|
23
|
+
|
|
24
|
+
let registry;
|
|
25
|
+
let fetch;
|
|
26
|
+
let recorder;
|
|
27
|
+
|
|
28
|
+
function createRegistry() {
|
|
29
|
+
const listeners = [];
|
|
30
|
+
return {
|
|
31
|
+
listeners,
|
|
32
|
+
subscribe: jest.fn((listener, options) => {
|
|
33
|
+
listeners.push({ listener, options });
|
|
34
|
+
return () => listeners.splice(listeners.indexOf(listener), 1);
|
|
35
|
+
}),
|
|
36
|
+
describeElement: jest.fn((element) => ({
|
|
37
|
+
page_id: 'tickets',
|
|
38
|
+
block_id: element.closest('[id^="bl-"]')?.id.slice(3) ?? null,
|
|
39
|
+
block_type: null,
|
|
40
|
+
row: null,
|
|
41
|
+
column: null,
|
|
42
|
+
text: element.textContent.trim() || null,
|
|
43
|
+
nth: null,
|
|
44
|
+
option: false,
|
|
45
|
+
block_ids: element.closest('[id^="bl-"]') ? [element.closest('[id^="bl-"]').id.slice(3)] : [],
|
|
46
|
+
})),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function start(recording = { enabled: true }, lowdefy = { user: { roles: ['admin'] } }) {
|
|
51
|
+
return createRecorder({
|
|
52
|
+
basePath: '/base',
|
|
53
|
+
lowdefy,
|
|
54
|
+
recording,
|
|
55
|
+
window,
|
|
56
|
+
getTrace: () => registry,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The page instance on screen, as Routing hands it to the Recorder.
|
|
61
|
+
function page(pageId, pathParams = {}) {
|
|
62
|
+
return { pageId, pathParams, instanceKey: `page:${pageId}:${JSON.stringify(pathParams)}` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sentBodies() {
|
|
66
|
+
return fetch.mock.calls.map(([, init]) => JSON.parse(init.body));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
registry = createRegistry();
|
|
71
|
+
fetch = jest.fn(() => Promise.resolve({ status: 204 }));
|
|
72
|
+
window.fetch = fetch;
|
|
73
|
+
window.sessionStorage.clear();
|
|
74
|
+
document.body.innerHTML = '<div id="bl-save"><button id="save">Save</button></div>';
|
|
75
|
+
recorder = null;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
recorder?.stop();
|
|
80
|
+
jest.restoreAllMocks();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('createRecorder with recording off subscribes to nothing and adds no listener', () => {
|
|
84
|
+
const addListener = jest.spyOn(document, 'addEventListener');
|
|
85
|
+
expect(start({ enabled: false })).toBe(null);
|
|
86
|
+
expect(start(null)).toBe(null);
|
|
87
|
+
expect(registry.subscribe).not.toHaveBeenCalled();
|
|
88
|
+
expect(addListener).not.toHaveBeenCalled();
|
|
89
|
+
expect(window.__lowdefyRecorder).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('createRecorder subscribes once with state and posts records as keepalive fetches', async () => {
|
|
93
|
+
recorder = start();
|
|
94
|
+
expect(registry.subscribe).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(registry.listeners[0].options).toEqual({ state: true });
|
|
96
|
+
recorder.pageview({ path: 'tickets' });
|
|
97
|
+
recorder.pageShown(page('tickets'));
|
|
98
|
+
document.getElementById('save').click();
|
|
99
|
+
await window.__lowdefyRecorder.flush();
|
|
100
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
101
|
+
const [url, init] = fetch.mock.calls[0];
|
|
102
|
+
expect(url).toBe('/base/api/dev-recording');
|
|
103
|
+
expect(init).toMatchObject({ method: 'POST', keepalive: true });
|
|
104
|
+
const [body] = sentBodies();
|
|
105
|
+
expect(body.session).toMatch(/^\d{8}T\d{6}Z-[a-z0-9]{6}$/);
|
|
106
|
+
expect(body.records.map((record) => record.kind)).toEqual(['pageview', 'click']);
|
|
107
|
+
expect(body.records[1]).toMatchObject({ roles: ['admin'], target: { block_id: 'save' } });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('createRecorder flushes on pagehide', async () => {
|
|
111
|
+
recorder = start();
|
|
112
|
+
document.getElementById('save').click();
|
|
113
|
+
window.dispatchEvent(new Event('pagehide'));
|
|
114
|
+
await Promise.resolve();
|
|
115
|
+
expect(sentBodies()[0].records[0].kind).toBe('click');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("createRecorder flushes on the shared stream's reload event and detaches cleanly", async () => {
|
|
119
|
+
recorder = start();
|
|
120
|
+
const source = new EventTarget();
|
|
121
|
+
const detach = recorder.attachStream(source);
|
|
122
|
+
document.getElementById('save').click();
|
|
123
|
+
source.dispatchEvent(new Event('reload'));
|
|
124
|
+
await Promise.resolve();
|
|
125
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
126
|
+
detach();
|
|
127
|
+
document.getElementById('save').click();
|
|
128
|
+
source.dispatchEvent(new Event('reload'));
|
|
129
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
130
|
+
expect(recorder.attachStream(null)).toEqual(expect.any(Function));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('an interaction whose hold closes after a config reload keeps the build it was captured under', async () => {
|
|
134
|
+
const lowdefy = { user: { roles: ['admin'] }, _devBuildId: '2026-10-03T14:02:00.000Z' };
|
|
135
|
+
recorder = start({ enabled: true }, lowdefy);
|
|
136
|
+
const source = new EventTarget();
|
|
137
|
+
recorder.attachStream(source);
|
|
138
|
+
recorder.pageview({ path: 'tickets' });
|
|
139
|
+
recorder.pageShown(page('tickets'));
|
|
140
|
+
document.getElementById('save').click();
|
|
141
|
+
lowdefy._devBuildId = '2026-10-03T14:05:00.000Z';
|
|
142
|
+
source.dispatchEvent(new Event('reload'));
|
|
143
|
+
await Promise.resolve();
|
|
144
|
+
const [body] = sentBodies();
|
|
145
|
+
expect(body.records.map((record) => [record.kind, record.build])).toEqual([
|
|
146
|
+
['pageview', '2026-10-03T14:02:00.000Z'],
|
|
147
|
+
['click', '2026-10-03T14:02:00.000Z'],
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('an engine record keeps the build its trace payload arrived under', async () => {
|
|
152
|
+
const lowdefy = { user: { roles: ['admin'] }, _devBuildId: '2026-10-03T14:02:00.000Z' };
|
|
153
|
+
recorder = start({ enabled: true }, lowdefy);
|
|
154
|
+
registry.listeners[0].listener({
|
|
155
|
+
scope: 'page',
|
|
156
|
+
pageId: 'tickets',
|
|
157
|
+
blockId: 'tickets',
|
|
158
|
+
blockType: 'Box',
|
|
159
|
+
eventName: 'onMount',
|
|
160
|
+
success: true,
|
|
161
|
+
failure: null,
|
|
162
|
+
debounceMs: 0,
|
|
163
|
+
actions: [],
|
|
164
|
+
record: { startTimestamp: new Date(), responses: {} },
|
|
165
|
+
context: { state: {}, requests: {} },
|
|
166
|
+
stateBefore: {},
|
|
167
|
+
});
|
|
168
|
+
lowdefy._devBuildId = '2026-10-03T14:05:00.000Z';
|
|
169
|
+
await window.__lowdefyRecorder.flush();
|
|
170
|
+
const [record] = sentBodies()[0].records;
|
|
171
|
+
expect(record).toMatchObject({ kind: 'engine', build: '2026-10-03T14:02:00.000Z' });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('a record made before any page config named its build carries build null', async () => {
|
|
175
|
+
recorder = start();
|
|
176
|
+
document.getElementById('save').click();
|
|
177
|
+
await window.__lowdefyRecorder.flush();
|
|
178
|
+
expect(sentBodies()[0].records[0].build).toBe(null);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('flush resolves only after the POST settles', async () => {
|
|
182
|
+
let respond;
|
|
183
|
+
fetch.mockImplementation(
|
|
184
|
+
() =>
|
|
185
|
+
new Promise((resolve) => {
|
|
186
|
+
respond = resolve;
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
recorder = start();
|
|
190
|
+
document.getElementById('save').click();
|
|
191
|
+
let done = false;
|
|
192
|
+
const flushed = window.__lowdefyRecorder.flush().then(() => {
|
|
193
|
+
done = true;
|
|
194
|
+
});
|
|
195
|
+
await Promise.resolve();
|
|
196
|
+
await Promise.resolve();
|
|
197
|
+
expect(done).toBe(false);
|
|
198
|
+
respond({ status: 204 });
|
|
199
|
+
await flushed;
|
|
200
|
+
expect(done).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('a throwing trace payload or describeElement leaves the app running', () => {
|
|
204
|
+
recorder = start();
|
|
205
|
+
expect(() => registry.listeners[0].listener(null)).not.toThrow();
|
|
206
|
+
registry.describeElement.mockImplementation(() => {
|
|
207
|
+
throw new Error('broken');
|
|
208
|
+
});
|
|
209
|
+
expect(() => document.getElementById('save').click()).not.toThrow();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('a throwing sessionStorage leaves the recorder recording under an unremembered session', async () => {
|
|
213
|
+
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
|
214
|
+
throw new Error('SecurityError');
|
|
215
|
+
});
|
|
216
|
+
jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
217
|
+
throw new Error('SecurityError');
|
|
218
|
+
});
|
|
219
|
+
recorder = start();
|
|
220
|
+
document.getElementById('save').click();
|
|
221
|
+
await window.__lowdefyRecorder.flush();
|
|
222
|
+
expect(sentBodies()[0].session).toMatch(/^\d{8}T\d{6}Z-[a-z0-9]{6}$/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('a failed send is dropped silently', async () => {
|
|
226
|
+
fetch.mockImplementation(() => Promise.reject(new Error('offline')));
|
|
227
|
+
recorder = start();
|
|
228
|
+
document.getElementById('save').click();
|
|
229
|
+
await expect(window.__lowdefyRecorder.flush()).resolves.toBeUndefined();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('stop unsubscribes, removes listeners and the flush hook', () => {
|
|
233
|
+
const lowdefy = { user: { roles: ['admin'] } };
|
|
234
|
+
recorder = start({ enabled: true }, lowdefy);
|
|
235
|
+
recorder.stop();
|
|
236
|
+
expect(registry.listeners).toHaveLength(0);
|
|
237
|
+
expect(window.__lowdefyRecorder).toBeUndefined();
|
|
238
|
+
document.getElementById('save').click();
|
|
239
|
+
recorder = null;
|
|
240
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const BUILD_A = '2026-10-03T14:02:00.000Z';
|
|
244
|
+
const BUILD_B = '2026-10-03T14:05:00.000Z';
|
|
245
|
+
|
|
246
|
+
test('a pageview waits for its page to be shown and carries the build that page rendered under', async () => {
|
|
247
|
+
const lowdefy = { user: { roles: ['admin'] } };
|
|
248
|
+
recorder = start({ enabled: true }, lowdefy);
|
|
249
|
+
recorder.pageview({ path: 'tickets' });
|
|
250
|
+
await window.__lowdefyRecorder.flush();
|
|
251
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
252
|
+
lowdefy._devBuildId = BUILD_A;
|
|
253
|
+
recorder.pageShown(page('tickets'));
|
|
254
|
+
await window.__lowdefyRecorder.flush();
|
|
255
|
+
const [record] = sentBodies()[0].records;
|
|
256
|
+
expect(record).toMatchObject({ kind: 'pageview', page_id: 'tickets', build: BUILD_A });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('a navigation carries the build of the new page config, not the one the previous page ran on', async () => {
|
|
260
|
+
const lowdefy = { user: { roles: ['admin'] }, _devBuildId: BUILD_A };
|
|
261
|
+
recorder = start({ enabled: true }, lowdefy);
|
|
262
|
+
recorder.pageview({ path: 'tickets' });
|
|
263
|
+
recorder.pageShown(page('tickets'));
|
|
264
|
+
recorder.pageview({ path: 'orders' });
|
|
265
|
+
lowdefy._devBuildId = BUILD_B;
|
|
266
|
+
recorder.pageShown(page('orders'));
|
|
267
|
+
await window.__lowdefyRecorder.flush();
|
|
268
|
+
expect(sentBodies()[0].records.map((record) => record.build)).toEqual([BUILD_A, BUILD_B]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('a path left before its page was shown is not recorded', async () => {
|
|
272
|
+
const lowdefy = { user: { roles: ['admin'] }, _devBuildId: BUILD_A };
|
|
273
|
+
recorder = start({ enabled: true }, lowdefy);
|
|
274
|
+
recorder.pageview({ path: 'tickets' });
|
|
275
|
+
recorder.pageShown(page('tickets'));
|
|
276
|
+
recorder.pageview({ path: 'orders' });
|
|
277
|
+
recorder.pageview({ path: 'settings' });
|
|
278
|
+
window.dispatchEvent(new Event('pagehide'));
|
|
279
|
+
await Promise.resolve();
|
|
280
|
+
expect(sentBodies()[0].records.map((record) => record.page_id)).toEqual(['tickets']);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('coming back to the page still on screen is its visit at once', async () => {
|
|
284
|
+
const lowdefy = { user: { roles: ['admin'] }, _devBuildId: BUILD_A };
|
|
285
|
+
recorder = start({ enabled: true }, lowdefy);
|
|
286
|
+
recorder.pageview({ path: 'tickets' });
|
|
287
|
+
recorder.pageShown(page('tickets'));
|
|
288
|
+
recorder.pageview({ path: 'broken' });
|
|
289
|
+
recorder.pageview({ path: 'tickets' });
|
|
290
|
+
await window.__lowdefyRecorder.flush();
|
|
291
|
+
expect(sentBodies()[0].records.map((record) => [record.kind, record.page_id])).toEqual([
|
|
292
|
+
['pageview', 'tickets'],
|
|
293
|
+
['pageview', 'tickets'],
|
|
294
|
+
]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('a visit to a patterned page records the page and path values of the page shown', async () => {
|
|
298
|
+
const lowdefy = { user: { roles: ['admin'] }, _devBuildId: BUILD_A };
|
|
299
|
+
recorder = start({ enabled: true }, lowdefy);
|
|
300
|
+
recorder.pageview({ path: 'tickets/s/1' });
|
|
301
|
+
recorder.pageShown(page('ticket', { space: 's', ticket_id: '1' }));
|
|
302
|
+
recorder.pageview({ path: 'tickets/s/2' });
|
|
303
|
+
recorder.pageShown(page('ticket', { space: 's', ticket_id: '2' }));
|
|
304
|
+
await window.__lowdefyRecorder.flush();
|
|
305
|
+
expect(
|
|
306
|
+
sentBodies()[0].records.map((record) => [record.kind, record.page_id, record.path_params])
|
|
307
|
+
).toEqual([
|
|
308
|
+
['pageview', 'ticket', { space: 's', ticket_id: '1' }],
|
|
309
|
+
['pageview', 'ticket', { space: 's', ticket_id: '2' }],
|
|
310
|
+
]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('a page shown with no pageview pending records nothing', async () => {
|
|
314
|
+
recorder = start();
|
|
315
|
+
recorder.pageShown(page('tickets'));
|
|
316
|
+
await window.__lowdefyRecorder.flush();
|
|
317
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
318
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
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 { createTraceId, isTraceId } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
const STORAGE_KEY = 'lowdefy_dev_recording';
|
|
20
|
+
const IDLE_MS = 10 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
function readStored(storage) {
|
|
23
|
+
try {
|
|
24
|
+
const stored = JSON.parse(storage.getItem(STORAGE_KEY));
|
|
25
|
+
if (isTraceId(stored?.id) && typeof stored.lastAt === 'number') return stored;
|
|
26
|
+
} catch {
|
|
27
|
+
// Blocked or malformed storage: start an unremembered session.
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeStored(storage, value) {
|
|
33
|
+
try {
|
|
34
|
+
storage.setItem(STORAGE_KEY, JSON.stringify(value));
|
|
35
|
+
} catch {
|
|
36
|
+
// A browser that blocks site data records under an unremembered session.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The tab's recording session. sessionStorage lives as long as the tab, so a
|
|
41
|
+
// session survives the reload a dev server restart triggers; a gap of more
|
|
42
|
+
// than 10 minutes starts a new one.
|
|
43
|
+
function createRecordingSession({ storage, now = Date.now }) {
|
|
44
|
+
let current = null;
|
|
45
|
+
|
|
46
|
+
function getId() {
|
|
47
|
+
const time = now();
|
|
48
|
+
const stored = readStored(storage) ?? current;
|
|
49
|
+
const session =
|
|
50
|
+
stored !== null && time - stored.lastAt <= IDLE_MS
|
|
51
|
+
? { id: stored.id, lastAt: time }
|
|
52
|
+
: { id: createTraceId({ now: time }), lastAt: time };
|
|
53
|
+
current = session;
|
|
54
|
+
writeStored(storage, session);
|
|
55
|
+
return session.id;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { getId };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default createRecordingSession;
|
|
@@ -0,0 +1,72 @@
|
|
|
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 createRecordingSession from './createRecordingSession.js';
|
|
18
|
+
|
|
19
|
+
const START = Date.parse('2026-10-03T14:03:11.000Z');
|
|
20
|
+
|
|
21
|
+
function createStorage() {
|
|
22
|
+
const values = new Map();
|
|
23
|
+
return {
|
|
24
|
+
getItem: (key) => (values.has(key) ? values.get(key) : null),
|
|
25
|
+
setItem: (key, value) => values.set(key, value),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('createRecordingSession keeps one session id while the tab is active', () => {
|
|
30
|
+
let time = START;
|
|
31
|
+
const session = createRecordingSession({ storage: createStorage(), now: () => time });
|
|
32
|
+
const id = session.getId();
|
|
33
|
+
expect(id).toMatch(/^20261003T140311Z-[a-z0-9]{6}$/);
|
|
34
|
+
time += 9 * 60 * 1000;
|
|
35
|
+
expect(session.getId()).toBe(id);
|
|
36
|
+
time += 9 * 60 * 1000;
|
|
37
|
+
expect(session.getId()).toBe(id);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('createRecordingSession starts a new session after 10 minutes idle', () => {
|
|
41
|
+
let time = START;
|
|
42
|
+
const session = createRecordingSession({ storage: createStorage(), now: () => time });
|
|
43
|
+
const id = session.getId();
|
|
44
|
+
time += 10 * 60 * 1000 + 1;
|
|
45
|
+
const next = session.getId();
|
|
46
|
+
expect(next).not.toBe(id);
|
|
47
|
+
expect(next).toMatch(/^20261003T141311Z-/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('createRecordingSession survives a reload through sessionStorage', () => {
|
|
51
|
+
const storage = createStorage();
|
|
52
|
+
let time = START;
|
|
53
|
+
const id = createRecordingSession({ storage, now: () => time }).getId();
|
|
54
|
+
time += 2000;
|
|
55
|
+
expect(createRecordingSession({ storage, now: () => time }).getId()).toBe(id);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('createRecordingSession records under an unremembered session when storage throws', () => {
|
|
59
|
+
const storage = {
|
|
60
|
+
getItem: () => {
|
|
61
|
+
throw new Error('SecurityError');
|
|
62
|
+
},
|
|
63
|
+
setItem: () => {
|
|
64
|
+
throw new Error('SecurityError');
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
let time = START;
|
|
68
|
+
const session = createRecordingSession({ storage, now: () => time });
|
|
69
|
+
const id = session.getId();
|
|
70
|
+
time += 1000;
|
|
71
|
+
expect(session.getId()).toBe(id);
|
|
72
|
+
});
|