@lowdefy/server-dev 6.1.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +67 -0
- package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
- package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +679 -0
- package/lib/docs/dataSets/describeDataSetResult.js +25 -0
- package/lib/docs/dataSets/describeDatabaseTarget.js +46 -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/findCollectionCollisions.js +52 -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 +44 -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 +119 -0
- package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +343 -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 +68 -0
- package/lib/docs/dataSets/readDataSet.test.mjs +220 -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/sweepOrphanDatabases.js +33 -0
- package/lib/docs/dataSets/test/app/lowdefy.yaml +203 -0
- package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
- package/lib/docs/dataSets/test/app/tests/data/shop.yaml +8 -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 +54 -6
- package/lib/docs/getBuildStatus.test.mjs +133 -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 +83 -0
- package/lib/server/applyDataSetRedirect.test.mjs +340 -0
- package/lib/server/applyDataSetRedirectTenantReach.test.mjs +204 -0
- package/lib/server/applyDataSetRedirectWalled.test.mjs +131 -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 +328 -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 +112 -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 +24 -2
- package/manager/processes/initialBuild.mjs +14 -0
- package/manager/processes/initialBuild.test.mjs +101 -0
- package/manager/processes/lowdefyBuild.mjs +30 -7
- package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +62 -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,236 @@
|
|
|
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 path from 'node:path';
|
|
18
|
+
import { type } from '@lowdefy/helpers';
|
|
19
|
+
import { getStepKey, INTERACTION_STEP_KEYS } from '@lowdefy/node-utils';
|
|
20
|
+
|
|
21
|
+
import claimExpectedErrors from './claimExpectedErrors.js';
|
|
22
|
+
|
|
23
|
+
import evaluateInvariants from './evaluateInvariants.js';
|
|
24
|
+
import installStepObserver from './installStepObserver.js';
|
|
25
|
+
import readStepObserver from './readStepObserver.js';
|
|
26
|
+
import resolveConfigKeySource from './resolveConfigKeySource.js';
|
|
27
|
+
import takeJourneyEvents from './takeJourneyEvents.js';
|
|
28
|
+
import { takeRunErrors } from '../runErrorBuffers.js';
|
|
29
|
+
import toAppErrorFailure from './toAppErrorFailure.js';
|
|
30
|
+
import TRANSIENT_SELECTOR from './TRANSIENT_SELECTOR.js';
|
|
31
|
+
import usesSearchStage from './usesSearchStage.js';
|
|
32
|
+
import waitForClientErrorReports from './waitForClientErrorReports.js';
|
|
33
|
+
import watchJourneyContext from './watchJourneyContext.js';
|
|
34
|
+
import withAppErrors from './withAppErrors.js';
|
|
35
|
+
|
|
36
|
+
function isExpectError(step) {
|
|
37
|
+
return getStepKey(step ?? {}) === 'expect' && getStepKey(step.expect) === 'error';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Steps that cannot cause an error a page reports later, so their window
|
|
41
|
+
// closes without waiting for client error reports in flight. An expect.error
|
|
42
|
+
// waits, so a late report of the interaction before it can be claimed.
|
|
43
|
+
function isQuietStep(step) {
|
|
44
|
+
return ['expect', 'wait', 'screenshot'].includes(getStepKey(step)) && !isExpectError(step);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// What fails a journey: an app error. A dead click is only a warning; a
|
|
48
|
+
// journey asserts it with expect.effect.
|
|
49
|
+
function isAppError(finding) {
|
|
50
|
+
return finding.severity === 'error' || finding.kind === 'environment';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Watches a journey run for app errors and judges each step's window with the
|
|
54
|
+
// app error invariants (evaluateInvariants), each error keyed by findingKey. Every
|
|
55
|
+
// actor's context is watched from before its first request (onContext). The
|
|
56
|
+
// windows are contiguous, from the page open to the final drain after the
|
|
57
|
+
// last step, so an error that lands between two steps is never lost. Each
|
|
58
|
+
// window takes what the run's actors buffered (page errors, app API
|
|
59
|
+
// responses) and the error entries the run's own error buffer claimed (see
|
|
60
|
+
// runErrorBuffers), and nothing a developer's own tab or another run caused.
|
|
61
|
+
function createJourneyAppErrors({ origin, basePath, recording, configDirectory }) {
|
|
62
|
+
const buildDirectory = path.join(process.cwd(), 'build');
|
|
63
|
+
const run = { run: recording.run.id, journey: recording.run.journey };
|
|
64
|
+
const events = {
|
|
65
|
+
pageErrors: [],
|
|
66
|
+
requests: [],
|
|
67
|
+
responses: [],
|
|
68
|
+
abortedRequests: [],
|
|
69
|
+
pendingClientErrors: new Set(),
|
|
70
|
+
};
|
|
71
|
+
let since = 0;
|
|
72
|
+
let current = null;
|
|
73
|
+
// The window the last step closed, which an expect.effect step reads for
|
|
74
|
+
// the interaction just before it.
|
|
75
|
+
let closed = null;
|
|
76
|
+
|
|
77
|
+
function onContext({ context }) {
|
|
78
|
+
watchJourneyContext({ context, events, origin, basePath });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function takeWindow({ page, observed, urlBefore }) {
|
|
82
|
+
const until = Date.now();
|
|
83
|
+
const window = {
|
|
84
|
+
since,
|
|
85
|
+
until,
|
|
86
|
+
urlBefore,
|
|
87
|
+
urlAfter: page.isClosed() ? null : page.url(),
|
|
88
|
+
emits: observed?.emits ?? [],
|
|
89
|
+
mutationCount: observed?.mutationCount ?? null,
|
|
90
|
+
...takeJourneyEvents({ events, since, until }),
|
|
91
|
+
errors: takeRunErrors({ ...run, since, until }),
|
|
92
|
+
};
|
|
93
|
+
since = until;
|
|
94
|
+
return window;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function findAppErrors({ step, result, window, pageId }) {
|
|
98
|
+
const findings = await evaluateInvariants({
|
|
99
|
+
step,
|
|
100
|
+
result,
|
|
101
|
+
window,
|
|
102
|
+
pageId,
|
|
103
|
+
basePath,
|
|
104
|
+
configDirectory,
|
|
105
|
+
usesSearchStage: (entry) => usesSearchStage({ buildDirectory, entry }),
|
|
106
|
+
resolveSource: (configKey) =>
|
|
107
|
+
resolveConfigKeySource({ buildDirectory, configDirectory, configKey }),
|
|
108
|
+
});
|
|
109
|
+
return findings.filter(isAppError);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// A page mid-navigation, or one that has crashed, has nothing to report.
|
|
113
|
+
async function readPageId(page) {
|
|
114
|
+
return page.evaluate(() => window.lowdefy?.pageId ?? null).catch(() => null);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Judges what opening the journey's first page caused (its onInit and
|
|
118
|
+
// onMount requests included). Returns a failure with phase 'open', or
|
|
119
|
+
// undefined.
|
|
120
|
+
async function judgeOpen({ page }) {
|
|
121
|
+
await waitForClientErrorReports({ events });
|
|
122
|
+
const pageId = await readPageId(page);
|
|
123
|
+
const window = takeWindow({ page, observed: null, urlBefore: page.url() });
|
|
124
|
+
const findings = await findAppErrors({ step: {}, result: { status: 'ok' }, window, pageId });
|
|
125
|
+
if (findings.length === 0) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return toAppErrorFailure({ findings, phase: 'open' });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Opens a step's window on the page the step starts on. The observer is
|
|
132
|
+
// installed in each new document; a page mid-navigation cannot take it, and
|
|
133
|
+
// its window then has no trace emits (its error reports still count).
|
|
134
|
+
async function openWindow({ page }) {
|
|
135
|
+
await page.evaluate(installStepObserver).catch(() => null);
|
|
136
|
+
current = { page, urlBefore: page.url(), pageId: await readPageId(page) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Closes the step's window and returns the app errors it held, as findings
|
|
140
|
+
// ({ kind, severity, message, pageId, source, configKey, key }).
|
|
141
|
+
async function closeWindow({ journey, step, result }) {
|
|
142
|
+
if (!isQuietStep(step)) {
|
|
143
|
+
await waitForClientErrorReports({ events });
|
|
144
|
+
}
|
|
145
|
+
const { page, urlBefore, pageId } = current;
|
|
146
|
+
current = null;
|
|
147
|
+
const observed = page.isClosed()
|
|
148
|
+
? null
|
|
149
|
+
: await page
|
|
150
|
+
.evaluate(readStepObserver, { transientSelector: TRANSIENT_SELECTOR })
|
|
151
|
+
.catch(() => null);
|
|
152
|
+
const window = takeWindow({ page: journey.actors.current().page, observed, urlBefore });
|
|
153
|
+
closed = window;
|
|
154
|
+
return findAppErrors({ step, result, window, pageId });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The window of the step before the one running now: an expect step opens
|
|
158
|
+
// its own window, so the last one closed is the interaction's before it.
|
|
159
|
+
function previousWindow() {
|
|
160
|
+
return closed;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// After the last step: what its effects reported late (a client error
|
|
164
|
+
// report still in flight when its window closed) is judged against it.
|
|
165
|
+
async function drain({ journey, step, result }) {
|
|
166
|
+
await waitForClientErrorReports({ events });
|
|
167
|
+
const page = journey.actors.current().page;
|
|
168
|
+
const pageId = await readPageId(page);
|
|
169
|
+
const window = takeWindow({ page, observed: null, urlBefore: page.url() });
|
|
170
|
+
return findAppErrors({ step, result, window, pageId });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The app errors of an interaction an expect.error step follows, held for
|
|
174
|
+
// that step to claim instead of failing the interaction at once.
|
|
175
|
+
let held = null;
|
|
176
|
+
|
|
177
|
+
// Closes the step's window and returns the journey's failure once its app
|
|
178
|
+
// errors are counted (see withAppErrors), or undefined while it passes.
|
|
179
|
+
// `failure` is the step's own failure, if any. An interaction followed by
|
|
180
|
+
// an expect.error step holds its app errors for that step to claim (see
|
|
181
|
+
// claimExpectedErrors); it fails on them only if they are not claimed.
|
|
182
|
+
async function judgeStep({ journey, steps, index, results, failure, leftOrigin }) {
|
|
183
|
+
const step = steps[index];
|
|
184
|
+
const result = results[index];
|
|
185
|
+
const findings = await closeWindow({ journey, step, result });
|
|
186
|
+
if (isExpectError(step) && type.isUndefined(failure)) {
|
|
187
|
+
const claim = claimExpectedErrors({ step, index, held, findings });
|
|
188
|
+
held = null;
|
|
189
|
+
if (!type.isUndefined(claim)) {
|
|
190
|
+
results[claim.index].status = 'failed';
|
|
191
|
+
}
|
|
192
|
+
return claim;
|
|
193
|
+
}
|
|
194
|
+
if (
|
|
195
|
+
type.isUndefined(failure) &&
|
|
196
|
+
INTERACTION_STEP_KEYS.includes(getStepKey(step)) &&
|
|
197
|
+
isExpectError(steps[index + 1])
|
|
198
|
+
) {
|
|
199
|
+
held = { index, step, findings };
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
if (findings.length === 0) {
|
|
203
|
+
return failure;
|
|
204
|
+
}
|
|
205
|
+
result.status = 'failed';
|
|
206
|
+
return withAppErrors({ failure, leftOrigin, findings, index, step });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// After the last step passed, what its effects reported late (a client
|
|
210
|
+
// error report still in flight when its window closed) is judged against
|
|
211
|
+
// it.
|
|
212
|
+
async function judgeDrain({ journey, steps, results }) {
|
|
213
|
+
const index = steps.length - 1;
|
|
214
|
+
const step = steps[index];
|
|
215
|
+
const result = results[index];
|
|
216
|
+
const findings = await drain({ journey, step, result });
|
|
217
|
+
if (findings.length === 0) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
result.status = 'failed';
|
|
221
|
+
return withAppErrors({ findings, index, step });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
onContext,
|
|
226
|
+
judgeOpen,
|
|
227
|
+
openWindow,
|
|
228
|
+
judgeStep,
|
|
229
|
+
judgeDrain,
|
|
230
|
+
closeWindow,
|
|
231
|
+
drain,
|
|
232
|
+
previousWindow,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export default createJourneyAppErrors;
|
|
@@ -0,0 +1,385 @@
|
|
|
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 { jest } from '@jest/globals';
|
|
18
|
+
|
|
19
|
+
jest.unstable_mockModule('./waitForClientErrorReports.js', () => ({ default: async () => {} }));
|
|
20
|
+
|
|
21
|
+
const { default: createJourneyAppErrors } = await import('./createJourneyAppErrors.js');
|
|
22
|
+
const { default: installStepObserver } = await import('./installStepObserver.js');
|
|
23
|
+
const { default: readStepObserver } = await import('./readStepObserver.js');
|
|
24
|
+
const { recordRunError, registerRunBuffer, releaseRunBuffer } = await import(
|
|
25
|
+
'../runErrorBuffers.js'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const origin = 'http://localhost:3227';
|
|
29
|
+
const run = { run: '20261005T120000Z-ae0001', journey: null };
|
|
30
|
+
const recording = { source: 'journey', run: { id: run.run, by: 'test', journey: null } };
|
|
31
|
+
|
|
32
|
+
let emits;
|
|
33
|
+
|
|
34
|
+
function createPage() {
|
|
35
|
+
return {
|
|
36
|
+
evaluate: jest.fn(async (fn) => {
|
|
37
|
+
if (fn === installStepObserver) return true;
|
|
38
|
+
if (fn === readStepObserver) return { emits, mutationCount: 1 };
|
|
39
|
+
return 'orders';
|
|
40
|
+
}),
|
|
41
|
+
url: () => `${origin}/orders`,
|
|
42
|
+
isClosed: () => false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A fake browser context: the watch's listeners, called as Playwright would.
|
|
47
|
+
function createContext() {
|
|
48
|
+
const listeners = {};
|
|
49
|
+
return {
|
|
50
|
+
on: (event, callback) => {
|
|
51
|
+
listeners[event] = callback;
|
|
52
|
+
},
|
|
53
|
+
abort({ url }) {
|
|
54
|
+
listeners.requestfailed({
|
|
55
|
+
url: () => url,
|
|
56
|
+
failure: () => ({ errorText: 'net::ERR_ABORTED' }),
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
respond({ url, status }) {
|
|
60
|
+
listeners.response({
|
|
61
|
+
url: () => url,
|
|
62
|
+
status: () => status,
|
|
63
|
+
request: () => ({ method: () => 'POST' }),
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function serverEntry({ message, recordingRun = run.run }) {
|
|
70
|
+
return {
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
message,
|
|
73
|
+
source: '/app/pages/orders.yaml:12',
|
|
74
|
+
requestId: 'broken',
|
|
75
|
+
pageId: 'orders',
|
|
76
|
+
store: 'server',
|
|
77
|
+
recording: { source: 'journey', run: recordingRun, journey: null },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let page;
|
|
82
|
+
let journey;
|
|
83
|
+
let context;
|
|
84
|
+
let appErrors;
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
emits = [];
|
|
88
|
+
registerRunBuffer(run);
|
|
89
|
+
appErrors = createJourneyAppErrors({ origin, basePath: '', recording, configDirectory: '/app' });
|
|
90
|
+
context = createContext();
|
|
91
|
+
appErrors.onContext({ context });
|
|
92
|
+
page = createPage();
|
|
93
|
+
journey = { actors: { current: () => ({ page }) } };
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
releaseRunBuffer(run);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
async function runStepWindow({ step, during = () => {} }) {
|
|
101
|
+
await appErrors.openWindow({ page });
|
|
102
|
+
during();
|
|
103
|
+
return appErrors.closeWindow({ journey, step, result: { status: 'ok' } });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
test('a server error entry the run claimed fails the step, with its kind, source and key', async () => {
|
|
107
|
+
const findings = await runStepWindow({
|
|
108
|
+
step: { click: 'broken' },
|
|
109
|
+
during: () => recordRunError(serverEntry({ message: 'Unrecognized pipeline stage' })),
|
|
110
|
+
});
|
|
111
|
+
expect(findings).toEqual([
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
kind: 'server-error',
|
|
114
|
+
severity: 'error',
|
|
115
|
+
message: 'Unrecognized pipeline stage',
|
|
116
|
+
source: 'pages/orders.yaml:12',
|
|
117
|
+
key: 'server-error|orders|pages/orders.yaml:12',
|
|
118
|
+
}),
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("an error entry of another run is not this journey's", async () => {
|
|
123
|
+
const other = { run: '20261005T120000Z-ae0002', journey: null };
|
|
124
|
+
registerRunBuffer(other);
|
|
125
|
+
const findings = await runStepWindow({
|
|
126
|
+
step: { click: 'broken' },
|
|
127
|
+
during: () => recordRunError(serverEntry({ message: 'elsewhere', recordingRun: other.run })),
|
|
128
|
+
});
|
|
129
|
+
releaseRunBuffer(other);
|
|
130
|
+
expect(findings).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('an unexplained 5xx from an app API route fails the step as request-failed', async () => {
|
|
134
|
+
const findings = await runStepWindow({
|
|
135
|
+
step: { click: 'save' },
|
|
136
|
+
during: () => context.respond({ url: `${origin}/api/request/orders/save`, status: 500 }),
|
|
137
|
+
});
|
|
138
|
+
expect(findings.map((finding) => finding.kind)).toEqual(['request-failed']);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test.each([
|
|
142
|
+
['AuthorizationError', 403],
|
|
143
|
+
['AuthenticationError', 401],
|
|
144
|
+
['TwoFactorEnrolmentRequiredError', 403],
|
|
145
|
+
])(
|
|
146
|
+
'a CallAPI an auth gate refused (%s, %i) is not an app error: the refusal and its emit both pass',
|
|
147
|
+
async (errorName, status) => {
|
|
148
|
+
emits = [
|
|
149
|
+
{
|
|
150
|
+
blockId: 'admin_button',
|
|
151
|
+
eventName: 'onClick',
|
|
152
|
+
success: false,
|
|
153
|
+
failure: { errorName, actionId: 'call_admin', actionType: 'CallAPI', configKey: 'k1' },
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
const findings = await runStepWindow({
|
|
157
|
+
step: { click: 'admin_button' },
|
|
158
|
+
during: () => context.respond({ url: `${origin}/api/endpoints/admin_only`, status }),
|
|
159
|
+
});
|
|
160
|
+
expect(findings).toEqual([]);
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
test('a UserError and a dead click are not app errors', async () => {
|
|
165
|
+
emits = [
|
|
166
|
+
{
|
|
167
|
+
blockId: 'throw_button',
|
|
168
|
+
eventName: 'onClick',
|
|
169
|
+
success: false,
|
|
170
|
+
failure: { errorName: 'UserError', actionId: 'throw', actionType: 'Throw' },
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
const thrown = await runStepWindow({ step: { click: 'throw_button' } });
|
|
174
|
+
expect(thrown).toEqual([]);
|
|
175
|
+
emits = [];
|
|
176
|
+
page.evaluate.mockImplementation(async (fn) => {
|
|
177
|
+
if (fn === readStepObserver) return { emits: [], mutationCount: 0 };
|
|
178
|
+
return fn === installStepObserver ? true : 'orders';
|
|
179
|
+
});
|
|
180
|
+
const dead = await runStepWindow({ step: { click: 'dead_button' } });
|
|
181
|
+
expect(dead).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('an action that fails with an error that is not a UserError fails the step as action-error', async () => {
|
|
185
|
+
emits = [
|
|
186
|
+
{
|
|
187
|
+
blockId: 'explode_button',
|
|
188
|
+
eventName: 'onClick',
|
|
189
|
+
success: false,
|
|
190
|
+
failure: { errorName: 'RequestError', actionId: 'call_explode', actionType: 'CallAPI' },
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
const findings = await runStepWindow({ step: { click: 'explode_button' } });
|
|
194
|
+
expect(findings.map((finding) => finding.kind)).toEqual(['action-error']);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('an error that lands between two steps is judged in the next window, not lost', async () => {
|
|
198
|
+
expect(await runStepWindow({ step: { click: 'a' } })).toEqual([]);
|
|
199
|
+
recordRunError(serverEntry({ message: 'between steps' }));
|
|
200
|
+
const findings = await runStepWindow({ step: { expect: { visible: 'a' } } });
|
|
201
|
+
expect(findings.map((finding) => finding.message)).toEqual(['between steps']);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('judgeOpen fails with phase open on an error raised while the page opened', async () => {
|
|
205
|
+
recordRunError(serverEntry({ message: 'onInit failed' }));
|
|
206
|
+
const failure = await appErrors.judgeOpen({ page });
|
|
207
|
+
expect(failure).toEqual({
|
|
208
|
+
phase: 'open',
|
|
209
|
+
kind: 'app-error',
|
|
210
|
+
message: 'Opening the page caused an app error: server-error: onInit failed',
|
|
211
|
+
expected: 'no app error',
|
|
212
|
+
actual: ['server-error: onInit failed'],
|
|
213
|
+
errors: [
|
|
214
|
+
{
|
|
215
|
+
kind: 'server-error',
|
|
216
|
+
message: 'onInit failed',
|
|
217
|
+
source: 'pages/orders.yaml:12',
|
|
218
|
+
configKey: null,
|
|
219
|
+
key: 'server-error|orders|pages/orders.yaml:12',
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
expect(await appErrors.judgeOpen({ page })).toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('drain judges what arrived after the last window closed', async () => {
|
|
227
|
+
expect(await runStepWindow({ step: { click: 'a' } })).toEqual([]);
|
|
228
|
+
recordRunError(serverEntry({ message: 'late' }));
|
|
229
|
+
const findings = await appErrors.drain({
|
|
230
|
+
journey,
|
|
231
|
+
step: { click: 'a' },
|
|
232
|
+
result: { status: 'ok' },
|
|
233
|
+
});
|
|
234
|
+
expect(findings.map((finding) => finding.message)).toEqual(['late']);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Runs steps through judgeStep as runJourneySteps does, with `during(index)`
|
|
238
|
+
// raising what each step causes.
|
|
239
|
+
async function judgeSteps({ steps, during = () => {} }) {
|
|
240
|
+
const results = [];
|
|
241
|
+
let failure;
|
|
242
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
243
|
+
if (failure !== undefined) {
|
|
244
|
+
results.push({ index, status: 'skipped' });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
await appErrors.openWindow({ page });
|
|
248
|
+
during(index);
|
|
249
|
+
results.push({ index, status: 'ok' });
|
|
250
|
+
failure = await appErrors.judgeStep({
|
|
251
|
+
journey,
|
|
252
|
+
steps,
|
|
253
|
+
index,
|
|
254
|
+
results,
|
|
255
|
+
failure,
|
|
256
|
+
leftOrigin: false,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return { results, failure };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const duplicateSteps = [
|
|
263
|
+
{ click: 'save' },
|
|
264
|
+
{ expect: { error: 'duplicate key' } },
|
|
265
|
+
{ expect: { visible: 'already_exists' } },
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
test('expect.error claims the matching app error of the click before it, and the journey passes', async () => {
|
|
269
|
+
const { results, failure } = await judgeSteps({
|
|
270
|
+
steps: duplicateSteps,
|
|
271
|
+
during: (index) => {
|
|
272
|
+
if (index === 0) recordRunError(serverEntry({ message: 'E11000 duplicate key error' }));
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
expect(failure).toBeUndefined();
|
|
276
|
+
expect(results.map((result) => result.status)).toEqual(['ok', 'ok', 'ok']);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('expect.error fails at the expect step when no app error matches', async () => {
|
|
280
|
+
const { results, failure } = await judgeSteps({ steps: duplicateSteps });
|
|
281
|
+
expect(failure).toEqual({
|
|
282
|
+
index: 1,
|
|
283
|
+
step: { expect: { error: 'duplicate key' } },
|
|
284
|
+
expected: 'an app error containing "duplicate key"',
|
|
285
|
+
actual: 'no app error',
|
|
286
|
+
message: 'Expected an app error containing "duplicate key" but found no app error.',
|
|
287
|
+
});
|
|
288
|
+
expect(results.map((result) => result.status)).toEqual(['ok', 'failed', 'skipped']);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('an app error expect.error does not claim still fails the click', async () => {
|
|
292
|
+
const { results, failure } = await judgeSteps({
|
|
293
|
+
steps: duplicateSteps,
|
|
294
|
+
during: (index) => {
|
|
295
|
+
if (index !== 0) return;
|
|
296
|
+
recordRunError(serverEntry({ message: 'E11000 duplicate key error' }));
|
|
297
|
+
recordRunError({
|
|
298
|
+
...serverEntry({ message: 'Connection lost' }),
|
|
299
|
+
source: '/app/pages/orders.yaml:30',
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
expect(failure).toEqual(
|
|
304
|
+
expect.objectContaining({
|
|
305
|
+
index: 0,
|
|
306
|
+
step: { click: 'save' },
|
|
307
|
+
kind: 'app-error',
|
|
308
|
+
actual: ['server-error: Connection lost'],
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
expect(results.map((result) => result.status)).toEqual(['failed', 'ok', 'skipped']);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('an app error with no expect.error after it fails its step', async () => {
|
|
315
|
+
const { results, failure } = await judgeSteps({
|
|
316
|
+
steps: [{ click: 'save' }, { expect: { visible: 'saved' } }],
|
|
317
|
+
during: (index) => {
|
|
318
|
+
if (index === 0) recordRunError(serverEntry({ message: 'E11000 duplicate key error' }));
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
expect(failure).toEqual(expect.objectContaining({ index: 0, kind: 'app-error' }));
|
|
322
|
+
expect(results.map((result) => result.status)).toEqual(['failed', 'skipped']);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('expect.error claims the failed action that reported the error it matched', async () => {
|
|
326
|
+
const failedInsert = {
|
|
327
|
+
blockId: 'save',
|
|
328
|
+
eventName: 'onClick',
|
|
329
|
+
success: false,
|
|
330
|
+
failure: { errorName: 'ServiceError', actionId: 'insert', actionType: 'Request' },
|
|
331
|
+
};
|
|
332
|
+
const { failure } = await judgeSteps({
|
|
333
|
+
steps: duplicateSteps,
|
|
334
|
+
during: (index) => {
|
|
335
|
+
emits = index === 0 ? [failedInsert] : [];
|
|
336
|
+
if (index === 0) recordRunError(serverEntry({ message: 'E11000 duplicate key error' }));
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
expect(failure).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('previousWindow is the window the last step closed, for expect.effect to read', async () => {
|
|
343
|
+
expect(appErrors.previousWindow()).toBeNull();
|
|
344
|
+
emits = [{ blockId: 'save', eventName: 'onClick', success: true }];
|
|
345
|
+
await runStepWindow({ step: { click: 'save' } });
|
|
346
|
+
expect(appErrors.previousWindow()).toEqual(
|
|
347
|
+
expect.objectContaining({
|
|
348
|
+
emits,
|
|
349
|
+
mutationCount: 1,
|
|
350
|
+
requests: [],
|
|
351
|
+
urlBefore: `${origin}/orders`,
|
|
352
|
+
urlAfter: `${origin}/orders`,
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
function clientEntry({ message }) {
|
|
358
|
+
return {
|
|
359
|
+
timestamp: new Date().toISOString(),
|
|
360
|
+
name: 'TypeError',
|
|
361
|
+
message,
|
|
362
|
+
source: null,
|
|
363
|
+
store: 'client',
|
|
364
|
+
recording: { source: 'journey', run: run.run, journey: null },
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
test("a goto whose navigation aborted the old page's requests is not failed by their fetch errors", async () => {
|
|
369
|
+
const findings = await runStepWindow({
|
|
370
|
+
step: { goto: 'onboarding-step-connect' },
|
|
371
|
+
during: () => {
|
|
372
|
+
context.abort({ url: `${origin}/api/request/step/get_header` });
|
|
373
|
+
recordRunError(clientEntry({ message: 'Failed to fetch at onboarding-step-2.' }));
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
expect(findings).toEqual([]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('a failed fetch with no aborted request still fails a goto step', async () => {
|
|
380
|
+
const findings = await runStepWindow({
|
|
381
|
+
step: { goto: 'onboarding-step-connect' },
|
|
382
|
+
during: () => recordRunError(clientEntry({ message: 'Failed to fetch at onboarding-step-2.' })),
|
|
383
|
+
});
|
|
384
|
+
expect(findings.map((finding) => finding.kind)).toEqual(['client-error']);
|
|
385
|
+
});
|