@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,60 @@
|
|
|
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 BUILD_HEADER = 'x-lowdefy-build';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The fetch the core's requests and endpoint calls go through.
|
|
21
|
+
*
|
|
22
|
+
* - A page request carries the build of the root the shell holds (the core stamps the build it was
|
|
23
|
+
* created with; the shell keeps it current without rebuilding the core).
|
|
24
|
+
* - The server refuses a page request from another build with a 409. The shell then syncs: it
|
|
25
|
+
* refetches the root and the changed pages. When the request's own page did not change with the
|
|
26
|
+
* build, its payload is still the shape the new build expects, so the request is retried once,
|
|
27
|
+
* stamped with the new build. When the page changed, the 409 goes to the core, which hands it to
|
|
28
|
+
* onBuildChanged: the screen is rebuilt on the new page and runs its requests again.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} params
|
|
31
|
+
* @param {Function} params.fetch The Fetch API, sending the shell header (withShellHeader).
|
|
32
|
+
* @param {Object} params.session The app session (createAppSession).
|
|
33
|
+
*/
|
|
34
|
+
function createShellFetch({ fetch, session }) {
|
|
35
|
+
return async function shellFetch(url, init = {}) {
|
|
36
|
+
const headers = { ...init.headers };
|
|
37
|
+
const stamped = headers[BUILD_HEADER] !== undefined;
|
|
38
|
+
if (stamped) {
|
|
39
|
+
headers[BUILD_HEADER] = session.getState().buildId ?? headers[BUILD_HEADER];
|
|
40
|
+
}
|
|
41
|
+
const res = await fetch(url, { ...init, headers });
|
|
42
|
+
if (res.status !== 409 || !stamped) {
|
|
43
|
+
return res;
|
|
44
|
+
}
|
|
45
|
+
const before = session.getRequestPageEntry(url);
|
|
46
|
+
try {
|
|
47
|
+
await session.sync();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return res;
|
|
50
|
+
}
|
|
51
|
+
const after = session.getRequestPageEntry(url);
|
|
52
|
+
if (before === null || after === null || before.hash !== after.hash) {
|
|
53
|
+
return res;
|
|
54
|
+
}
|
|
55
|
+
const buildId = session.getState().buildId ?? headers[BUILD_HEADER];
|
|
56
|
+
return fetch(url, { ...init, headers: { ...headers, [BUILD_HEADER]: buildId } });
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default createShellFetch;
|
|
@@ -0,0 +1,150 @@
|
|
|
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 createAppSession from '../session/createAppSession.js';
|
|
18
|
+
import createFakeServer, { header } from '../test/createFakeServer.js';
|
|
19
|
+
import createMemoryCacheStore from '../test/createMemoryCacheStore.js';
|
|
20
|
+
import createMemoryStore from './createMemoryStore.js';
|
|
21
|
+
import createShellFetch from './createShellFetch.js';
|
|
22
|
+
import { createPages, createRoot } from '../test/fixtures.js';
|
|
23
|
+
|
|
24
|
+
const apiBase = 'https://field.example.com';
|
|
25
|
+
|
|
26
|
+
async function setup() {
|
|
27
|
+
const server = createFakeServer({
|
|
28
|
+
root: createRoot(),
|
|
29
|
+
rootEtag: '"root-1"',
|
|
30
|
+
pages: createPages([
|
|
31
|
+
['today', 'today', '"p-today-1"'],
|
|
32
|
+
['jobs', 'jobs', '"p-jobs-1"'],
|
|
33
|
+
['jobs/{job_id}', 'job', '"p-job-1"'],
|
|
34
|
+
]),
|
|
35
|
+
requests: () => ({ id: 'get_jobs', success: true, response: [] }),
|
|
36
|
+
});
|
|
37
|
+
const session = createAppSession({
|
|
38
|
+
apiBase,
|
|
39
|
+
app: { id: 'field' },
|
|
40
|
+
cacheStore: createMemoryCacheStore(),
|
|
41
|
+
fetch: server.fetch,
|
|
42
|
+
kv: createMemoryStore(),
|
|
43
|
+
tokenStore: { get: () => null },
|
|
44
|
+
});
|
|
45
|
+
await session.start();
|
|
46
|
+
const shellFetch = createShellFetch({ fetch: server.fetch, session });
|
|
47
|
+
return { server, session, shellFetch };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function deploy({ server, todayHash }) {
|
|
51
|
+
server.state.buildId = 'build-2';
|
|
52
|
+
server.state.root = createRoot({
|
|
53
|
+
buildId: 'build-2',
|
|
54
|
+
pageIndex: [
|
|
55
|
+
{ path: 'today', pageId: 'today', hash: todayHash, typesKey: 'carried' },
|
|
56
|
+
{ path: 'jobs', pageId: 'jobs', hash: 'h-jobs-1', typesKey: 'carried' },
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
server.state.rootEtag = '"root-2"';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const requestInit = {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json', 'x-lowdefy-build': 'build-1' },
|
|
65
|
+
body: '{}',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
test('a page request carries the build of the root the shell holds', async () => {
|
|
69
|
+
const { server, shellFetch } = await setup();
|
|
70
|
+
const res = await shellFetch(`${apiBase}/api/request/jobs/get_jobs`, {
|
|
71
|
+
...requestInit,
|
|
72
|
+
headers: { ...requestInit.headers, 'x-lowdefy-build': 'stale-core-build' },
|
|
73
|
+
});
|
|
74
|
+
expect(res.status).toBe(200);
|
|
75
|
+
expect(header(server.fetch.mock.calls.at(-1)[1], 'x-lowdefy-build')).toBe('build-1');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('a 409 syncs the root and changed pages, then retries the request once on the new build', async () => {
|
|
79
|
+
const { server, session, shellFetch } = await setup();
|
|
80
|
+
deploy({ server, todayHash: 'h-today-2' });
|
|
81
|
+
server.state.pages.today = { pageConfig: { pageId: 'today', v: 2 }, etag: '"p-today-2"' };
|
|
82
|
+
server.fetch.mockClear();
|
|
83
|
+
const res = await shellFetch(`${apiBase}/api/request/jobs/get_jobs`, requestInit);
|
|
84
|
+
expect(res.status).toBe(200);
|
|
85
|
+
expect(await res.json()).toEqual({ id: 'get_jobs', success: true, response: [] });
|
|
86
|
+
const urls = server.fetch.mock.calls.map(([url]) => new URL(url).pathname);
|
|
87
|
+
expect(urls).toEqual([
|
|
88
|
+
'/api/request/jobs/get_jobs',
|
|
89
|
+
'/api/mobile/root',
|
|
90
|
+
'/api/user',
|
|
91
|
+
'/api/page/today',
|
|
92
|
+
'/api/request/jobs/get_jobs',
|
|
93
|
+
]);
|
|
94
|
+
expect(header(server.fetch.mock.calls.at(-1)[1], 'x-lowdefy-build')).toBe('build-2');
|
|
95
|
+
expect(session.getState().buildId).toBe('build-2');
|
|
96
|
+
expect(session.getState().pages.today.pageConfig.v).toBe(2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('a 409 for a page that changed with the build goes to the core, not retried', async () => {
|
|
100
|
+
const { server, session, shellFetch } = await setup();
|
|
101
|
+
deploy({ server, todayHash: 'h-today-2' });
|
|
102
|
+
server.fetch.mockClear();
|
|
103
|
+
const res = await shellFetch(`${apiBase}/api/request/today/get_jobs`, requestInit);
|
|
104
|
+
expect(res.status).toBe(409);
|
|
105
|
+
expect((await res.json()).buildId).toBe('build-2');
|
|
106
|
+
expect(server.fetch.mock.calls.filter(([url]) => url.includes('/api/request/'))).toHaveLength(1);
|
|
107
|
+
expect(session.getState().buildId).toBe('build-2');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('a second 409 after the retry goes to the core', async () => {
|
|
111
|
+
const { server, shellFetch } = await setup();
|
|
112
|
+
deploy({ server, todayHash: 'h-today-1' });
|
|
113
|
+
const serve = server.fetch.getMockImplementation();
|
|
114
|
+
server.fetch.mockImplementation(async (url, init) => {
|
|
115
|
+
if (url.includes('/api/request/')) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
status: 409,
|
|
119
|
+
headers: { get: () => null },
|
|
120
|
+
json: async () => ({ buildId: 'build-3' }),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return serve(url, init);
|
|
124
|
+
});
|
|
125
|
+
const res = await shellFetch(`${apiBase}/api/request/jobs/get_jobs`, requestInit);
|
|
126
|
+
expect(res.status).toBe(409);
|
|
127
|
+
expect(server.fetch.mock.calls.filter(([url]) => url.includes('/api/request/'))).toHaveLength(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('a 409 the shell cannot sync for goes to the core', async () => {
|
|
131
|
+
const { server, shellFetch } = await setup();
|
|
132
|
+
deploy({ server, todayHash: 'h-today-1' });
|
|
133
|
+
const serve = server.fetch.getMockImplementation();
|
|
134
|
+
server.fetch.mockImplementation(async (url, init) => {
|
|
135
|
+
if (url.includes('/api/mobile/root')) throw new TypeError('Network request failed');
|
|
136
|
+
return serve(url, init);
|
|
137
|
+
});
|
|
138
|
+
const res = await shellFetch(`${apiBase}/api/request/jobs/get_jobs`, requestInit);
|
|
139
|
+
expect(res.status).toBe(409);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('a call without a build stamp (an endpoint call) is never retried', async () => {
|
|
143
|
+
const { server, shellFetch } = await setup();
|
|
144
|
+
deploy({ server, todayHash: 'h-today-1' });
|
|
145
|
+
server.fetch.mockClear();
|
|
146
|
+
const res = await shellFetch(`${apiBase}/api/endpoints/save`, { method: 'POST', headers: {} });
|
|
147
|
+
expect(res.status).toBe(404);
|
|
148
|
+
expect(server.fetch).toHaveBeenCalledTimes(1);
|
|
149
|
+
expect(header(server.fetch.mock.calls[0][1], 'x-lowdefy-build')).toBeNull();
|
|
150
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { createNativeGlobals } from '@lowdefy/client-native';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The host globals of one app in the shell: @lowdefy/client-native's (the shown route, window
|
|
21
|
+
* size, visibility, network and the pages' elements), and the device services the shell's native
|
|
22
|
+
* actions reach (src/actions):
|
|
23
|
+
* - `clipboard`: `{ setString(text) }`, for CopyToClipboard.
|
|
24
|
+
* - `storage`: the device store the core keeps the app's and its users' keys in
|
|
25
|
+
* (`{ get, set, remove, keys }`), for the local storage actions. Sign-out clears the user keys
|
|
26
|
+
* from it.
|
|
27
|
+
* - `shownScreen`: `{ instanceId }`, the route instance of the focused screen, whose page context
|
|
28
|
+
* is keyed by it (client-native LowdefyPage); Lowdefy Dev finds the shown page's context by it.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} params
|
|
31
|
+
* @param {(url: string) => void} params.assign
|
|
32
|
+
* @param {{ setString: (text: string) => Promise<void> }} params.clipboard
|
|
33
|
+
* @param {Function} params.fetch
|
|
34
|
+
* @param {() => (string|null)} params.getInstanceId The focused screen's route instance.
|
|
35
|
+
* @param {() => string} params.getPath
|
|
36
|
+
* @param {Object} [params.network]
|
|
37
|
+
* @param {string} params.origin
|
|
38
|
+
* @param {Object} params.storage
|
|
39
|
+
*/
|
|
40
|
+
function createShellGlobals({
|
|
41
|
+
assign,
|
|
42
|
+
clipboard,
|
|
43
|
+
fetch,
|
|
44
|
+
getInstanceId,
|
|
45
|
+
getPath,
|
|
46
|
+
network,
|
|
47
|
+
origin,
|
|
48
|
+
storage,
|
|
49
|
+
}) {
|
|
50
|
+
return {
|
|
51
|
+
...createNativeGlobals({ assign, fetch, getPath, network, origin }),
|
|
52
|
+
clipboard,
|
|
53
|
+
shownScreen: {
|
|
54
|
+
get instanceId() {
|
|
55
|
+
return getInstanceId();
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
storage,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default createShellGlobals;
|
|
@@ -0,0 +1,73 @@
|
|
|
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 { Linking } from 'react-native';
|
|
18
|
+
import * as Clipboard from 'expo-clipboard';
|
|
19
|
+
|
|
20
|
+
import createDeviceStore from './createDeviceStore.js';
|
|
21
|
+
import createMemoryStore from './createMemoryStore.js';
|
|
22
|
+
import createShellGlobals from './createShellGlobals.js';
|
|
23
|
+
import getOrigin from './getOrigin.js';
|
|
24
|
+
import networkStatus from './networkStatus.js';
|
|
25
|
+
|
|
26
|
+
// The app's `online` and `offline` events follow the shell's one online state.
|
|
27
|
+
const network = {
|
|
28
|
+
addListener: networkStatus.subscribe,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The @lowdefy/client-core host for one app in the shell. Calls are signed in with the session
|
|
33
|
+
* token as a bearer token. The core's build and caller handling lands on the session: a page
|
|
34
|
+
* request refused for another build is retried by `fetch` (createShellFetch) or rebuilds the
|
|
35
|
+
* screens (onBuildChanged); another caller resets the app (onCallerChanged).
|
|
36
|
+
*/
|
|
37
|
+
const clipboard = {
|
|
38
|
+
setString: (text) => Clipboard.setStringAsync(text),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function createShellHost({ fetch, router, session, streamFetch, tokenStore, WebSocket }) {
|
|
42
|
+
const storage = createDeviceStore();
|
|
43
|
+
return {
|
|
44
|
+
authTransport: 'bearer',
|
|
45
|
+
fetch,
|
|
46
|
+
globals: createShellGlobals({
|
|
47
|
+
assign: (url) => Linking.openURL(url),
|
|
48
|
+
clipboard,
|
|
49
|
+
fetch,
|
|
50
|
+
getInstanceId: session.getCurrentInstanceId,
|
|
51
|
+
getPath: session.getCurrentPath,
|
|
52
|
+
network,
|
|
53
|
+
origin: getOrigin(session.apiBase),
|
|
54
|
+
storage,
|
|
55
|
+
}),
|
|
56
|
+
onBuildChanged: () => {
|
|
57
|
+
session.handleBuildChanged();
|
|
58
|
+
},
|
|
59
|
+
onCallerChanged: () => {
|
|
60
|
+
session.handleCallerChanged();
|
|
61
|
+
},
|
|
62
|
+
router,
|
|
63
|
+
sessionStorage: createMemoryStore(),
|
|
64
|
+
storage,
|
|
65
|
+
// The Fetch API whose response body streams, for blocks (client-native's createStreamFetch).
|
|
66
|
+
streamFetch,
|
|
67
|
+
tokenStore,
|
|
68
|
+
WebSocket,
|
|
69
|
+
websocketUrl: (apiPath) => apiPath.replace(/^http/, 'ws'),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default createShellHost;
|
|
@@ -0,0 +1,36 @@
|
|
|
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 createShellHost from './createShellHost.js';
|
|
20
|
+
|
|
21
|
+
jest.mock('expo-clipboard', () => ({ setStringAsync: jest.fn(async () => true) }));
|
|
22
|
+
jest.mock('./createDeviceStore.js', () => () => ({}));
|
|
23
|
+
jest.mock('./networkStatus.js', () => ({ subscribe: () => () => undefined }));
|
|
24
|
+
|
|
25
|
+
// Copy link and the other CopyToClipboard actions reach the phone's clipboard through the host.
|
|
26
|
+
test('the host clipboard copies through expo-clipboard', async () => {
|
|
27
|
+
const Clipboard = jest.requireMock('expo-clipboard');
|
|
28
|
+
const session = {
|
|
29
|
+
apiBase: 'https://field.example.com',
|
|
30
|
+
getCurrentInstanceId: () => null,
|
|
31
|
+
getCurrentPath: () => '/',
|
|
32
|
+
};
|
|
33
|
+
const host = createShellHost({ fetch: jest.fn(), router: {}, session, tokenStore: {} });
|
|
34
|
+
await host.globals.clipboard.setString('https://field.example.com/tasks/12');
|
|
35
|
+
expect(Clipboard.setStringAsync.mock.calls).toEqual([['https://field.example.com/tasks/12']]);
|
|
36
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
// The origin of the app server's URL: its scheme, host and port.
|
|
18
|
+
function getOrigin(url) {
|
|
19
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\/[^/?#]+/i.exec(url);
|
|
20
|
+
if (match === null) {
|
|
21
|
+
throw new Error(`Shell app url is not an absolute URL. Received ${JSON.stringify(url)}.`);
|
|
22
|
+
}
|
|
23
|
+
return match[0];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default getOrigin;
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
// An expo-network state counts as offline only when it says so: unknown reachability is online.
|
|
18
|
+
function isOnline(networkState) {
|
|
19
|
+
return networkState.isConnected !== false && networkState.isInternetReachable !== false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default isOnline;
|
|
@@ -0,0 +1,24 @@
|
|
|
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 * as Network from 'expo-network';
|
|
18
|
+
|
|
19
|
+
import createNetworkStatus from './createNetworkStatus.js';
|
|
20
|
+
|
|
21
|
+
// The shell's one online state (createNetworkStatus), shared by every app the shell opens.
|
|
22
|
+
const networkStatus = createNetworkStatus({ network: Network });
|
|
23
|
+
|
|
24
|
+
export default networkStatus;
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
// Every call a shell makes carries x-lowdefy-shell, its version: the server serves mobile pages
|
|
18
|
+
// only to shells and web pages only to browsers.
|
|
19
|
+
function withShellHeader({ fetch, shellVersion }) {
|
|
20
|
+
return function shellHeaderFetch(url, init = {}) {
|
|
21
|
+
return fetch(url, { ...init, headers: { ...init.headers, 'x-lowdefy-shell': shellVersion } });
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default withShellHeader;
|
|
@@ -0,0 +1,28 @@
|
|
|
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 withShellHeader from './withShellHeader.js';
|
|
20
|
+
|
|
21
|
+
test('every call carries the shell version as x-lowdefy-shell', async () => {
|
|
22
|
+
const fetch = jest.fn(async () => ({ status: 200 }));
|
|
23
|
+
const shellFetch = withShellHeader({ fetch, shellVersion: '7.4.0' });
|
|
24
|
+
await shellFetch('https://app.example.com/api/page/home', { headers: { Accept: 'x' } });
|
|
25
|
+
await shellFetch('https://app.example.com/api/user');
|
|
26
|
+
expect(fetch.mock.calls[0][1].headers).toEqual({ Accept: 'x', 'x-lowdefy-shell': '7.4.0' });
|
|
27
|
+
expect(fetch.mock.calls[1][1].headers).toEqual({ 'x-lowdefy-shell': '7.4.0' });
|
|
28
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
/**
|
|
20
|
+
* The one place the shell turns `_js` source into functions. A shell is shared by every app, so
|
|
21
|
+
* it cannot carry an app's compiled clientJsMap: each mobile page, and the mobile root for the
|
|
22
|
+
* app events, carries its `_js` as `_jsEntries`, the module text the build generates
|
|
23
|
+
* (`export default { [hash]: fn }`). It is evaluated with `new Function`, as the dev server
|
|
24
|
+
* evaluates a page it builds on request (server-dev usePageConfig), and keeps the `volatile` flag
|
|
25
|
+
* the module sets for dependency tracking.
|
|
26
|
+
*
|
|
27
|
+
* Hermes must run `new Function` for this to work. Should it not, `_js` on mobile pages becomes a
|
|
28
|
+
* build error, and this function the one place that refuses it.
|
|
29
|
+
*
|
|
30
|
+
* @param {string|null|undefined} jsEntries The module text; none when the page has no `_js`.
|
|
31
|
+
* @returns {Object} `{ [hash]: fn }`, the jsMap the engine's `_js` operator reads.
|
|
32
|
+
*/
|
|
33
|
+
function evaluateJsEntries(jsEntries) {
|
|
34
|
+
if (type.isNone(jsEntries)) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
const define = new Function('exports', jsEntries.replace('export default', 'exports.default ='));
|
|
38
|
+
const module = {};
|
|
39
|
+
define(module);
|
|
40
|
+
return module.default;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default evaluateJsEntries;
|
|
@@ -0,0 +1,48 @@
|
|
|
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 evaluateJsEntries from './evaluateJsEntries.js';
|
|
18
|
+
import helloJsEntries from '../../fixtures/hello.jsEntries.json';
|
|
19
|
+
|
|
20
|
+
const accessors = {
|
|
21
|
+
lowdefyGlobal: (key) => ({ greeting: 'Hello from Lowdefy' })[key],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
test('evaluateJsEntries turns the hello page module text into its _js functions', () => {
|
|
25
|
+
const jsMap = evaluateJsEntries(helloJsEntries.today);
|
|
26
|
+
expect(Object.keys(jsMap)).toEqual([
|
|
27
|
+
'HorPchxU3cTNYhB6dbx9DFcepx4=',
|
|
28
|
+
'b+IYcPY3kW9J7YdZ4RyICtelfz8=',
|
|
29
|
+
]);
|
|
30
|
+
expect(jsMap['HorPchxU3cTNYhB6dbx9DFcepx4='](accessors)).toBe('Hello from Lowdefy');
|
|
31
|
+
expect(jsMap['b+IYcPY3kW9J7YdZ4RyICtelfz8='](accessors)).toBe(String(new Date().getFullYear()));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('evaluateJsEntries keeps the volatile flag dependency tracking reads', () => {
|
|
35
|
+
const jsMap = evaluateJsEntries(helloJsEntries.today);
|
|
36
|
+
expect(jsMap['b+IYcPY3kW9J7YdZ4RyICtelfz8='].volatile).toBe(true);
|
|
37
|
+
expect(jsMap['HorPchxU3cTNYhB6dbx9DFcepx4='].volatile).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('evaluateJsEntries evaluates the app events module text of the root', () => {
|
|
41
|
+
const jsMap = evaluateJsEntries(helloJsEntries.app);
|
|
42
|
+
expect(jsMap['yHZGzdxAuSQ8P5USi0AFFY81iNg='](accessors)).toBe('Hello from Lowdefy');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('evaluateJsEntries gives an empty map for a page without _js', () => {
|
|
46
|
+
expect(evaluateJsEntries(undefined)).toEqual({});
|
|
47
|
+
expect(evaluateJsEntries(null)).toEqual({});
|
|
48
|
+
});
|