@lowdefy/shell 0.0.0-experimental-20261019052246
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 +201 -0
- package/README.md +308 -0
- 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 +30 -0
- package/app/_layout.js +33 -0
- package/app.config.ts +168 -0
- package/applyShellPlugins.js +43 -0
- package/authCallbackIntentFilters.js +46 -0
- package/babel.config.js +25 -0
- package/checkShellConfig.js +145 -0
- package/checkShellPlugins.js +74 -0
- package/getPrivacyManifest.js +39 -0
- package/metro.config.js +73 -0
- package/native-fingerprint.json +12 -0
- package/package.json +130 -0
- package/profiles.js +274 -0
- package/readShellConfig.js +36 -0
- package/resolveShellPlugins.js +109 -0
- package/runtimeVersion.js +105 -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 +111 -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 +55 -0
- package/src/actions/GeolocationCurrentPosition.test.js +66 -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 +256 -0
- package/src/auth/SignInScreen.test.js +182 -0
- package/src/auth/createAppAuth.js +306 -0
- package/src/auth/createAppAuth.test.js +414 -0
- package/src/auth/createAuthApi.js +99 -0
- package/src/auth/createAuthApi.test.js +135 -0
- package/src/auth/createCoreAuth.js +96 -0
- package/src/auth/createCoreAuth.test.js +188 -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 +45 -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 +21 -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 +90 -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 +108 -0
- package/src/components/MenuButton.test.js +51 -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 +56 -0
- package/src/components/PageScreen.js +244 -0
- package/src/components/PageScreen.test.js +492 -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 +36 -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 +31 -0
- package/src/config/getShellConfig.js +35 -0
- package/src/config/getShellConfig.test.js +80 -0
- package/src/createShellApp.js +105 -0
- package/src/dev/DevShell.js +228 -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 +185 -0
- package/src/dev/inject/createDeviceInjections.test.js +208 -0
- package/src/dev/inject/createOfflineFetch.js +35 -0
- package/src/dev/inject/createOfflineFetch.test.js +68 -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/answerLoadState.js +50 -0
- package/src/dev/tab/createDevTabClient.js +277 -0
- package/src/dev/tab/createDevTabClient.test.js +314 -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/injected.js +60 -0
- package/src/device/injected.test.js +39 -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 +60 -0
- package/src/host/createShellFetch.test.js +150 -0
- package/src/host/createShellGlobals.js +62 -0
- package/src/host/createShellHost.js +73 -0
- package/src/host/createShellHost.test.js +36 -0
- package/src/host/getOrigin.js +26 -0
- package/src/host/isOnline.js +22 -0
- package/src/host/networkStatus.js +24 -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/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/toRouterHref.js +29 -0
- package/src/plugins/appTypes.js +28 -0
- package/src/plugins/prefixTypes.js +28 -0
- package/src/plugins/shellPlugins.json +14 -0
- package/src/plugins/typeManifest.js +649 -0
- package/src/plugins/types.js +61 -0
- package/src/plugins/types.test.js +173 -0
- package/src/polyfills/installGetRandomValues.js +28 -0
- package/src/session/createAppSession.js +318 -0
- package/src/session/createAppSession.test.js +359 -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 +49 -0
- package/src/sync/fetchRoot.js +38 -0
- package/src/sync/fetchUser.js +28 -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/expoImage.js +38 -0
- package/src/test/expoSpeechRecognition.js +31 -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/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/withExcludedNativeModules.js +93 -0
|
@@ -0,0 +1,262 @@
|
|
|
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, 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
|
+
|
|
25
|
+
import createMemoryStore from '../../host/createMemoryStore.js';
|
|
26
|
+
import createStepRunner from './createStepRunner.js';
|
|
27
|
+
|
|
28
|
+
// Stand-ins for native blocks: a container, a button, a text input and a selector.
|
|
29
|
+
function Box({ content }) {
|
|
30
|
+
return <View>{content.content?.()}</View>;
|
|
31
|
+
}
|
|
32
|
+
Box.meta = { category: 'container', slots: { content: 'Child blocks.' } };
|
|
33
|
+
|
|
34
|
+
function Button({ methods, properties }) {
|
|
35
|
+
return (
|
|
36
|
+
<Pressable accessibilityRole="button" onPress={() => methods.triggerEvent({ name: 'onClick' })}>
|
|
37
|
+
<Text>{properties.title}</Text>
|
|
38
|
+
</Pressable>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
Button.meta = { category: 'display' };
|
|
42
|
+
|
|
43
|
+
function TextInput({ blockId, value }) {
|
|
44
|
+
return <Text testID={`${blockId}-value`}>{value ?? ''}</Text>;
|
|
45
|
+
}
|
|
46
|
+
TextInput.meta = { category: 'input', valueType: 'string' };
|
|
47
|
+
|
|
48
|
+
function Selector({ blockId, value }) {
|
|
49
|
+
return <Text testID={`${blockId}-value`}>{JSON.stringify(value ?? null)}</Text>;
|
|
50
|
+
}
|
|
51
|
+
Selector.meta = { category: 'input', valueType: 'any' };
|
|
52
|
+
|
|
53
|
+
// An action that takes a while, so a step that returns before its chain ends would be seen.
|
|
54
|
+
function Pause({ params }) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
setTimeout(resolve, params.ms);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const types = {
|
|
61
|
+
actions: { Pause, SetState },
|
|
62
|
+
blocks: { Box, Button, Selector, TextInput },
|
|
63
|
+
operators: { _state },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function block({ id, type, events, ...rest }) {
|
|
67
|
+
const built = { id: `block:form:${id}:0`, blockId: id, type, ...rest };
|
|
68
|
+
if (events) {
|
|
69
|
+
built.events = Object.fromEntries(
|
|
70
|
+
Object.entries(events).map(([name, actions]) => [name, { try: actions, catch: [] }])
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return built;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const pageConfig = {
|
|
77
|
+
id: 'page:form',
|
|
78
|
+
pageId: 'form',
|
|
79
|
+
blockId: 'form',
|
|
80
|
+
type: 'Box',
|
|
81
|
+
path: 'form',
|
|
82
|
+
linkPaths: {},
|
|
83
|
+
subscriptions: [],
|
|
84
|
+
requests: [],
|
|
85
|
+
slots: {
|
|
86
|
+
content: {
|
|
87
|
+
blocks: [
|
|
88
|
+
block({
|
|
89
|
+
id: 'title',
|
|
90
|
+
type: 'TextInput',
|
|
91
|
+
events: {
|
|
92
|
+
onChange: [{ id: 'echo', type: 'SetState', params: { echo: { _state: 'title' } } }],
|
|
93
|
+
onPressEnter: [{ id: 'enter', type: 'SetState', params: { entered: true } }],
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
block({
|
|
97
|
+
id: 'priority',
|
|
98
|
+
type: 'Selector',
|
|
99
|
+
properties: {
|
|
100
|
+
options: [
|
|
101
|
+
{ label: 'Low', value: 'low' },
|
|
102
|
+
{ label: 'High', value: 'high' },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
block({
|
|
107
|
+
id: 'save',
|
|
108
|
+
type: 'Button',
|
|
109
|
+
properties: { title: 'Save' },
|
|
110
|
+
events: {
|
|
111
|
+
onClick: [
|
|
112
|
+
{ id: 'pause', type: 'Pause', params: { ms: 200 } },
|
|
113
|
+
{ id: 'saved', type: 'SetState', params: { saved: true } },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
block({ id: 'locked', type: 'Button', properties: { title: 'Locked', disabled: true } }),
|
|
118
|
+
block({ id: 'hidden', type: 'Button', visible: false, properties: { title: 'Hidden' } }),
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const rootConfig = {
|
|
125
|
+
classMap: {},
|
|
126
|
+
home: { configured: true, pageId: 'form' },
|
|
127
|
+
lowdefyApp: { slug: 'test-app' },
|
|
128
|
+
menus: [],
|
|
129
|
+
pagePaths: { form: 'form' },
|
|
130
|
+
theme: { light: {}, dark: {} },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
async function renderForm() {
|
|
134
|
+
const fetch = jest.fn();
|
|
135
|
+
const host = {
|
|
136
|
+
fetch,
|
|
137
|
+
globals: createNativeGlobals({
|
|
138
|
+
assign: jest.fn(),
|
|
139
|
+
fetch,
|
|
140
|
+
getPath: () => '/form',
|
|
141
|
+
origin: 'https://app.lowdefy.test',
|
|
142
|
+
}),
|
|
143
|
+
onBuildChanged: jest.fn(),
|
|
144
|
+
onCallerChanged: jest.fn(),
|
|
145
|
+
router: { back: jest.fn(), push: jest.fn(), replace: jest.fn() },
|
|
146
|
+
sessionStorage: createMemoryStore(),
|
|
147
|
+
storage: createMemoryStore(),
|
|
148
|
+
WebSocket: jest.fn(),
|
|
149
|
+
websocketUrl: (apiPath) => apiPath,
|
|
150
|
+
};
|
|
151
|
+
const lowdefy = {};
|
|
152
|
+
const core = {
|
|
153
|
+
apiBase: 'https://app.lowdefy.test',
|
|
154
|
+
auth: { authConfig: {} },
|
|
155
|
+
basePath: '',
|
|
156
|
+
lowdefy,
|
|
157
|
+
};
|
|
158
|
+
await render(
|
|
159
|
+
<LowdefyPage
|
|
160
|
+
core={core}
|
|
161
|
+
host={host}
|
|
162
|
+
pageConfig={pageConfig}
|
|
163
|
+
rootConfig={rootConfig}
|
|
164
|
+
types={types}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
await screen.findByText('Save');
|
|
168
|
+
const capture = jest.fn(async () => 'iVBORw0KGgo=');
|
|
169
|
+
const runner = createStepRunner({ capture, getLowdefy: () => lowdefy, timeout: 300 });
|
|
170
|
+
const state = () => lowdefy.contexts['page:form'].state;
|
|
171
|
+
async function run(step) {
|
|
172
|
+
let result;
|
|
173
|
+
await act(async () => {
|
|
174
|
+
result = await runner.run({ step });
|
|
175
|
+
});
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
async function fail(step) {
|
|
179
|
+
let error;
|
|
180
|
+
await act(async () => {
|
|
181
|
+
error = await runner.run({ step }).catch((caught) => caught);
|
|
182
|
+
});
|
|
183
|
+
return error.message;
|
|
184
|
+
}
|
|
185
|
+
return { capture, fail, run, state };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
test('a click step finds the block by the id its view carries as testID and waits for its action chain', async () => {
|
|
189
|
+
const { run, state } = await renderForm();
|
|
190
|
+
expect(screen.getByTestId('save')).toBeTruthy();
|
|
191
|
+
const result = await run({ click: 'save' });
|
|
192
|
+
expect(result).toEqual({ ok: true, settled: true });
|
|
193
|
+
expect(state().saved).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('a click step takes a { blockId } target', async () => {
|
|
197
|
+
const { run, state } = await renderForm();
|
|
198
|
+
await run({ click: { blockId: 'save' } });
|
|
199
|
+
expect(state().saved).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('a fill step sets an input and runs its onChange, which the page then shows', async () => {
|
|
203
|
+
const { run, state } = await renderForm();
|
|
204
|
+
await run({ fill: { blockId: 'title', value: 'Quarterly' } });
|
|
205
|
+
expect(state().title).toBe('Quarterly');
|
|
206
|
+
expect(state().echo).toBe('Quarterly');
|
|
207
|
+
expect(screen.getByTestId('title-value').props.children).toBe('Quarterly');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('a press Enter step runs the onPressEnter of the input filled last', async () => {
|
|
211
|
+
const { run, state } = await renderForm();
|
|
212
|
+
await run({ fill: { blockId: 'title', value: 'Quarterly' } });
|
|
213
|
+
await run({ press: 'Enter' });
|
|
214
|
+
expect(state().entered).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('a select step picks the option by its label', async () => {
|
|
218
|
+
const { run, state } = await renderForm();
|
|
219
|
+
await run({ select: { blockId: 'priority', value: 'High' } });
|
|
220
|
+
expect(state().priority).toBe('high');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('a wait step waits for state to be set', async () => {
|
|
224
|
+
const { run, state } = await renderForm();
|
|
225
|
+
await act(async () => {
|
|
226
|
+
fireEvent.press(screen.getByText('Save'));
|
|
227
|
+
});
|
|
228
|
+
expect(state().saved).toBeUndefined();
|
|
229
|
+
expect(await run({ wait: { state: 'saved' } })).toEqual({ ok: true });
|
|
230
|
+
expect(state().saved).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('a screenshot step captures the root view', async () => {
|
|
234
|
+
const { capture, run } = await renderForm();
|
|
235
|
+
expect(await run({ screenshot: 'after-save' })).toEqual({
|
|
236
|
+
ok: true,
|
|
237
|
+
name: 'after-save',
|
|
238
|
+
image: 'iVBORw0KGgo=',
|
|
239
|
+
});
|
|
240
|
+
expect(capture).toHaveBeenCalledTimes(1);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('steps that cannot run say why', async () => {
|
|
244
|
+
const { fail } = await renderForm();
|
|
245
|
+
expect(await fail({ click: 'missing' })).toBe(
|
|
246
|
+
'Block "missing" is not on page "form". Waited 300 ms.'
|
|
247
|
+
);
|
|
248
|
+
expect(await fail({ click: 'hidden' })).toBe('Block "hidden" is not visible. Waited 300 ms.');
|
|
249
|
+
expect(await fail({ click: 'locked' })).toBe('Block "locked" is disabled.');
|
|
250
|
+
expect(await fail({ fill: { blockId: 'save', value: 'x' } })).toBe(
|
|
251
|
+
'Block "save" is not an input.'
|
|
252
|
+
);
|
|
253
|
+
expect(await fail({ select: { blockId: 'priority', value: 'Urgent' } })).toBe(
|
|
254
|
+
'Block "priority" has no option "Urgent". Its options are ["Low","High"].'
|
|
255
|
+
);
|
|
256
|
+
expect(await fail({ press: 'Enter' })).toBe('Press "Enter" needs an input filled before it.');
|
|
257
|
+
expect(await fail({ press: 'Tab' })).toBe('A phone presses only "Enter". Received "Tab".');
|
|
258
|
+
expect(await fail({ click: { blockId: 'save', text: 'Save' } })).toMatch(/by blockId only/);
|
|
259
|
+
expect(await fail({ goto: 'home' })).toBe(
|
|
260
|
+
'A phone runs the steps click, fill, select, press, wait, screenshot. Received "goto".'
|
|
261
|
+
);
|
|
262
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import findPageContext from '../tab/findPageContext.js';
|
|
20
|
+
import getShownPage from '../tab/getShownPage.js';
|
|
21
|
+
import waitUntil from './waitUntil.js';
|
|
22
|
+
|
|
23
|
+
// A step's target: a block id, or `{ blockId }`. The browser's grid, text and nth targets read the
|
|
24
|
+
// DOM, which a phone does not have.
|
|
25
|
+
function readBlockId(target) {
|
|
26
|
+
if (type.isString(target)) {
|
|
27
|
+
return target;
|
|
28
|
+
}
|
|
29
|
+
if (type.isObject(target) && type.isString(target.blockId)) {
|
|
30
|
+
const others = Object.keys(target).filter((key) => !['blockId', 'value'].includes(key));
|
|
31
|
+
if (others.length > 0) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`A step on a phone targets a block by blockId only. Received ${JSON.stringify(target)}.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return target.blockId;
|
|
37
|
+
}
|
|
38
|
+
throw new Error(
|
|
39
|
+
`A step target must be a blockId or { blockId }. Received ${JSON.stringify(target)}.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The block a step acts on, on the page the shell shows: `{ block, context, page }`. Its view is
|
|
45
|
+
* the one with the block id as testID (client-native BlockLayout), so a step needs no screen
|
|
46
|
+
* position. Waits up to `timeout` ms for the page to render the block and show it.
|
|
47
|
+
*/
|
|
48
|
+
async function findBlock({ lowdefy, target, timeout }) {
|
|
49
|
+
const blockId = readBlockId(target);
|
|
50
|
+
let lastError = null;
|
|
51
|
+
const found = await waitUntil({
|
|
52
|
+
timeout,
|
|
53
|
+
check: () => {
|
|
54
|
+
const page = getShownPage(lowdefy);
|
|
55
|
+
const { context, error } = findPageContext({ lowdefy, page });
|
|
56
|
+
if (error) {
|
|
57
|
+
lastError = error;
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const block = context._internal.RootSlots?.map?.[blockId];
|
|
61
|
+
if (type.isNone(block)) {
|
|
62
|
+
lastError = `Block "${blockId}" is not on page "${page.pageId}".`;
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (block.eval?.visible === false) {
|
|
66
|
+
lastError = `Block "${blockId}" is not visible.`;
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return { block, context, page };
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
if (found === undefined) {
|
|
73
|
+
throw new Error(`${lastError} Waited ${timeout} ms.`);
|
|
74
|
+
}
|
|
75
|
+
return found;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default findBlock;
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
// A page context with nothing left running: its onInit and onInitAsync (and the app's) are done, no
|
|
18
|
+
// block event is running (an onClick's action chain, an onMountAsync), no request is loading and
|
|
19
|
+
// every subscription has connected or failed. The dev server's isPageReady reads a browser tab the
|
|
20
|
+
// same way.
|
|
21
|
+
function isContextSettled({ context, lowdefy }) {
|
|
22
|
+
if (context._internal?.onInitDone !== true) return false;
|
|
23
|
+
if (context._internal?.onInitAsyncDone !== true) return false;
|
|
24
|
+
const appContext = lowdefy.appContext;
|
|
25
|
+
if (appContext && appContext._internal?.onInitAsyncDone !== true) return false;
|
|
26
|
+
const blocks = Object.values(context._internal?.RootSlots?.map ?? {});
|
|
27
|
+
const running = blocks.some((block) =>
|
|
28
|
+
Object.values(block?.Events?.events ?? {}).some((event) => event?.loading === true)
|
|
29
|
+
);
|
|
30
|
+
if (running) return false;
|
|
31
|
+
const requests = Object.values(context.requests ?? {});
|
|
32
|
+
if (requests.some((calls) => calls?.[0]?.loading)) return false;
|
|
33
|
+
const channels = Object.values(context.websockets ?? {});
|
|
34
|
+
return channels.every((channel) => channel?.connected === true || Boolean(channel?.error));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default isContextSettled;
|
|
@@ -0,0 +1,39 @@
|
|
|
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 findPageContext from '../tab/findPageContext.js';
|
|
18
|
+
import getShownPage from '../tab/getShownPage.js';
|
|
19
|
+
import isContextSettled from './isContextSettled.js';
|
|
20
|
+
import waitUntil from './waitUntil.js';
|
|
21
|
+
|
|
22
|
+
// Waits, at most `timeout` ms, for the page the shell shows (it may have changed: a click can open
|
|
23
|
+
// another page) to finish what an interaction started. True once settled; false when it is still
|
|
24
|
+
// busy, which, as in the browser runner, does not fail the step.
|
|
25
|
+
async function waitForSettle({ lowdefy, timeout }) {
|
|
26
|
+
const settled = await waitUntil({
|
|
27
|
+
timeout,
|
|
28
|
+
check: () => {
|
|
29
|
+
const { context } = findPageContext({ lowdefy, page: getShownPage(lowdefy) });
|
|
30
|
+
if (context === undefined || !isContextSettled({ context, lowdefy })) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return settled === true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default waitForSettle;
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
const POLL_MS = 50;
|
|
18
|
+
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Calls check every 50 ms until it returns a value other than undefined, and resolves with it, or
|
|
24
|
+
// with undefined once `timeout` ms have passed.
|
|
25
|
+
async function waitUntil({ check, timeout }) {
|
|
26
|
+
const deadline = Date.now() + timeout;
|
|
27
|
+
for (;;) {
|
|
28
|
+
const value = check();
|
|
29
|
+
if (value !== undefined) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
if (Date.now() >= deadline) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
await sleep(POLL_MS);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { sleep };
|
|
40
|
+
export default waitUntil;
|
|
@@ -0,0 +1,97 @@
|
|
|
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 { useEffect } from 'react';
|
|
18
|
+
import { Platform } from 'react-native';
|
|
19
|
+
import { fetch as expoFetch } from 'expo/fetch';
|
|
20
|
+
import { captureRef } from 'react-native-view-shot';
|
|
21
|
+
|
|
22
|
+
import createDevTabClient from './createDevTabClient.js';
|
|
23
|
+
import createNativeRecorder from '../recorder/createNativeRecorder.js';
|
|
24
|
+
import createStepRunner from '../steps/createStepRunner.js';
|
|
25
|
+
import reloadApp from './reloadApp.js';
|
|
26
|
+
import withShellHeader from '../../host/withShellHeader.js';
|
|
27
|
+
import { useShell } from '../../ShellContext.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Joins the dev server's tab channel for the app that is open (createDevTabClient), records what
|
|
31
|
+
* the developer does in it (createNativeRecorder), and tells Lowdefy Dev which app that is through
|
|
32
|
+
* onApp, so the shake menu can reload it or clear its data. Rendered inside the app's provider;
|
|
33
|
+
* draws nothing.
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} props
|
|
36
|
+
* @param {Object} props.injections The device injections (createDeviceInjections).
|
|
37
|
+
* @param {Object} props.kv The device store.
|
|
38
|
+
* @param {Object} props.logs The log buffer (createLogBuffer).
|
|
39
|
+
* @param {(app: Object|null) => void} props.onApp
|
|
40
|
+
* @param {{ current: Object }} props.rootRef The view the whole app is drawn in, for screenshots.
|
|
41
|
+
*/
|
|
42
|
+
function DevTabClient({ injections, kv, logs, onApp, rootRef }) {
|
|
43
|
+
const app = useShell();
|
|
44
|
+
const { session, shell } = app;
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
onApp(app);
|
|
48
|
+
return () => onApp(null);
|
|
49
|
+
}, [app, onApp]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const getLowdefy = () => session.getState().lowdefy;
|
|
53
|
+
const withHeaders = (fetch) =>
|
|
54
|
+
withShellHeader({ fetch: shell.transport.wrapFetch(fetch), shellVersion: shell.version });
|
|
55
|
+
const runner = createStepRunner({
|
|
56
|
+
capture: () => captureRef(rootRef, { format: 'png', result: 'base64' }),
|
|
57
|
+
getLowdefy,
|
|
58
|
+
});
|
|
59
|
+
const fetch = withHeaders(globalThis.fetch);
|
|
60
|
+
const recorder = createNativeRecorder({
|
|
61
|
+
apiBase: session.apiBase,
|
|
62
|
+
device: Platform.OS,
|
|
63
|
+
fetch,
|
|
64
|
+
getBuild: () => session.getState().buildId ?? null,
|
|
65
|
+
getLowdefy,
|
|
66
|
+
kv,
|
|
67
|
+
shellVersion: shell.version,
|
|
68
|
+
});
|
|
69
|
+
const client = createDevTabClient({
|
|
70
|
+
apiBase: session.apiBase,
|
|
71
|
+
device: {
|
|
72
|
+
platform: Platform.OS,
|
|
73
|
+
app: Platform.OS === 'ios' ? shell.ios?.bundleIdentifier : shell.android?.package,
|
|
74
|
+
},
|
|
75
|
+
fetch,
|
|
76
|
+
getLowdefy,
|
|
77
|
+
log: (message) => logs.add('info', message),
|
|
78
|
+
onInject: ({ injection }) => injections.inject(injection ?? {}),
|
|
79
|
+
onReload: async () => {
|
|
80
|
+
await recorder.flush();
|
|
81
|
+
await reloadApp({ session });
|
|
82
|
+
},
|
|
83
|
+
onStep: ({ step }) => runner.run({ step }),
|
|
84
|
+
streamFetch: withHeaders(expoFetch),
|
|
85
|
+
});
|
|
86
|
+
recorder.start();
|
|
87
|
+
client.start();
|
|
88
|
+
return () => {
|
|
89
|
+
client.stop();
|
|
90
|
+
recorder.stop();
|
|
91
|
+
};
|
|
92
|
+
}, [injections, kv, logs, rootRef, session, shell]);
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default DevTabClient;
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { serializer, type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import findPageContext from './findPageContext.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The answer to an eval-request: the operator expression evaluated with the page instance's own
|
|
23
|
+
* parser, `{ value, errors }`, as a browser tab answers it (server-dev client/Inspector.jsx). The
|
|
24
|
+
* expression may arrive as JSON text, as an MCP tool argument passes it.
|
|
25
|
+
*/
|
|
26
|
+
function answerEval({ expression, lowdefy, page }) {
|
|
27
|
+
const { context, error } = findPageContext({ lowdefy, page });
|
|
28
|
+
if (error) {
|
|
29
|
+
return { error };
|
|
30
|
+
}
|
|
31
|
+
const input = type.isString(expression) ? JSON.parse(expression) : expression;
|
|
32
|
+
const { output, errors } = context._internal.parser.parse({ input, location: 'agent_eval' });
|
|
33
|
+
return {
|
|
34
|
+
value: serializer.serializeToString(output),
|
|
35
|
+
errors: errors.map((parseError) => parseError.message),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default answerEval;
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { serializer } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import findPageContext from './findPageContext.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The answer to an inspect-request: the page instance's live state, as a browser tab answers it
|
|
23
|
+
* (server-dev client/Inspector.jsx), a serialized string the dev server deserializes
|
|
24
|
+
* (lib/docs/inspectStateFromTab.js). The URL query is the shown route's.
|
|
25
|
+
*/
|
|
26
|
+
function answerInspect({ lowdefy, page }) {
|
|
27
|
+
const { context, error } = findPageContext({ lowdefy, page });
|
|
28
|
+
if (error) {
|
|
29
|
+
return { error };
|
|
30
|
+
}
|
|
31
|
+
return serializer.serializeToString({
|
|
32
|
+
pageId: page.pageId,
|
|
33
|
+
pathParams: context.pathParams,
|
|
34
|
+
instanceKey: page.instanceKey,
|
|
35
|
+
state: context.state,
|
|
36
|
+
requests: context.requests,
|
|
37
|
+
eventLog: (context.eventLog ?? []).slice(-50),
|
|
38
|
+
global: lowdefy.lowdefyGlobal,
|
|
39
|
+
user: lowdefy.user,
|
|
40
|
+
input: lowdefy.inputs?.[page.instanceKey],
|
|
41
|
+
urlQuery: lowdefy._internal.globals.window.location.search,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default answerInspect;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import findPageContext from './findPageContext.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The answer to a load-state-request (server-dev lib/docs/loadState.js, with a device): sets each
|
|
23
|
+
* key of a state checkpoint on the page instance's state and re-renders it, as the dev server does
|
|
24
|
+
* in its headless browser, then reads back the first `verify` keys so the agent sees they landed.
|
|
25
|
+
* Answers `{ verifiedKeys }`, or `{ error }` as a browser tab words it.
|
|
26
|
+
*/
|
|
27
|
+
function answerLoadState({ lowdefy, page, state, verify = 3 }) {
|
|
28
|
+
if (!type.isObject(state)) {
|
|
29
|
+
return {
|
|
30
|
+
error: `A load-state-request needs the state as an object. Received ${JSON.stringify(
|
|
31
|
+
state
|
|
32
|
+
)}.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const { context, error } = findPageContext({ lowdefy, page });
|
|
36
|
+
if (error) {
|
|
37
|
+
return { error };
|
|
38
|
+
}
|
|
39
|
+
const entries = Object.entries(state);
|
|
40
|
+
entries.forEach(([key, value]) => {
|
|
41
|
+
context._internal.State.set(key, value);
|
|
42
|
+
});
|
|
43
|
+
context._internal.update();
|
|
44
|
+
const verifiedKeys = Object.fromEntries(
|
|
45
|
+
entries.slice(0, verify).map(([key]) => [key, context.state?.[key]])
|
|
46
|
+
);
|
|
47
|
+
return { verifiedKeys };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default answerLoadState;
|