@lowdefy/server-e2e 5.6.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{pages/_app.js → client/App.jsx} +43 -28
- package/client/Page.jsx +110 -0
- package/client/main.jsx +39 -0
- package/lib/build/app.js +8 -1
- package/lib/build/appMeta.js +8 -1
- package/lib/build/auth.js +8 -1
- package/lib/build/config.js +8 -1
- package/lib/build/i18n.js +26 -0
- package/lib/build/logger.js +8 -1
- package/lib/build/theme.js +8 -1
- package/lib/client/auth/{Auth.js → Auth.jsx} +5 -1
- package/lib/server/auth/{getServerSession.js → session.js} +5 -3
- package/lib/server/log/createHandleError.test.mjs +126 -0
- package/lowdefy/createCustomPluginTypesMap.mjs +1 -0
- package/lowdefy/createCustomPluginTypesMap.test.mjs +2 -0
- package/package.json +49 -41
- package/package.original.json +52 -44
- package/postcss.config.cjs +1 -0
- package/src/app.js +78 -0
- package/src/html/getAssets.js +45 -0
- package/src/html/renderPage.js +79 -0
- package/src/html/template.js +95 -0
- package/src/index.js +40 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +81 -0
- package/src/middleware/errorHandler.js +39 -0
- package/src/routes/apiPage.js +37 -0
- package/{pages/api/client-error.js → src/routes/clientError.js} +14 -18
- package/{pages/api/endpoints/[...endpointId].js → src/routes/endpoints.js} +8 -7
- package/{pages/api/request/[...path].js → src/routes/request.js} +10 -10
- package/{pages/api/auth/session.js → src/routes/sessionMock.js} +4 -5
- package/{pages/api → src/routes}/usage.js +11 -8
- package/src/routes/websocket.js +46 -0
- package/vite.config.js +45 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/Page.js +0 -57
- package/lib/server/serverSidePropsWrapper.js +0 -64
- package/next.config.js +0 -20
- package/pages/404.js +0 -49
- package/pages/[[...pageId]].js +0 -85
- package/pages/_document.js +0 -108
|
@@ -14,24 +14,20 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
// CSS layer order — MUST be the first CSS import. Next.js treats this as critical
|
|
18
|
-
// CSS that loads before hydration, locking the cascade priority (antd > base/preflight)
|
|
19
|
-
// before antd's StyleProvider injects @layer antd {} at runtime.
|
|
20
|
-
import '../build/layer-order.css';
|
|
21
|
-
|
|
22
17
|
import React, { useCallback, useRef } from 'react';
|
|
23
|
-
import dynamic from 'next/dynamic';
|
|
24
18
|
|
|
25
19
|
import { ErrorBoundary } from '@lowdefy/block-utils';
|
|
26
|
-
import { useDarkMode } from '@lowdefy/client';
|
|
20
|
+
import { useDarkMode, useLocale } from '@lowdefy/client';
|
|
27
21
|
import { StyleProvider } from '@ant-design/cssinjs';
|
|
28
|
-
import { App as AntdApp,
|
|
22
|
+
import { App as AntdApp, theme as antdTheme } from 'antd';
|
|
23
|
+
import { XProvider } from '@ant-design/x';
|
|
29
24
|
|
|
30
|
-
import
|
|
25
|
+
import antdLocaleLoaders from '../build/i18n/antdLocales.js';
|
|
26
|
+
import antdXLocaleLoaders from '../build/i18n/antdXLocales.js';
|
|
27
|
+
import dayjsLocaleMap from '../build/i18n/dayjsLocales.js';
|
|
28
|
+
import Auth from '../lib/client/auth/Auth.jsx';
|
|
31
29
|
import createLogUsage from '../lib/client/createLogUsage.js';
|
|
32
|
-
|
|
33
|
-
// Must be in _app due to next specifications.
|
|
34
|
-
import '../build/globals.css';
|
|
30
|
+
import Page from './Page.jsx';
|
|
35
31
|
|
|
36
32
|
function ThemeTokenResolver({ lowdefyRef, children }) {
|
|
37
33
|
const { token } = antdTheme.useToken();
|
|
@@ -42,7 +38,9 @@ function ThemeTokenResolver({ lowdefyRef, children }) {
|
|
|
42
38
|
return children;
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
function App({
|
|
41
|
+
function App({ config }) {
|
|
42
|
+
const { rootConfig, session } = config;
|
|
43
|
+
|
|
46
44
|
const usageDataRef = useRef({});
|
|
47
45
|
const lowdefyRef = useRef({ eventCallback: createLogUsage({ usageDataRef }) });
|
|
48
46
|
if (rootConfig?.theme) {
|
|
@@ -54,6 +52,21 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
|
|
|
54
52
|
configDarkMode: lowdefyRef.current.theme?.darkMode,
|
|
55
53
|
});
|
|
56
54
|
|
|
55
|
+
const {
|
|
56
|
+
active: activeLocale,
|
|
57
|
+
antdLocale,
|
|
58
|
+
antdXLocale,
|
|
59
|
+
} = useLocale({
|
|
60
|
+
i18n: rootConfig?.i18n,
|
|
61
|
+
antdLocaleLoaders,
|
|
62
|
+
antdXLocaleLoaders,
|
|
63
|
+
dayjsLocaleMap,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (rootConfig?.i18n?.defaultLocale) {
|
|
67
|
+
lowdefyRef.current.i18n = { ...rootConfig.i18n, active: activeLocale };
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
const {
|
|
58
71
|
lightToken: _lightToken,
|
|
59
72
|
darkToken: _darkToken,
|
|
@@ -70,9 +83,22 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
|
|
|
70
83
|
}
|
|
71
84
|
}, []);
|
|
72
85
|
|
|
86
|
+
// XProvider extends antd's ConfigProvider; merging antd + antd-X locale packs
|
|
87
|
+
// gives X components (Bubble, Sender, Conversations, ...) their built-in strings
|
|
88
|
+
// alongside antd's. antd X ships only en_US + zh_CN; other locales fall back
|
|
89
|
+
// to en_US for X-native strings.
|
|
90
|
+
const mergedLocale =
|
|
91
|
+
antdLocale || antdXLocale ? { ...(antdLocale ?? {}), ...(antdXLocale ?? {}) } : undefined;
|
|
92
|
+
|
|
73
93
|
return (
|
|
74
94
|
<StyleProvider layer>
|
|
75
|
-
<
|
|
95
|
+
<XProvider
|
|
96
|
+
locale={mergedLocale}
|
|
97
|
+
form={
|
|
98
|
+
antdLocale?.Form?.defaultValidateMessages
|
|
99
|
+
? { validateMessages: antdLocale.Form.defaultValidateMessages }
|
|
100
|
+
: undefined
|
|
101
|
+
}
|
|
76
102
|
theme={{
|
|
77
103
|
...antdConfig,
|
|
78
104
|
token,
|
|
@@ -88,26 +114,15 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
|
|
|
88
114
|
<Auth session={session}>
|
|
89
115
|
{(auth) => {
|
|
90
116
|
usageDataRef.current.user = auth.session?.hashed_id;
|
|
91
|
-
return
|
|
92
|
-
<Component
|
|
93
|
-
auth={auth}
|
|
94
|
-
lowdefy={lowdefyRef.current}
|
|
95
|
-
rootConfig={rootConfig}
|
|
96
|
-
pageConfig={pageConfig}
|
|
97
|
-
/>
|
|
98
|
-
);
|
|
117
|
+
return <Page auth={auth} config={config} lowdefy={lowdefyRef.current} />;
|
|
99
118
|
}}
|
|
100
119
|
</Auth>
|
|
101
120
|
</ErrorBoundary>
|
|
102
121
|
</ThemeTokenResolver>
|
|
103
122
|
</AntdApp>
|
|
104
|
-
</
|
|
123
|
+
</XProvider>
|
|
105
124
|
</StyleProvider>
|
|
106
125
|
);
|
|
107
126
|
}
|
|
108
127
|
|
|
109
|
-
|
|
110
|
-
ssr: false,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
export default DynamicApp;
|
|
128
|
+
export default App;
|
package/client/Page.jsx
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
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, { useEffect, useRef, useState } from 'react';
|
|
18
|
+
|
|
19
|
+
import Client from '@lowdefy/client';
|
|
20
|
+
import createRouter from '@lowdefy/client/adapters/createRouter.js';
|
|
21
|
+
import createLinkComponent from '@lowdefy/client/adapters/Link.js';
|
|
22
|
+
import { createUrl } from '@lowdefy/client/adapters/url.js';
|
|
23
|
+
import Head from '@lowdefy/client/adapters/Head.js';
|
|
24
|
+
|
|
25
|
+
import actions from '../build/plugins/actions.js';
|
|
26
|
+
import blockMetas from '../build/plugins/blockMetas.json';
|
|
27
|
+
import blocks from '../build/plugins/blocks.js';
|
|
28
|
+
import icons from '../build/plugins/icons.js';
|
|
29
|
+
import operators from '../build/plugins/operators/client.js';
|
|
30
|
+
import jsMap from '../build/plugins/operators/clientJsMap.js';
|
|
31
|
+
|
|
32
|
+
// Replaces lib/client/Page.js. The first page renders from the config
|
|
33
|
+
// embedded in the HTML; SPA navigations fetch /api/page/* and swap pageConfig.
|
|
34
|
+
function Page({ auth, config, lowdefy }) {
|
|
35
|
+
const [pageConfig, setPageConfig] = useState(config.pageConfig);
|
|
36
|
+
|
|
37
|
+
const routerRef = useRef(null);
|
|
38
|
+
if (!routerRef.current) {
|
|
39
|
+
const router = createRouter({ basePath: config.basePath ?? '', window });
|
|
40
|
+
routerRef.current = {
|
|
41
|
+
router,
|
|
42
|
+
Link: createLinkComponent({ router }),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const { router, Link } = routerRef.current;
|
|
46
|
+
|
|
47
|
+
// Temporary sequence guard for the superseded-navigation race: a slow
|
|
48
|
+
// response for an abandoned navigation must not paint over a newer one.
|
|
49
|
+
// Remove when the loader-based lifecycle replaces these fetch paths.
|
|
50
|
+
const latestNavRef = useRef(0);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const unsubscribe = router.subscribe(async ({ pageId }) => {
|
|
54
|
+
const token = ++latestNavRef.current;
|
|
55
|
+
const targetPageId = pageId ?? config.rootConfig.home.pageId;
|
|
56
|
+
try {
|
|
57
|
+
// Forward the current query string so server-side Dynamic block
|
|
58
|
+
// resolution sees the same urlQuery as an initial HTML load.
|
|
59
|
+
const res = await fetch(
|
|
60
|
+
`${router.basePath}/api/page/${targetPageId}${window.location.search}`
|
|
61
|
+
);
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
if (token !== latestNavRef.current) return;
|
|
64
|
+
if (targetPageId !== '404') {
|
|
65
|
+
router.replace({ pathname: '/404' });
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const { pageConfig: nextPageConfig } = await res.json();
|
|
70
|
+
if (token !== latestNavRef.current) return;
|
|
71
|
+
setPageConfig(nextPageConfig);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Network failure on SPA navigation — fall back to a full page load.
|
|
74
|
+
if (token !== latestNavRef.current) return;
|
|
75
|
+
window.location.assign(
|
|
76
|
+
createUrl({
|
|
77
|
+
basePath: router.basePath,
|
|
78
|
+
pathname: targetPageId === config.rootConfig.home.pageId ? '/' : `/${targetPageId}`,
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return unsubscribe;
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Client
|
|
88
|
+
auth={auth}
|
|
89
|
+
Components={{ Head, Link }}
|
|
90
|
+
config={{
|
|
91
|
+
pageConfig,
|
|
92
|
+
rootConfig: config.rootConfig,
|
|
93
|
+
}}
|
|
94
|
+
jsMap={jsMap}
|
|
95
|
+
lowdefy={lowdefy}
|
|
96
|
+
router={router}
|
|
97
|
+
stage="e2e"
|
|
98
|
+
types={{
|
|
99
|
+
actions,
|
|
100
|
+
blockMetas,
|
|
101
|
+
blocks,
|
|
102
|
+
icons,
|
|
103
|
+
operators,
|
|
104
|
+
}}
|
|
105
|
+
window={window}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default Page;
|
package/client/main.jsx
ADDED
|
@@ -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
|
+
// CSS layer order — MUST be the first CSS import. This locks the cascade
|
|
18
|
+
// priority (antd > base/preflight) before antd's StyleProvider injects
|
|
19
|
+
// @layer antd {} at runtime.
|
|
20
|
+
import '../build/layer-order.css';
|
|
21
|
+
|
|
22
|
+
import React from 'react';
|
|
23
|
+
import { createRoot } from 'react-dom/client';
|
|
24
|
+
|
|
25
|
+
import App from './App.jsx';
|
|
26
|
+
|
|
27
|
+
import '../build/globals.css';
|
|
28
|
+
|
|
29
|
+
const config = JSON.parse(document.getElementById('__LOWDEFY_CONFIG__').textContent);
|
|
30
|
+
const container = document.getElementById('root');
|
|
31
|
+
|
|
32
|
+
// Keep one React root across Vite HMR updates — re-running createRoot on a
|
|
33
|
+
// container that already has a root is an error.
|
|
34
|
+
const root = import.meta.hot?.data.root ?? createRoot(container);
|
|
35
|
+
if (import.meta.hot) {
|
|
36
|
+
import.meta.hot.data.root = root;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
root.render(<App config={config} />);
|
package/lib/build/app.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/app.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
package/lib/build/appMeta.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/appMeta.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
package/lib/build/auth.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/auth.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
package/lib/build/config.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/config.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
|
@@ -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
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { serializer } from '@lowdefy/helpers';
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/i18n.json'), 'utf8'));
|
|
25
|
+
|
|
26
|
+
export default serializer.deserialize(raw);
|
package/lib/build/logger.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/logger.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
package/lib/build/theme.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/theme.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import
|
|
17
|
+
import { serializer } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import rawAuthConfig from '../../../build/auth.json';
|
|
20
|
+
|
|
21
|
+
const authConfig = serializer.deserialize(rawAuthConfig);
|
|
18
22
|
|
|
19
23
|
function e2eNotSupported() {
|
|
20
24
|
throw new Error('Sign-in and sign-out are not supported in e2e testing.');
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// E2E test-user injection: the session is read from the lowdefy_e2e_user
|
|
18
|
+
// cookie (base64-encoded JSON user object) instead of a real auth engine.
|
|
19
|
+
function getSession(c) {
|
|
20
|
+
const cookieHeader = c.req.header('cookie') ?? '';
|
|
19
21
|
const match = cookieHeader.match(/lowdefy_e2e_user=([^;]+)/);
|
|
20
22
|
if (!match) {
|
|
21
23
|
return undefined;
|
|
@@ -30,4 +32,4 @@ function getServerSession({ req }) {
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
export default
|
|
35
|
+
export default getSession;
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
import { ConfigError, LowdefyInternalError } from '@lowdefy/errors';
|
|
19
|
+
|
|
20
|
+
import createHandleError from './createHandleError.js';
|
|
21
|
+
|
|
22
|
+
// This sink is the sole writer of `error.handled`. Two callers read the flag and
|
|
23
|
+
// neither can set it for itself: runRoutine's catch guards on `!error.handled` to
|
|
24
|
+
// avoid logging a nested error twice, and the browser's handleError skips the
|
|
25
|
+
// round-trip to /api/client-error when it is set. runRoutine deliberately gave up
|
|
26
|
+
// its own assignment on the strength of this function, so the tests below pin the
|
|
27
|
+
// assignment and — more importantly — its position ahead of everything that can
|
|
28
|
+
// throw.
|
|
29
|
+
|
|
30
|
+
const keyMap = { key_1: { key: 'root.pages[0:home]', '~r': 'ref_1', '~l': 5 } };
|
|
31
|
+
const refMap = { ref_1: { path: 'pages/home.yaml' } };
|
|
32
|
+
|
|
33
|
+
function testContext({ logger, readConfigFile, configDirectory } = {}) {
|
|
34
|
+
return {
|
|
35
|
+
configDirectory,
|
|
36
|
+
logger: logger ?? { error: jest.fn() },
|
|
37
|
+
readConfigFile:
|
|
38
|
+
readConfigFile ??
|
|
39
|
+
jest.fn(async (path) => {
|
|
40
|
+
if (path === 'keyMap.json') return keyMap;
|
|
41
|
+
if (path === 'refMap.json') return refMap;
|
|
42
|
+
return null;
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
console.error = jest.fn();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('handleError marks the error handled and resolves its location', async () => {
|
|
52
|
+
const context = testContext();
|
|
53
|
+
const handleError = createHandleError({ context });
|
|
54
|
+
const error = new ConfigError('Bad config.', { configKey: 'key_1' });
|
|
55
|
+
|
|
56
|
+
await handleError(error);
|
|
57
|
+
|
|
58
|
+
expect(error.handled).toBe(true);
|
|
59
|
+
expect(error.source).toBe('pages/home.yaml:5');
|
|
60
|
+
expect(error.config).toBe('root.pages[0:home]');
|
|
61
|
+
expect(context.logger.error).toHaveBeenCalledWith(error);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('handleError marks a LowdefyInternalError handled even though it never gets a source', async () => {
|
|
65
|
+
// The Decision 7 case. Location resolution is deliberately skipped for an
|
|
66
|
+
// internal error, so `source` stays undefined - which is why the client's
|
|
67
|
+
// already-logged gate reads `handled` and not `source`. Keyed on source, this
|
|
68
|
+
// error was POSTed back to /api/client-error and logged a second time.
|
|
69
|
+
const context = testContext();
|
|
70
|
+
const handleError = createHandleError({ context });
|
|
71
|
+
const error = new LowdefyInternalError('Unexpected condition.');
|
|
72
|
+
|
|
73
|
+
await handleError(error);
|
|
74
|
+
|
|
75
|
+
expect(error.handled).toBe(true);
|
|
76
|
+
expect(error.source).toBeUndefined();
|
|
77
|
+
expect(context.readConfigFile).not.toHaveBeenCalled();
|
|
78
|
+
expect(context.logger.error).toHaveBeenCalledWith(error);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('handleError marks the error handled before it logs, so a throwing logger cannot skip the flag', async () => {
|
|
82
|
+
const logger = {
|
|
83
|
+
error: jest.fn(() => {
|
|
84
|
+
throw new Error('Log drain unreachable.');
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
const context = testContext({ logger });
|
|
88
|
+
const handleError = createHandleError({ context });
|
|
89
|
+
const error = new ConfigError('Bad config.', { configKey: 'key_1' });
|
|
90
|
+
|
|
91
|
+
await handleError(error);
|
|
92
|
+
|
|
93
|
+
// The sink swallows its own failure - reaching it is what makes the error
|
|
94
|
+
// already-logged, so the flag must survive the log call failing. Were it set
|
|
95
|
+
// after the log, this error would cross the wire unflagged and be logged a
|
|
96
|
+
// second time from the browser.
|
|
97
|
+
expect(error.handled).toBe(true);
|
|
98
|
+
expect(logger.error).toHaveBeenCalled();
|
|
99
|
+
expect(console.error).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('handleError marks the error handled when location resolution rejects', async () => {
|
|
103
|
+
const readConfigFile = jest.fn(async () => {
|
|
104
|
+
throw new Error('keyMap.json unreadable.');
|
|
105
|
+
});
|
|
106
|
+
const context = testContext({ readConfigFile });
|
|
107
|
+
const handleError = createHandleError({ context });
|
|
108
|
+
const error = new ConfigError('Bad config.', { configKey: 'key_1' });
|
|
109
|
+
|
|
110
|
+
await handleError(error);
|
|
111
|
+
|
|
112
|
+
expect(error.handled).toBe(true);
|
|
113
|
+
// ConfigError's constructor initialises source to null; resolution failing
|
|
114
|
+
// leaves it there rather than assigning.
|
|
115
|
+
expect(error.source).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('handleError marks a plain Error handled', async () => {
|
|
119
|
+
const context = testContext();
|
|
120
|
+
const handleError = createHandleError({ context });
|
|
121
|
+
const error = new Error('Something broke.');
|
|
122
|
+
|
|
123
|
+
await handleError(error);
|
|
124
|
+
|
|
125
|
+
expect(error.handled).toBe(true);
|
|
126
|
+
});
|
|
@@ -50,6 +50,8 @@ jest.unstable_mockModule('@lowdefy/node-utils', () => ({
|
|
|
50
50
|
spawnProcess: jest.fn(),
|
|
51
51
|
readFile: jest.fn(async () => pluginYaml),
|
|
52
52
|
writeFile: jest.fn(),
|
|
53
|
+
writeFileIfChanged: jest.fn(),
|
|
54
|
+
installIfPackageJsonChanged: jest.fn(),
|
|
53
55
|
}));
|
|
54
56
|
|
|
55
57
|
jest.unstable_mockModule('node:module', () => ({
|