@lowdefy/shell 3.23.3 → 7.2.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/LICENSE +183 -183
- package/README.md +364 -2
- package/app/(t0,t1,t2,t3,t4)/[...path].js +38 -0
- package/app/(t0,t1,t2,t3,t4)/_layout.js +48 -0
- package/app/(t0,t1,t2,t3,t4)/index.js +38 -0
- package/app/+native-intent.js +39 -0
- package/app/_layout.js +36 -0
- package/app.config.ts +183 -0
- package/appLinks.js +65 -0
- package/applyShellPlugins.js +43 -0
- package/authCallbackIntentFilters.js +46 -0
- package/babel.config.js +25 -0
- package/checkShellConfig.js +172 -0
- package/checkShellPlugins.js +74 -0
- package/getPrivacyManifest.js +39 -0
- package/metro.config.js +73 -0
- package/native/LowdefyAppShortcuts.swift.template +84 -0
- package/native/withAppShortcuts.js +60 -0
- package/native-fingerprint.json +16 -0
- package/package.json +120 -40
- package/profiles.js +607 -0
- package/readShellConfig.js +36 -0
- package/resolveShellPlugins.js +109 -0
- package/runtimeVersion.js +112 -0
- package/scripts/checkReleaseBundle.mjs +358 -0
- package/scripts/createDayjsLocales.mjs +67 -0
- package/scripts/createShellAssets.mjs +54 -0
- package/scripts/createShellVersion.mjs +83 -0
- package/scripts/createTypeManifest.mjs +205 -0
- package/shell.dev.json +11 -0
- package/shellVariant.js +126 -0
- package/src/ShellContext.js +32 -0
- package/src/actions/CopyToClipboard.js +29 -0
- package/src/actions/CopyToClipboard.test.js +33 -0
- package/src/actions/GeolocationCurrentPosition.js +61 -0
- package/src/actions/GeolocationCurrentPosition.test.js +79 -0
- package/src/actions/GetLocalStorage.js +36 -0
- package/src/actions/RemoveLocalStorage.js +31 -0
- package/src/actions/ScrollTo.js +66 -0
- package/src/actions/ScrollTo.test.js +83 -0
- package/src/actions/SetFocus.js +33 -0
- package/src/actions/SetFocus.test.js +49 -0
- package/src/actions/SetLocalStorage.js +34 -0
- package/src/actions/localStorage.test.js +68 -0
- package/src/actions/measureInScrollView.js +36 -0
- package/src/actions/nativeActions.js +41 -0
- package/src/auth/AuthButton.js +44 -0
- package/src/auth/AuthGate.js +57 -0
- package/src/auth/AuthLink.js +43 -0
- package/src/auth/LockScreen.js +67 -0
- package/src/auth/LockScreen.test.js +37 -0
- package/src/auth/SignInScreen.js +268 -0
- package/src/auth/SignInScreen.test.js +187 -0
- package/src/auth/createAppAuth.js +325 -0
- package/src/auth/createAppAuth.test.js +424 -0
- package/src/auth/createAuthApi.js +115 -0
- package/src/auth/createAuthApi.test.js +138 -0
- package/src/auth/createCoreAuth.js +96 -0
- package/src/auth/createCoreAuth.test.js +188 -0
- package/src/auth/createDeviceKey.js +62 -0
- package/src/auth/createDeviceKey.test.js +54 -0
- package/src/auth/createSessionStores.js +45 -0
- package/src/auth/createTokenStore.js +103 -0
- package/src/auth/createTokenStore.test.js +116 -0
- package/src/auth/getAuthSettings.js +24 -0
- package/src/auth/getAuthView.js +39 -0
- package/src/auth/getAuthView.test.js +52 -0
- package/src/auth/getSheetReturnUrl.js +35 -0
- package/src/auth/getTokenStoreOptions.js +46 -0
- package/src/auth/isBackgroundShell.js +26 -0
- package/src/auth/isSheetReturnLink.js +35 -0
- package/src/auth/parseQuery.js +39 -0
- package/src/auth/purgeAppData.js +80 -0
- package/src/auth/purgeAppData.test.js +104 -0
- package/src/auth/shouldLock.js +26 -0
- package/src/auth/signInWithSheet.js +97 -0
- package/src/auth/signInWithSheet.test.js +205 -0
- package/src/auth/styles.js +78 -0
- package/src/auth/unregisterPushToken.js +62 -0
- package/src/auth/unregisterPushToken.test.js +67 -0
- package/src/auth/usesBiometricUnlock.js +23 -0
- package/src/auth/watchSession.js +47 -0
- package/src/auth/withoutCookies.js +26 -0
- package/src/cache/createSqliteCacheStore.js +113 -0
- package/src/cache/createSqliteCacheStore.test.js +93 -0
- package/src/cache/getCacheKey.js +25 -0
- package/src/cache/getLastCacheKeyName.js +22 -0
- package/src/components/AppProvider.js +118 -0
- package/src/components/Frame.js +116 -0
- package/src/components/Frame.test.js +101 -0
- package/src/components/LoadingScreen.js +35 -0
- package/src/components/MenuButton.js +121 -0
- package/src/components/MenuButton.test.js +74 -0
- package/src/components/MessageScreen.js +51 -0
- package/src/components/NavigationTheme.js +33 -0
- package/src/components/NoConnectionBanner.js +51 -0
- package/src/components/NoConnectionBanner.test.js +73 -0
- package/src/components/OtherPageScreen.js +95 -0
- package/src/components/OtherPageScreen.test.js +74 -0
- package/src/components/PageScreen.js +254 -0
- package/src/components/PageScreen.test.js +492 -0
- package/src/components/RouteErrorScreen.js +47 -0
- package/src/components/ShellRoot.js +34 -0
- package/src/components/ShellRoot.test.js +62 -0
- package/src/components/ShellThemeProvider.js +45 -0
- package/src/components/SwitchToTab.js +36 -0
- package/src/components/SwitchToTab.test.js +63 -0
- package/src/components/TabIconImages.js +72 -0
- package/src/components/UpdateScreen.js +40 -0
- package/src/components/getNavigationTheme.js +44 -0
- package/src/components/getNavigationTheme.test.js +41 -0
- package/src/components/styles.js +50 -0
- package/src/components/useSurfaceTokens.js +47 -0
- package/src/config/getAppApiBase.js +34 -0
- package/src/config/getShellConfig.js +37 -0
- package/src/config/getShellConfig.test.js +80 -0
- package/src/createShellApp.js +126 -0
- package/src/dev/DevShell.js +239 -0
- package/src/dev/DevShell.test.js +233 -0
- package/src/dev/connect/ConfirmSheet.js +71 -0
- package/src/dev/connect/ConnectScreen.js +117 -0
- package/src/dev/connect/QrScanner.js +89 -0
- package/src/dev/connection/createDevShellConfig.js +47 -0
- package/src/dev/connection/createDevTransport.js +51 -0
- package/src/dev/connection/createDevTransport.test.js +87 -0
- package/src/dev/connection/createServerList.js +48 -0
- package/src/dev/connection/isLoopbackHost.js +25 -0
- package/src/dev/connection/pairedShellApp.test.js +104 -0
- package/src/dev/connection/parseDevLink.js +66 -0
- package/src/dev/connection/parseDevLink.test.js +97 -0
- package/src/dev/connection/parseQuery.js +33 -0
- package/src/dev/connection/parseServerInput.js +40 -0
- package/src/dev/connection/parseServerUrl.js +45 -0
- package/src/dev/devStyles.js +97 -0
- package/src/dev/index.js +21 -0
- package/src/dev/inject/createDeviceInjections.js +188 -0
- package/src/dev/inject/createDeviceInjections.test.js +224 -0
- package/src/dev/inject/createOfflineFetch.js +35 -0
- package/src/dev/inject/createOfflineFetch.test.js +68 -0
- package/src/dev/inject/injectPush.js +60 -0
- package/src/dev/inject/injectShare.js +55 -0
- package/src/dev/inject/withInjectedDevice.js +98 -0
- package/src/dev/menu/BlockGallery.js +33 -0
- package/src/dev/menu/DevMenu.js +121 -0
- package/src/dev/menu/createLogBuffer.js +83 -0
- package/src/dev/menu/watchShake.js +52 -0
- package/src/dev/menu/watchShake.test.js +61 -0
- package/src/dev/recorder/createNativeRecorder.js +204 -0
- package/src/dev/recorder/createNativeRecorder.test.js +307 -0
- package/src/dev/redirectDevLink.js +30 -0
- package/src/dev/redirectDevLink.test.js +49 -0
- package/src/dev/steps/createStepRunner.js +184 -0
- package/src/dev/steps/createStepRunner.test.js +262 -0
- package/src/dev/steps/findBlock.js +78 -0
- package/src/dev/steps/isContextSettled.js +37 -0
- package/src/dev/steps/waitForSettle.js +39 -0
- package/src/dev/steps/waitUntil.js +40 -0
- package/src/dev/tab/DevTabClient.js +97 -0
- package/src/dev/tab/answerEval.js +39 -0
- package/src/dev/tab/answerInspect.js +45 -0
- package/src/dev/tab/createDevTabClient.js +272 -0
- package/src/dev/tab/createDevTabClient.test.js +317 -0
- package/src/dev/tab/describeTabError.js +35 -0
- package/src/dev/tab/devClientMarker.js +22 -0
- package/src/dev/tab/devClientMarker.test.js +23 -0
- package/src/dev/tab/findPageContext.js +43 -0
- package/src/dev/tab/getShownPage.js +42 -0
- package/src/dev/tab/readEventStream.js +68 -0
- package/src/dev/tab/reloadApp.js +42 -0
- package/src/dev/tab/reloadApp.test.js +115 -0
- package/src/device/DeviceHosts.js +32 -0
- package/src/device/basic/createFilesFeature.js +41 -0
- package/src/device/basic/createHapticsFeature.js +46 -0
- package/src/device/basic/createKeepAwakeFeature.js +37 -0
- package/src/device/basic/createPhotosFeature.js +40 -0
- package/src/device/basic/createRequestQueueFeature.js +37 -0
- package/src/device/basic/createShareFeature.js +63 -0
- package/src/device/basic/createShareFeature.test.js +47 -0
- package/src/device/basic/createSpeechFeature.js +43 -0
- package/src/device/basic/createUploadFeature.js +41 -0
- package/src/device/basic/toImagePickerOptions.js +41 -0
- package/src/device/basic/toPickedFile.js +54 -0
- package/src/device/calendar/index.js +68 -0
- package/src/device/contacts/index.js +67 -0
- package/src/device/createDevice.js +65 -0
- package/src/device/device.js +27 -0
- package/src/device/field/MapView.js +93 -0
- package/src/device/field/ScanCodeSheet.js +86 -0
- package/src/device/field/ScannerView.js +52 -0
- package/src/device/field/createAudioFeature.js +70 -0
- package/src/device/field/createBrightnessFeature.js +40 -0
- package/src/device/field/createCameraFeature.js +56 -0
- package/src/device/field/createDocumentScannerFeature.js +51 -0
- package/src/device/field/createLocationFeature.js +39 -0
- package/src/device/field/createScanRequests.js +55 -0
- package/src/device/field/index.js +39 -0
- package/src/device/field/scanRequests.js +22 -0
- package/src/device/injected.js +60 -0
- package/src/device/injected.test.js +39 -0
- package/src/device/nfc/index.js +89 -0
- package/src/device/nfc/toNfcRecord.js +44 -0
- package/src/device/nfc/toNfcRecord.test.js +48 -0
- package/src/device/tracking/createBacklog.js +63 -0
- package/src/device/tracking/createBacklog.test.js +47 -0
- package/src/device/tracking/index.js +211 -0
- package/src/device/tracking/index.test.js +110 -0
- package/src/host/createDeviceStore.js +32 -0
- package/src/host/createMemoryStore.js +32 -0
- package/src/host/createNetworkStatus.js +94 -0
- package/src/host/createNetworkStatus.test.js +94 -0
- package/src/host/createRouterAdapter.js +33 -0
- package/src/host/createShellFetch.js +62 -0
- package/src/host/createShellFetch.test.js +165 -0
- package/src/host/createShellGlobals.js +62 -0
- package/src/host/createShellHost.js +83 -0
- package/src/host/createShellHost.test.js +36 -0
- package/src/host/getOrigin.js +31 -0
- package/src/host/isNetworkError.js +24 -0
- package/src/host/isOnline.js +22 -0
- package/src/host/networkStatus.js +24 -0
- package/src/host/readServerError.js +45 -0
- package/src/host/readServerError.test.js +85 -0
- package/src/host/reportShellError.js +47 -0
- package/src/host/reportShellError.test.js +79 -0
- package/src/host/withNetworkErrors.js +38 -0
- package/src/host/withNetworkErrors.test.js +61 -0
- package/src/host/withShellHeader.js +25 -0
- package/src/host/withShellHeader.test.js +28 -0
- package/src/js/evaluateJsEntries.js +43 -0
- package/src/js/evaluateJsEntries.test.js +48 -0
- package/src/locale/createShellLocale.js +112 -0
- package/src/locale/createShellLocale.test.js +74 -0
- package/src/locale/dayjsLocales.js +165 -0
- package/src/locale/getDeviceLocales.js +23 -0
- package/src/locale/loadDayjsLocale.js +32 -0
- package/src/locale/resolveActiveLocale.js +46 -0
- package/src/locale/resolveActiveLocale.test.js +40 -0
- package/src/native/backgroundFetch.js +97 -0
- package/src/native/callEndpoint.js +63 -0
- package/src/native/createAppEventTrigger.js +70 -0
- package/src/native/currentApp.js +49 -0
- package/src/native/getPagePath.js +32 -0
- package/src/native/openPath.js +55 -0
- package/src/native/openPath.test.js +39 -0
- package/src/native/parseShellLink.js +38 -0
- package/src/native/parseShellLink.test.js +29 -0
- package/src/native/startAppServices.js +94 -0
- package/src/native/startAppServices.test.js +97 -0
- package/src/native/startShareIn.js +59 -0
- package/src/native/startShortcuts.js +88 -0
- package/src/native/startShortcuts.test.js +59 -0
- package/src/native/toSharePayload.js +47 -0
- package/src/native/toSharePayload.test.js +47 -0
- package/src/native/whenAppReady.js +48 -0
- package/src/navigation/SlotContext.js +24 -0
- package/src/navigation/frameSlots.js +27 -0
- package/src/navigation/getFrame.js +30 -0
- package/src/navigation/getInitialScreenOptions.js +63 -0
- package/src/navigation/getInitialScreenOptions.test.js +66 -0
- package/src/navigation/getPresentation.js +28 -0
- package/src/navigation/getSlotRootPath.js +27 -0
- package/src/navigation/getStackOptions.js +185 -0
- package/src/navigation/getTabHref.js +45 -0
- package/src/navigation/getTabIcons.js +43 -0
- package/src/navigation/isPageUnderHeader.js +46 -0
- package/src/navigation/navigation.test.js +342 -0
- package/src/navigation/toAppLinkPath.js +43 -0
- package/src/navigation/toAppLinkPath.test.js +53 -0
- package/src/navigation/toRouterHref.js +29 -0
- package/src/notifications/createNotificationsFeature.js +61 -0
- package/src/notifications/getCategoryAction.js +31 -0
- package/src/notifications/handleNotificationResponse.js +82 -0
- package/src/notifications/registerPushToken.js +71 -0
- package/src/notifications/startNotifications.js +114 -0
- package/src/notifications/toCategoryActions.js +42 -0
- package/src/notifications/toNotificationEvent.js +36 -0
- package/src/notifications/toScheduleRequest.js +62 -0
- package/src/plugins/appTypes.js +28 -0
- package/src/plugins/prefixTypes.js +28 -0
- package/src/plugins/shellPlugins.json +15 -0
- package/src/plugins/typeManifest.js +789 -0
- package/src/plugins/types.js +64 -0
- package/src/plugins/types.test.js +173 -0
- package/src/polyfills/installGetRandomValues.js +28 -0
- package/src/queue/createRequestQueue.js +198 -0
- package/src/queue/createRequestQueue.test.js +215 -0
- package/src/queue/createSqliteQueueStore.js +121 -0
- package/src/queue/summarizeQueue.js +39 -0
- package/src/session/createAppSession.js +323 -0
- package/src/session/createAppSession.test.js +391 -0
- package/src/session/getPageView.js +59 -0
- package/src/session/getPageView.test.js +137 -0
- package/src/sync/encodePagePath.js +27 -0
- package/src/sync/fetchPage.js +72 -0
- package/src/sync/fetchPage.test.js +84 -0
- package/src/sync/fetchRoot.js +43 -0
- package/src/sync/fetchUser.js +33 -0
- package/src/sync/runLimited.js +36 -0
- package/src/sync/toRootConfig.js +35 -0
- package/src/test/createFakeNetwork.js +40 -0
- package/src/test/createFakeSecureStore.js +40 -0
- package/src/test/createFakeServer.js +98 -0
- package/src/test/createMemoryCacheStore.js +59 -0
- package/src/test/createNetworkError.js +24 -0
- package/src/test/expoImage.js +42 -0
- package/src/test/expoNotifications.js +40 -0
- package/src/test/expoQuickActions.js +22 -0
- package/src/test/expoSpeechRecognition.js +31 -0
- package/src/test/expoTaskManager.js +25 -0
- package/src/test/expoUi.js +55 -0
- package/src/test/expoUiBottomSheet.js +64 -0
- package/src/test/expoUiDateTimePicker.js +26 -0
- package/src/test/expoUiMenu.js +46 -0
- package/src/test/expoUiSegmentedControl.js +47 -0
- package/src/test/expoUiSwiftUi.js +62 -0
- package/src/test/expoUiSwiftUiModifiers.js +23 -0
- package/src/test/expoWebBrowser.js +23 -0
- package/src/test/fixtures.js +98 -0
- package/src/test/nativeModule.js +33 -0
- package/src/test/renderPageScreen.js +109 -0
- package/src/types/checkTypeSet.js +39 -0
- package/src/types/collectPageTypes.js +49 -0
- package/src/types/collectPageTypes.test.js +90 -0
- package/src/types/findMissingPageTypes.js +40 -0
- package/src/version/compareVersions.js +38 -0
- package/src/version/isShellTooOld.js +30 -0
- package/src/version/shellVersion.js +21 -0
- package/src/version/shellVersion.test.js +32 -0
- package/src/widgets/fetchWidgetData.js +59 -0
- package/src/widgets/index.js +156 -0
- package/src/widgets/refreshWidgets.js +31 -0
- package/withExcludedNativeModules.js +93 -0
- package/dist/location.js +0 -33
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/icon-32.png +0 -0
- package/dist/public/icon-512.png +0 -0
- package/dist/public/icon.svg +0 -17
- package/dist/public/logo-dark-theme.png +0 -0
- package/dist/public/logo-light-theme.png +0 -0
- package/dist/public/logo-square-dark-theme.png +0 -0
- package/dist/public/logo-square-light-theme.png +0 -0
- package/dist/public/manifest.webmanifest +0 -16
- package/dist/shell/246.93432ff68186aae6c87b.js +0 -2
- package/dist/shell/246.93432ff68186aae6c87b.js.LICENSE.txt +0 -8
- package/dist/shell/454.b597ca21366afda000af.js +0 -2
- package/dist/shell/454.b597ca21366afda000af.js.LICENSE.txt +0 -14
- package/dist/shell/484.d645e6c09079f45ce044.js +0 -1
- package/dist/shell/706.a66931e02a549fd269bd.js +0 -2
- package/dist/shell/706.a66931e02a549fd269bd.js.LICENSE.txt +0 -8
- package/dist/shell/9.78be8c1b6c3986f404fc.js +0 -2
- package/dist/shell/9.78be8c1b6c3986f404fc.js.LICENSE.txt +0 -45
- package/dist/shell/922.43b7a3ebe22ef05b3049.js +0 -2
- package/dist/shell/922.43b7a3ebe22ef05b3049.js.LICENSE.txt +0 -14
- package/dist/shell/index.html +0 -57
- package/dist/shell/main.f01501a75909f38ccc32.js +0 -1
- package/dist/shell/runtime.e50f6c3a5f7c74a0a717.js +0 -1
|
@@ -0,0 +1,204 @@
|
|
|
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 {
|
|
18
|
+
createPairingBuffer,
|
|
19
|
+
createPasswordRedactor,
|
|
20
|
+
createRecordBatcher,
|
|
21
|
+
createRecordingSession,
|
|
22
|
+
sendRecords,
|
|
23
|
+
} from '@lowdefy/client-core';
|
|
24
|
+
import { describeBlock } from '@lowdefy/client-native';
|
|
25
|
+
import { getTrace } from '@lowdefy/engine';
|
|
26
|
+
import { type } from '@lowdefy/helpers';
|
|
27
|
+
|
|
28
|
+
import getShownPage from '../tab/getShownPage.js';
|
|
29
|
+
|
|
30
|
+
const PAGE_CHECK_MS = 500;
|
|
31
|
+
|
|
32
|
+
// A TextInput of type password draws a secure field; its value is never recorded.
|
|
33
|
+
function isPasswordBlock(block) {
|
|
34
|
+
return block?.eval?.properties?.type === 'password';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function findBlock({ blockId, lowdefy, page }) {
|
|
38
|
+
return (
|
|
39
|
+
lowdefy.contexts?.[page?.instanceKey]?._internal.RootSlots?.map?.[blockId] ??
|
|
40
|
+
lowdefy.appContext?._internal.RootSlots?.map?.[blockId] ??
|
|
41
|
+
null
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function currentUrl(lowdefy) {
|
|
46
|
+
const { location } = lowdefy._internal.globals.window;
|
|
47
|
+
return `${location.pathname}${location.search}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The device store as the sessionStorage the recording session keeps its id in, so a session lasts
|
|
51
|
+
// across reloads and a restart of the app within 10 minutes.
|
|
52
|
+
function createSessionStorage(kv) {
|
|
53
|
+
return {
|
|
54
|
+
getItem: (key) => kv.get(key) ?? null,
|
|
55
|
+
setItem: (key, value) => kv.set(key, value),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Lowdefy Dev's recorder: what a developer does on the phone, written as the web recorder writes
|
|
61
|
+
* it (server-dev client/recorder/createRecorder.js), so `lowdefy journeys session` and the
|
|
62
|
+
* journeys-from-dev skill read phone sessions as they read browser ones.
|
|
63
|
+
*
|
|
64
|
+
* - The engine trace's interactions (client-native reports presses and changes by block id) pair
|
|
65
|
+
* with its completed events through the shared pairing buffer (@lowdefy/client-core).
|
|
66
|
+
* - A pageview is recorded when the page on screen changes, checked every 500 ms and before each
|
|
67
|
+
* interaction and event.
|
|
68
|
+
* - Targets are block ids (client-native describeBlock). Password inputs are redacted.
|
|
69
|
+
* - Each record is labelled with the device (`ios` or `android`) and the shell's version, and
|
|
70
|
+
* batches are posted to the dev server's /api/dev-recording.
|
|
71
|
+
* - A reload of the app makes a new lowdefy object: the recorder follows it.
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} params
|
|
74
|
+
* @param {string} params.apiBase The dev server's URL.
|
|
75
|
+
* @param {string} params.device 'ios' or 'android'.
|
|
76
|
+
* @param {Function} params.fetch The Fetch API, with the pairing and shell headers.
|
|
77
|
+
* @param {() => string|null} params.getBuild The build of the root the shell holds.
|
|
78
|
+
* @param {() => Object|null} params.getLowdefy The open app's lowdefy object.
|
|
79
|
+
* @param {Object} params.kv The device store.
|
|
80
|
+
* @param {string} params.shellVersion
|
|
81
|
+
* @param {() => number} [params.now]
|
|
82
|
+
*/
|
|
83
|
+
function createNativeRecorder({
|
|
84
|
+
apiBase,
|
|
85
|
+
device,
|
|
86
|
+
fetch,
|
|
87
|
+
getBuild,
|
|
88
|
+
getLowdefy,
|
|
89
|
+
kv,
|
|
90
|
+
now = Date.now,
|
|
91
|
+
shellVersion,
|
|
92
|
+
}) {
|
|
93
|
+
const session = createRecordingSession({ storage: createSessionStorage(kv), now });
|
|
94
|
+
const batcher = createRecordBatcher({
|
|
95
|
+
send: (records) => sendRecords({ basePath: apiBase, records, fetch }),
|
|
96
|
+
});
|
|
97
|
+
let lowdefy = null;
|
|
98
|
+
let detach = () => undefined;
|
|
99
|
+
let shown = null;
|
|
100
|
+
let timer = null;
|
|
101
|
+
|
|
102
|
+
const buffer = createPairingBuffer({
|
|
103
|
+
onRecord: (record) => batcher.add({ ...record, device, shell_version: shellVersion }),
|
|
104
|
+
now,
|
|
105
|
+
getSession: () => session.getId(),
|
|
106
|
+
getRoles: () => lowdefy?.user?.roles ?? [],
|
|
107
|
+
getBuild,
|
|
108
|
+
redactor: createPasswordRedactor({ isPassword: isPasswordBlock }),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Records a pageview when the page instance on screen changed. `t` places it before the
|
|
112
|
+
// interaction or event that found the change, which happened on the new page.
|
|
113
|
+
function checkPage({ t } = {}) {
|
|
114
|
+
if (lowdefy === null) return;
|
|
115
|
+
const page = getShownPage(lowdefy);
|
|
116
|
+
if (page === null || page.instanceKey === shown?.instanceKey) return;
|
|
117
|
+
shown = page;
|
|
118
|
+
buffer.addInteraction({
|
|
119
|
+
t: t ?? now(),
|
|
120
|
+
kind: 'pageview',
|
|
121
|
+
pageId: page.pageId,
|
|
122
|
+
pathParams: page.pathParams,
|
|
123
|
+
url: currentUrl(lowdefy),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function onInteraction(interaction) {
|
|
128
|
+
try {
|
|
129
|
+
checkPage({ t: interaction.t - 1 });
|
|
130
|
+
const target = describeBlock({ blockId: interaction.blockId, lowdefy });
|
|
131
|
+
buffer.addInteraction({
|
|
132
|
+
t: interaction.t,
|
|
133
|
+
kind: interaction.kind,
|
|
134
|
+
element: findBlock({ blockId: interaction.blockId, lowdefy, page: shown }),
|
|
135
|
+
target,
|
|
136
|
+
pageId: target.page_id ?? shown?.pageId,
|
|
137
|
+
value: interaction.value,
|
|
138
|
+
key: interaction.key,
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
// Recording is best effort: it never gets in the app's way.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function onTrace(payload) {
|
|
146
|
+
try {
|
|
147
|
+
checkPage();
|
|
148
|
+
buffer.addEvent(payload, { urlAfter: currentUrl(lowdefy) });
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// Recording is best effort.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function flush() {
|
|
155
|
+
try {
|
|
156
|
+
buffer.flushAll();
|
|
157
|
+
return batcher.flush();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Follows the open app's lowdefy object, which a reload replaces.
|
|
164
|
+
function follow() {
|
|
165
|
+
const next = getLowdefy();
|
|
166
|
+
if (next === lowdefy) {
|
|
167
|
+
checkPage();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
detach();
|
|
171
|
+
flush();
|
|
172
|
+
lowdefy = next;
|
|
173
|
+
shown = null;
|
|
174
|
+
if (type.isNone(lowdefy)) {
|
|
175
|
+
lowdefy = null;
|
|
176
|
+
detach = () => undefined;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const trace = getTrace(lowdefy);
|
|
180
|
+
const unsubscribeEvents = trace.subscribe(onTrace, { state: true });
|
|
181
|
+
const unsubscribeInteractions = trace.subscribeInteractions(onInteraction);
|
|
182
|
+
detach = () => {
|
|
183
|
+
unsubscribeEvents();
|
|
184
|
+
unsubscribeInteractions();
|
|
185
|
+
};
|
|
186
|
+
checkPage();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function start() {
|
|
190
|
+
follow();
|
|
191
|
+
timer = setInterval(follow, PAGE_CHECK_MS);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function stop() {
|
|
195
|
+
clearInterval(timer);
|
|
196
|
+
detach();
|
|
197
|
+
detach = () => undefined;
|
|
198
|
+
return flush();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { flush, start, stop };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export default createNativeRecorder;
|
|
@@ -0,0 +1,307 @@
|
|
|
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 React from 'react';
|
|
18
|
+
import { jest } from '@jest/globals';
|
|
19
|
+
import { Pressable, Text, TextInput as RNTextInput, View } from 'react-native';
|
|
20
|
+
import { act, fireEvent, render, screen } from '@testing-library/react-native';
|
|
21
|
+
import { createNativeGlobals, LowdefyPage } from '@lowdefy/client-native';
|
|
22
|
+
import { SetState } from '@lowdefy/actions-core/actions';
|
|
23
|
+
import { _state } from '@lowdefy/operators-js/operators/client';
|
|
24
|
+
import validateTraceRecord from '@lowdefy/node-utils/journeyTrace/validateTraceRecord.js';
|
|
25
|
+
|
|
26
|
+
import createMemoryStore from '../../host/createMemoryStore.js';
|
|
27
|
+
import createNativeRecorder from './createNativeRecorder.js';
|
|
28
|
+
|
|
29
|
+
// The records the dev server's recording validation runs over (server-dev
|
|
30
|
+
// src/routes/devRecording.test.mjs): what this recorder writes for a click, a fill and a select.
|
|
31
|
+
const fixture = require('../../../../servers/server-dev/test/nativeRecordingFixture.json');
|
|
32
|
+
|
|
33
|
+
// Stand-ins for native blocks that call their methods as blocks-native's do.
|
|
34
|
+
function Box({ content }) {
|
|
35
|
+
return <View>{content.content?.()}</View>;
|
|
36
|
+
}
|
|
37
|
+
Box.meta = { category: 'container', slots: { content: 'Child blocks.' } };
|
|
38
|
+
|
|
39
|
+
function Button({ blockId, methods, properties }) {
|
|
40
|
+
return (
|
|
41
|
+
<Pressable
|
|
42
|
+
accessibilityRole="button"
|
|
43
|
+
onPress={() => methods.triggerEvent({ name: 'onClick' })}
|
|
44
|
+
testID={`${blockId}-press`}
|
|
45
|
+
>
|
|
46
|
+
<Text>{properties.title}</Text>
|
|
47
|
+
</Pressable>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
Button.meta = { category: 'display' };
|
|
51
|
+
|
|
52
|
+
function TextInput({ blockId, methods, properties, value }) {
|
|
53
|
+
return (
|
|
54
|
+
<RNTextInput
|
|
55
|
+
onChangeText={(text) => {
|
|
56
|
+
methods.setValue(text);
|
|
57
|
+
methods.triggerEvent({ name: 'onChange', event: { value: text } });
|
|
58
|
+
}}
|
|
59
|
+
onSubmitEditing={() => methods.triggerEvent({ name: 'onPressEnter' })}
|
|
60
|
+
secureTextEntry={properties.type === 'password'}
|
|
61
|
+
testID={`${blockId}_input`}
|
|
62
|
+
value={value ?? ''}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
TextInput.meta = { category: 'input', valueType: 'string' };
|
|
67
|
+
|
|
68
|
+
function Selector({ blockId, methods, properties }) {
|
|
69
|
+
return (
|
|
70
|
+
<View>
|
|
71
|
+
{properties.options.map((option) => (
|
|
72
|
+
<Pressable
|
|
73
|
+
key={option.value}
|
|
74
|
+
onPress={() => {
|
|
75
|
+
methods.setValue(option.value);
|
|
76
|
+
methods.triggerEvent({ name: 'onChange', event: { value: option.value } });
|
|
77
|
+
}}
|
|
78
|
+
testID={`${blockId}-${option.value}`}
|
|
79
|
+
>
|
|
80
|
+
<Text>{option.label}</Text>
|
|
81
|
+
</Pressable>
|
|
82
|
+
))}
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
Selector.meta = { category: 'input', valueType: 'any' };
|
|
87
|
+
|
|
88
|
+
const types = {
|
|
89
|
+
actions: { SetState },
|
|
90
|
+
blocks: { Box, Button, Selector, TextInput },
|
|
91
|
+
operators: { _state },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function block({ id, type, events, ...rest }) {
|
|
95
|
+
const built = { id: `block:ticket:${id}:0`, blockId: id, type, ...rest };
|
|
96
|
+
if (events) {
|
|
97
|
+
built.events = Object.fromEntries(
|
|
98
|
+
Object.entries(events).map(([name, actions]) => [name, { try: actions, catch: [] }])
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return built;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const pageConfig = {
|
|
105
|
+
id: 'page:ticket',
|
|
106
|
+
pageId: 'ticket',
|
|
107
|
+
blockId: 'ticket',
|
|
108
|
+
type: 'Box',
|
|
109
|
+
path: 'ticket',
|
|
110
|
+
linkPaths: {},
|
|
111
|
+
subscriptions: [],
|
|
112
|
+
requests: [],
|
|
113
|
+
slots: {
|
|
114
|
+
content: {
|
|
115
|
+
blocks: [
|
|
116
|
+
block({
|
|
117
|
+
id: 'title',
|
|
118
|
+
type: 'TextInput',
|
|
119
|
+
events: {
|
|
120
|
+
onChange: [{ id: 'echo', type: 'SetState', params: { echo: { _state: 'title' } } }],
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
block({ id: 'secret', type: 'TextInput', properties: { type: 'password' } }),
|
|
124
|
+
block({
|
|
125
|
+
id: 'priority',
|
|
126
|
+
type: 'Selector',
|
|
127
|
+
properties: {
|
|
128
|
+
options: [
|
|
129
|
+
{ label: 'Low', value: 'low' },
|
|
130
|
+
{ label: 'High', value: 'high' },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
block({
|
|
135
|
+
id: 'save',
|
|
136
|
+
type: 'Button',
|
|
137
|
+
properties: { title: 'Save' },
|
|
138
|
+
events: {
|
|
139
|
+
onClick: [{ id: 'saved', type: 'SetState', params: { saved: true } }],
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const rootConfig = {
|
|
148
|
+
classMap: {},
|
|
149
|
+
home: { configured: true, pageId: 'ticket' },
|
|
150
|
+
lowdefyApp: { slug: 'test-app' },
|
|
151
|
+
menus: [],
|
|
152
|
+
pagePaths: { ticket: 'ticket' },
|
|
153
|
+
theme: { light: {}, dark: {} },
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const apiBase = 'http://192.168.1.20:3000';
|
|
157
|
+
|
|
158
|
+
async function renderTicket() {
|
|
159
|
+
const fetch = jest.fn();
|
|
160
|
+
const host = {
|
|
161
|
+
fetch,
|
|
162
|
+
globals: createNativeGlobals({
|
|
163
|
+
assign: jest.fn(),
|
|
164
|
+
fetch,
|
|
165
|
+
getPath: () => '/ticket?from=list',
|
|
166
|
+
origin: apiBase,
|
|
167
|
+
}),
|
|
168
|
+
onBuildChanged: jest.fn(),
|
|
169
|
+
onCallerChanged: jest.fn(),
|
|
170
|
+
router: { back: jest.fn(), push: jest.fn(), replace: jest.fn() },
|
|
171
|
+
sessionStorage: createMemoryStore(),
|
|
172
|
+
storage: createMemoryStore(),
|
|
173
|
+
WebSocket: jest.fn(),
|
|
174
|
+
websocketUrl: (apiPath) => apiPath,
|
|
175
|
+
};
|
|
176
|
+
const lowdefy = {};
|
|
177
|
+
const core = { apiBase, auth: { authConfig: {} }, basePath: '', lowdefy };
|
|
178
|
+
await render(
|
|
179
|
+
<LowdefyPage
|
|
180
|
+
core={core}
|
|
181
|
+
host={host}
|
|
182
|
+
pageConfig={pageConfig}
|
|
183
|
+
rootConfig={rootConfig}
|
|
184
|
+
types={types}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
await screen.findByText('Save');
|
|
188
|
+
return lowdefy;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function startRecorder(lowdefy) {
|
|
192
|
+
const posts = [];
|
|
193
|
+
const fetch = jest.fn(async (url, init) => {
|
|
194
|
+
posts.push({ url, body: JSON.parse(init.body) });
|
|
195
|
+
return { ok: true, status: 204 };
|
|
196
|
+
});
|
|
197
|
+
const kv = createMemoryStore();
|
|
198
|
+
const recorder = createNativeRecorder({
|
|
199
|
+
apiBase,
|
|
200
|
+
device: 'ios',
|
|
201
|
+
fetch,
|
|
202
|
+
getBuild: () => 'build-1',
|
|
203
|
+
getLowdefy: () => lowdefy,
|
|
204
|
+
kv,
|
|
205
|
+
shellVersion: '7.4.0',
|
|
206
|
+
});
|
|
207
|
+
recorder.start();
|
|
208
|
+
return { kv, posts, recorder };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function recordsOf(posts) {
|
|
212
|
+
return posts.flatMap((post) => post.body.records);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Times and the session id differ on every run; the fixture keeps their order.
|
|
216
|
+
function normalise(records) {
|
|
217
|
+
const start = Date.parse('2026-10-18T09:00:00.000Z');
|
|
218
|
+
return records.map((record, index) => ({
|
|
219
|
+
...record,
|
|
220
|
+
session: '20261018T090000Z-ph0ne1',
|
|
221
|
+
t: new Date(start + index * 1000).toISOString(),
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function act$(callback) {
|
|
226
|
+
await act(async () => {
|
|
227
|
+
callback();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let running;
|
|
232
|
+
afterEach(async () => {
|
|
233
|
+
await running?.recorder.stop();
|
|
234
|
+
running = null;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('a click, a fill and a select are recorded by block id, paired with the events they ran', async () => {
|
|
238
|
+
const lowdefy = await renderTicket();
|
|
239
|
+
running = startRecorder(lowdefy);
|
|
240
|
+
await act$(() => fireEvent.changeText(screen.getByTestId('title_input'), 'Pump'));
|
|
241
|
+
await act$(() => fireEvent.press(screen.getByTestId('priority-high')));
|
|
242
|
+
await act$(() => fireEvent.press(screen.getByTestId('save-press')));
|
|
243
|
+
await running.recorder.flush();
|
|
244
|
+
|
|
245
|
+
const { posts } = running;
|
|
246
|
+
expect(posts.map((post) => post.url)).toEqual([`${apiBase}/api/dev-recording`]);
|
|
247
|
+
const records = recordsOf(posts);
|
|
248
|
+
expect(records.every((record) => record.session === posts[0].body.session)).toBe(true);
|
|
249
|
+
expect(normalise(records)).toEqual(fixture.records);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('every record passes the trace record validation once the dev server stamps its source', async () => {
|
|
253
|
+
const lowdefy = await renderTicket();
|
|
254
|
+
running = startRecorder(lowdefy);
|
|
255
|
+
await act$(() => fireEvent.changeText(screen.getByTestId('title_input'), 'Pump'));
|
|
256
|
+
await act$(() => fireEvent.press(screen.getByTestId('save-press')));
|
|
257
|
+
await running.recorder.flush();
|
|
258
|
+
const records = recordsOf(running.posts);
|
|
259
|
+
expect(records.length).toBeGreaterThan(0);
|
|
260
|
+
records.forEach((record) => {
|
|
261
|
+
expect(validateTraceRecord({ record: { ...record, source: 'dev' } })).toEqual({});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('a password input is never recorded', async () => {
|
|
266
|
+
const lowdefy = await renderTicket();
|
|
267
|
+
running = startRecorder(lowdefy);
|
|
268
|
+
await act$(() => fireEvent.changeText(screen.getByTestId('secret_input'), 'hunter2'));
|
|
269
|
+
await running.recorder.flush();
|
|
270
|
+
const change = recordsOf(running.posts).find((record) => record.kind === 'change');
|
|
271
|
+
expect(change).toMatchObject({ target: { block_id: 'secret' }, value: null, redacted: true });
|
|
272
|
+
expect(JSON.stringify(recordsOf(running.posts))).not.toContain('hunter2');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('pressing Enter in an input is recorded as an Enter key on its block', async () => {
|
|
276
|
+
const lowdefy = await renderTicket();
|
|
277
|
+
running = startRecorder(lowdefy);
|
|
278
|
+
await act$(() => fireEvent(screen.getByTestId('title_input'), 'submitEditing'));
|
|
279
|
+
await running.recorder.flush();
|
|
280
|
+
const key = recordsOf(running.posts).find((record) => record.kind === 'key');
|
|
281
|
+
expect(key).toMatchObject({ key: 'Enter', target: { block_id: 'title' }, page_id: 'ticket' });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('the session is kept in the device store, so a new recorder carries it on', async () => {
|
|
285
|
+
const lowdefy = await renderTicket();
|
|
286
|
+
running = startRecorder(lowdefy);
|
|
287
|
+
await running.recorder.flush();
|
|
288
|
+
const [first] = recordsOf(running.posts);
|
|
289
|
+
const again = createNativeRecorder({
|
|
290
|
+
apiBase,
|
|
291
|
+
device: 'android',
|
|
292
|
+
fetch: jest.fn(async () => ({ ok: true })),
|
|
293
|
+
getBuild: () => null,
|
|
294
|
+
getLowdefy: () => lowdefy,
|
|
295
|
+
kv: running.kv,
|
|
296
|
+
shellVersion: '7.4.0',
|
|
297
|
+
});
|
|
298
|
+
const stored = JSON.parse(running.kv.get('lowdefy_dev_recording'));
|
|
299
|
+
expect(stored.id).toBe(first.session);
|
|
300
|
+
await again.stop();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('with no open app the recorder records nothing', async () => {
|
|
304
|
+
running = startRecorder(null);
|
|
305
|
+
await running.recorder.flush();
|
|
306
|
+
expect(running.posts).toEqual([]);
|
|
307
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
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 parseDevLink from './connection/parseDevLink.js';
|
|
18
|
+
import getShellConfig from '../config/getShellConfig.js';
|
|
19
|
+
|
|
20
|
+
// Expo Router's redirectSystemPath (app/+native-intent.js): a Lowdefy Dev link is DevShell's, not a
|
|
21
|
+
// page path. Opened with one, the app starts on its home screen; while it runs, the router leaves
|
|
22
|
+
// it alone.
|
|
23
|
+
function redirectDevLink({ initial, path }) {
|
|
24
|
+
if (parseDevLink({ link: path, scheme: getShellConfig().scheme }) === null) {
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
return initial ? '/' : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default redirectDevLink;
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
import redirectDevLink from './redirectDevLink.js';
|
|
20
|
+
|
|
21
|
+
jest.mock('../config/getShellConfig.js', () => () => ({ scheme: 'lowdefy-dev' }));
|
|
22
|
+
|
|
23
|
+
const { getShellVariant } = require('../../shellVariant.js');
|
|
24
|
+
|
|
25
|
+
test('a Lowdefy Dev link is left to DevShell: home on launch, nothing while running', () => {
|
|
26
|
+
const link = 'lowdefy-dev://connect?url=http%3A%2F%2F192.168.1.20%3A3000&pair=k3x9q';
|
|
27
|
+
expect(redirectDevLink({ initial: true, path: link })).toBe('/');
|
|
28
|
+
expect(redirectDevLink({ initial: false, path: link })).toBeNull();
|
|
29
|
+
expect(redirectDevLink({ initial: false, path: 'lowdefy-dev://menu' })).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('any other link is routed as a page path', () => {
|
|
33
|
+
expect(redirectDevLink({ initial: false, path: 'lowdefy-dev://tickets/7' })).toBe(
|
|
34
|
+
'lowdefy-dev://tickets/7'
|
|
35
|
+
);
|
|
36
|
+
expect(redirectDevLink({ initial: true, path: '/tickets/7' })).toBe('/tickets/7');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('Lowdefy Dev builds as dev and every other shell as release unless the build says', () => {
|
|
40
|
+
expect(getShellVariant({})).toBe('dev');
|
|
41
|
+
expect(getShellVariant({ LOWDEFY_SHELL_CONFIG: 'shell.json' })).toBe('release');
|
|
42
|
+
expect(getShellVariant({ LOWDEFY_SHELL_VARIANT: 'release' })).toBe('release');
|
|
43
|
+
expect(
|
|
44
|
+
getShellVariant({ LOWDEFY_SHELL_CONFIG: 'shell.json', LOWDEFY_SHELL_VARIANT: 'dev' })
|
|
45
|
+
).toBe('dev');
|
|
46
|
+
expect(() => getShellVariant({ LOWDEFY_SHELL_VARIANT: 'prod' })).toThrow(
|
|
47
|
+
'LOWDEFY_SHELL_VARIANT must be "dev" or "release". Received "prod".'
|
|
48
|
+
);
|
|
49
|
+
});
|