@lowdefy/server-dev 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} +21 -33
- package/client/DevStreamContext.js +30 -0
- package/client/Inspector.jsx +226 -0
- package/{lib/client/Page.js → client/Page.jsx} +4 -8
- package/client/Reload.jsx +85 -0
- package/client/Routing.jsx +117 -0
- package/client/feedback/FeedbackMount.jsx +83 -0
- package/client/feedback/FeedbackOverlay.jsx +901 -0
- package/client/feedback/captureTabScreenshot.js +84 -0
- package/client/feedback/copyToClipboard.js +45 -0
- package/client/feedback/drawAnnotationsSvg.js +119 -0
- package/client/feedback/elementInspect.js +99 -0
- package/client/feedback/feedbackStyles.js +301 -0
- package/client/feedback/pinResponsiveImages.js +59 -0
- package/client/feedback/sendFeedback.js +39 -0
- package/client/feedback/useFeedbackToggle.js +70 -0
- package/client/main.jsx +42 -0
- package/client/openInEditor/OpenInEditorListener.jsx +268 -0
- package/client/openInEditor/findBlockLocation.js +51 -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 +8 -1
- package/lib/build/logger.js +8 -1
- package/lib/build/theme.js +8 -1
- package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
- package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +23 -10
- package/lib/client/setPageId.js +6 -5
- package/lib/client/utils/useMutateCache.js +14 -1
- package/lib/client/utils/usePageConfig.js +45 -43
- package/lib/client/utils/usePageConfig.test.mjs +62 -0
- package/lib/docs/captureAnnotatedScreenshot.js +88 -0
- package/lib/docs/checkpointPaths.js +91 -0
- package/lib/docs/checkpointStore.js +240 -0
- package/lib/docs/checkpointToMocks.js +65 -0
- package/lib/docs/clientErrorStore.js +36 -0
- package/lib/docs/configCheckpoints.test.mjs +156 -0
- package/lib/docs/createConfigCheckpoint.js +92 -0
- package/lib/docs/createDocsMcpServer.js +531 -0
- package/lib/docs/createDocsMcpServer.test.mjs +141 -0
- package/lib/docs/devMockRegistry.js +68 -0
- package/lib/docs/docs.test.mjs +448 -0
- package/lib/docs/enrichFeedback.js +71 -0
- package/lib/docs/evalOperator.js +47 -0
- package/lib/docs/evalOperator.test.mjs +157 -0
- package/lib/docs/evalOperatorHeadless.js +87 -0
- package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
- package/lib/docs/evalOperatorInTab.js +48 -0
- package/lib/docs/findConfig.js +260 -0
- package/lib/docs/formatFeedback.js +121 -0
- package/lib/docs/formatFeedback.test.mjs +204 -0
- package/lib/docs/getAppMap.js +180 -0
- package/lib/docs/getBrowser.js +127 -0
- package/lib/docs/getBrowser.test.mjs +122 -0
- package/lib/docs/getBuildStatus.js +37 -0
- package/lib/docs/getCoreDoc.js +63 -0
- package/lib/docs/getDocsManifest.js +40 -0
- package/lib/docs/getExamples.js +58 -0
- package/lib/docs/getOverview.js +115 -0
- package/lib/docs/getPageConfig.js +53 -0
- package/lib/docs/getPluginDoc.js +55 -0
- package/lib/docs/getSchema.js +65 -0
- package/lib/docs/inspectState.js +47 -0
- package/lib/docs/inspectState.test.mjs +140 -0
- package/lib/docs/inspectStateFromTab.js +40 -0
- package/lib/docs/inspectStateHeadless.js +84 -0
- package/lib/docs/inspectStateHeadless.test.mjs +60 -0
- package/lib/docs/isWriteRequestsAllowed.js +50 -0
- package/lib/docs/listConfigCheckpoints.js +55 -0
- package/lib/docs/listPlugins.js +80 -0
- package/lib/docs/listTypes.js +105 -0
- package/lib/docs/loadState.js +152 -0
- package/lib/docs/mapPageBuildErrors.js +28 -0
- package/lib/docs/normalizeTypeKind.js +40 -0
- package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
- package/lib/docs/readPageArtifact.js +26 -0
- package/lib/docs/resolvePluginDir.js +49 -0
- package/lib/docs/resolveSource.js +48 -0
- package/lib/docs/resolveSource.test.mjs +100 -0
- package/lib/docs/revertConfigCheckpoint.js +73 -0
- package/lib/docs/runRequest.js +141 -0
- package/lib/docs/saveAnnotatedScreenshot.js +43 -0
- package/lib/docs/scaffoldPage.js +75 -0
- package/lib/docs/scaffoldPage.test.mjs +97 -0
- package/lib/docs/screenshotPage.js +110 -0
- package/lib/docs/screenshotPage.test.mjs +59 -0
- package/lib/docs/screenshotPageClip.test.mjs +102 -0
- package/lib/docs/searchDocs.js +67 -0
- package/lib/docs/setupTestFixtures.mjs +335 -0
- package/lib/docs/snapshotState.js +59 -0
- package/lib/docs/stateCheckpoints.test.mjs +366 -0
- package/lib/docs/tabAvailable.js +28 -0
- package/lib/docs/tabChannel.js +119 -0
- package/lib/docs/tabChannel.test.mjs +130 -0
- package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
- package/lib/server/auth/getDevSession.js +70 -0
- package/lib/server/auth/getDevSession.test.mjs +138 -0
- package/lib/server/auth/getHeadlessUser.js +40 -0
- package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
- package/lib/server/auth/headlessUser.js +29 -0
- package/lib/server/auth/resolveHeadlessUser.js +38 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
- package/lib/server/auth/session.js +39 -0
- package/lib/server/auth/strategies.js +47 -0
- package/lib/server/createLowdefyContext.js +122 -0
- package/lib/server/getPageJitEnrichment.test.mjs +105 -0
- package/lib/server/jitPageBuilder.js +125 -12
- package/lib/server/log/createHandleError.test.mjs +126 -0
- package/manager/getContext.mjs +3 -7
- package/manager/processes/initialBuild.mjs +0 -2
- package/manager/processes/installPlugins.mjs +19 -11
- package/manager/processes/lowdefyBuild.mjs +29 -7
- package/manager/processes/lowdefyBuild.test.mjs +102 -0
- package/manager/processes/readDotEnv.mjs +13 -1
- package/manager/processes/shutdownServer.mjs +6 -6
- package/manager/processes/startProxy.mjs +160 -0
- package/manager/processes/startProxy.test.mjs +102 -0
- package/manager/processes/startServer.mjs +33 -16
- package/manager/processes/startWatchers.mjs +2 -2
- package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
- package/manager/run.mjs +50 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
- package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
- package/manager/utils/formatNoticeBox.mjs +30 -0
- package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
- package/manager/utils/updatePageTailwindCss.mjs +7 -1
- package/manager/utils/writeBuildStatus.mjs +39 -0
- package/manager/utils/writeBuildStatus.test.mjs +76 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
- package/manager/watchers/moduleBuildWatcher.mjs +0 -1
- package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
- package/package.json +61 -47
- package/package.original.json +64 -50
- package/postcss.config.cjs +1 -0
- package/src/app.js +175 -0
- package/src/html/renderDevPage.js +86 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +32 -0
- package/src/middleware/errorHandler.js +48 -0
- package/src/middleware/errorHandler.test.mjs +235 -0
- package/src/routes/agent.js +64 -0
- package/src/routes/apiPage.js +37 -0
- package/src/routes/auth.js +49 -0
- package/src/routes/auth.test.mjs +84 -0
- package/src/routes/clientError.js +57 -0
- package/src/routes/cron.js +47 -0
- package/src/routes/cronForward.js +47 -0
- package/src/routes/detached.js +47 -0
- package/src/routes/devInspect.js +111 -0
- package/src/routes/devInspect.test.mjs +119 -0
- package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
- package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
- package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
- package/src/routes/docs/checkpointsCreate.js +28 -0
- package/src/routes/docs/checkpointsList.js +23 -0
- package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
- package/src/routes/docs/content.js +31 -0
- package/src/routes/docs/evalOperator.js +56 -0
- package/src/routes/docs/examples.js +33 -0
- package/src/routes/docs/find.js +30 -0
- package/src/routes/docs/index.js +23 -0
- package/src/routes/docs/inspectState.js +44 -0
- package/src/routes/docs/loadState.js +49 -0
- package/src/routes/docs/mcp.js +30 -0
- package/src/routes/docs/pageConfig.js +36 -0
- package/src/routes/docs/parseUserParam.js +52 -0
- package/src/routes/docs/parseUserParam.test.mjs +45 -0
- package/src/routes/docs/pluginDoc.js +33 -0
- package/src/routes/docs/plugins.js +23 -0
- package/src/routes/docs/runRequest.js +36 -0
- package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
- package/src/routes/docs/screenshot.js +72 -0
- package/src/routes/docs/search.js +27 -0
- package/src/routes/docs/snapshotState.js +36 -0
- package/src/routes/docs/stateCheckpointsList.js +26 -0
- package/src/routes/docs/types.js +33 -0
- package/src/routes/endpoints.js +71 -0
- package/src/routes/feedback.js +110 -0
- package/src/routes/feedback.test.mjs +204 -0
- package/src/routes/jitPage.js +86 -0
- package/src/routes/mcp.js +34 -0
- package/{pages/api → src/routes}/ping.js +4 -2
- package/src/routes/reload.js +79 -0
- package/src/routes/request.js +74 -0
- package/src/routes/request.test.mjs +124 -0
- package/{pages/api → src/routes}/root.js +4 -4
- package/src/routes/usage.js +46 -0
- package/src/routes/websocket.js +33 -0
- package/src/websocket/devWebSocket.js +78 -0
- package/vite.config.js +111 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/App.js +0 -80
- package/lib/client/Reload.js +0 -60
- package/lib/server/apiWrapper.js +0 -109
- package/lib/server/compileCss.js +0 -39
- package/manager/processes/compileCss.mjs +0 -54
- package/manager/processes/nextBuild.mjs +0 -50
- package/next.config.js +0 -25
- package/pages/_document.js +0 -109
- package/pages/api/agent/[...path].js +0 -86
- package/pages/api/auth/[...nextauth].js +0 -48
- package/pages/api/client-error.js +0 -45
- package/pages/api/js/[env].js +0 -42
- package/pages/api/page/[...pageId].js +0 -74
- package/pages/api/reload.js +0 -53
- package/pages/api/request/[...path].js +0 -41
- /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
- /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
- /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
- /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
- /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
|
@@ -0,0 +1,366 @@
|
|
|
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 fs from 'fs';
|
|
18
|
+
import os from 'os';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { jest } from '@jest/globals';
|
|
21
|
+
|
|
22
|
+
// checkpointStore.js reads LOWDEFY_DIRECTORY_CONFIG for where to store
|
|
23
|
+
// checkpoints, and lib/build/config.js (imported transitively by
|
|
24
|
+
// loadState.js -> getBrowser.js) reads build/config.json from
|
|
25
|
+
// process.cwd() at import time — one fixture dir serves both, mirroring
|
|
26
|
+
// screenshotPage.test.mjs / configCheckpoints.test.mjs.
|
|
27
|
+
const originalCwd = process.cwd();
|
|
28
|
+
const previousConfigDirectory = process.env.LOWDEFY_DIRECTORY_CONFIG;
|
|
29
|
+
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-state-checkpoints-test-'));
|
|
30
|
+
fs.mkdirSync(path.join(fixtureDir, 'build'), { recursive: true });
|
|
31
|
+
fs.writeFileSync(path.join(fixtureDir, 'build', 'config.json'), JSON.stringify({ basePath: '' }));
|
|
32
|
+
process.chdir(fixtureDir);
|
|
33
|
+
process.env.LOWDEFY_DIRECTORY_CONFIG = fixtureDir;
|
|
34
|
+
|
|
35
|
+
// A real headless load needs an installed browser, which jest can't rely on —
|
|
36
|
+
// loadState's registry-only mode (the only mode under test here) never
|
|
37
|
+
// calls getBrowser(), but chromium is mocked anyway so importing
|
|
38
|
+
// getBrowser.js can never accidentally launch a real browser.
|
|
39
|
+
jest.unstable_mockModule('playwright-core', () => ({
|
|
40
|
+
chromium: { launch: jest.fn().mockRejectedValue(new Error("Executable doesn't exist")) },
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const mockInspectState = jest.fn();
|
|
44
|
+
jest.unstable_mockModule('./inspectState.js', () => ({
|
|
45
|
+
default: mockInspectState,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
checkpointExists,
|
|
50
|
+
getStateCheckpointsRoot,
|
|
51
|
+
listStateCheckpoints,
|
|
52
|
+
readCheckpoint,
|
|
53
|
+
writeCheckpoint,
|
|
54
|
+
} = await import('./checkpointStore.js');
|
|
55
|
+
const { clearMocks, getMock, listMocks, loadMocks } = await import('./devMockRegistry.js');
|
|
56
|
+
const { default: checkpointToMocks } = await import('./checkpointToMocks.js');
|
|
57
|
+
const { default: snapshotState } = await import('./snapshotState.js');
|
|
58
|
+
const { default: loadState } = await import('./loadState.js');
|
|
59
|
+
|
|
60
|
+
afterAll(() => {
|
|
61
|
+
process.chdir(originalCwd);
|
|
62
|
+
fs.rmSync(fixtureDir, { recursive: true, force: true });
|
|
63
|
+
if (previousConfigDirectory === undefined) {
|
|
64
|
+
delete process.env.LOWDEFY_DIRECTORY_CONFIG;
|
|
65
|
+
} else {
|
|
66
|
+
process.env.LOWDEFY_DIRECTORY_CONFIG = previousConfigDirectory;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
clearMocks();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function buildSnapshot(overrides = {}) {
|
|
75
|
+
return {
|
|
76
|
+
pageId: 'home',
|
|
77
|
+
url: 'http://localhost:3111/home',
|
|
78
|
+
state: { count: 1 },
|
|
79
|
+
urlQuery: '?a=1',
|
|
80
|
+
input: { fromInput: true },
|
|
81
|
+
user: { id: 'u1' },
|
|
82
|
+
global: { g: 1 },
|
|
83
|
+
requests: {
|
|
84
|
+
req1: [{ payload: { a: 1 }, response: { ok: true }, error: null, responseTime: 12 }],
|
|
85
|
+
},
|
|
86
|
+
apiResponses: {
|
|
87
|
+
ep1: { payload: {}, response: { v: 1 } },
|
|
88
|
+
},
|
|
89
|
+
...overrides,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe('checkpointStore', () => {
|
|
94
|
+
test('writeCheckpoint writes a manifest and per-part files, including per-request/api files on disk', () => {
|
|
95
|
+
const snapshot = buildSnapshot();
|
|
96
|
+
const { dir, parts } = writeCheckpoint({ name: 'cp-roundtrip', snapshot, notes: 'a note' });
|
|
97
|
+
|
|
98
|
+
expect(dir).toBe(path.join(getStateCheckpointsRoot(), 'cp-roundtrip'));
|
|
99
|
+
expect(parts.sort()).toEqual(
|
|
100
|
+
[
|
|
101
|
+
'checkpoint.json',
|
|
102
|
+
'state.json',
|
|
103
|
+
'urlQuery.json',
|
|
104
|
+
'inputs.json',
|
|
105
|
+
'user.json',
|
|
106
|
+
'global.json',
|
|
107
|
+
'requests/req1.json',
|
|
108
|
+
'api/ep1.json',
|
|
109
|
+
].sort()
|
|
110
|
+
);
|
|
111
|
+
expect(fs.existsSync(path.join(dir, 'requests', 'req1.json'))).toBe(true);
|
|
112
|
+
expect(fs.existsSync(path.join(dir, 'api', 'ep1.json'))).toBe(true);
|
|
113
|
+
|
|
114
|
+
const result = readCheckpoint({ name: 'cp-roundtrip' });
|
|
115
|
+
expect(result.checkpoint.pageId).toBe('home');
|
|
116
|
+
expect(result.checkpoint.url).toBe('http://localhost:3111/home');
|
|
117
|
+
expect(result.checkpoint.notes).toBe('a note');
|
|
118
|
+
expect(typeof result.checkpoint.capturedAt).toBe('string');
|
|
119
|
+
expect(result.state).toEqual({ count: 1 });
|
|
120
|
+
expect(result.urlQuery).toBe('?a=1');
|
|
121
|
+
expect(result.input).toEqual({ fromInput: true });
|
|
122
|
+
expect(result.user).toEqual({ id: 'u1' });
|
|
123
|
+
expect(result.global).toEqual({ g: 1 });
|
|
124
|
+
expect(result.requests).toEqual({
|
|
125
|
+
req1: { payload: { a: 1 }, response: { ok: true }, error: null, responseTime: 12 },
|
|
126
|
+
});
|
|
127
|
+
expect(result.api).toEqual({
|
|
128
|
+
ep1: { payload: {}, response: { v: 1 }, error: undefined, responseTime: undefined },
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('writeCheckpoint throws an actionable error for an invalid checkpoint name', () => {
|
|
133
|
+
expect(() => writeCheckpoint({ name: 'bad name!', snapshot: buildSnapshot() })).toThrow(
|
|
134
|
+
/invalid/
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('writeCheckpoint refuses to overwrite an existing checkpoint unless overwrite: true', () => {
|
|
139
|
+
writeCheckpoint({ name: 'cp-overwrite', snapshot: buildSnapshot() });
|
|
140
|
+
|
|
141
|
+
expect(() => writeCheckpoint({ name: 'cp-overwrite', snapshot: buildSnapshot() })).toThrow(
|
|
142
|
+
'Checkpoint "cp-overwrite" already exists. Pass overwrite: true to replace it.'
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const fewerRequests = buildSnapshot({ requests: {}, apiResponses: {} });
|
|
146
|
+
const { dir } = writeCheckpoint({
|
|
147
|
+
name: 'cp-overwrite',
|
|
148
|
+
snapshot: fewerRequests,
|
|
149
|
+
overwrite: true,
|
|
150
|
+
});
|
|
151
|
+
// Overwriting replaces the whole folder, so stale per-request files from
|
|
152
|
+
// the first write must not linger.
|
|
153
|
+
expect(fs.existsSync(path.join(dir, 'requests', 'req1.json'))).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('readCheckpoint throws when the checkpoint does not exist', () => {
|
|
157
|
+
expect(() => readCheckpoint({ name: 'does-not-exist' })).toThrow(
|
|
158
|
+
'Checkpoint "does-not-exist" not found.'
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('readCheckpoint is tolerant of missing parts', () => {
|
|
163
|
+
const dir = path.join(getStateCheckpointsRoot(), 'cp-partial');
|
|
164
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
165
|
+
fs.writeFileSync(path.join(dir, 'checkpoint.json'), JSON.stringify({ pageId: 'home' }));
|
|
166
|
+
// Deliberately no state.json/urlQuery.json/etc, no requests/ dir.
|
|
167
|
+
|
|
168
|
+
const result = readCheckpoint({ name: 'cp-partial' });
|
|
169
|
+
expect(result.state).toEqual({});
|
|
170
|
+
expect(result.urlQuery).toBe('');
|
|
171
|
+
expect(result.input).toEqual({});
|
|
172
|
+
expect(result.user).toBe(null);
|
|
173
|
+
expect(result.global).toEqual({});
|
|
174
|
+
expect(result.requests).toEqual({});
|
|
175
|
+
expect(result.api).toEqual({});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('checkpointExists reflects whether a checkpoint folder exists', () => {
|
|
179
|
+
writeCheckpoint({ name: 'cp-exists', snapshot: buildSnapshot() });
|
|
180
|
+
expect(checkpointExists({ name: 'cp-exists' })).toBe(true);
|
|
181
|
+
expect(checkpointExists({ name: 'cp-does-not-exist' })).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('listStateCheckpoints lists checkpoints with requestCount, most recent first', async () => {
|
|
185
|
+
writeCheckpoint({ name: 'cp-list-a', snapshot: buildSnapshot() });
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
187
|
+
writeCheckpoint({
|
|
188
|
+
name: 'cp-list-b',
|
|
189
|
+
snapshot: buildSnapshot({ requests: {}, apiResponses: {} }),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const checkpoints = listStateCheckpoints();
|
|
193
|
+
const a = checkpoints.find((checkpoint) => checkpoint.name === 'cp-list-a');
|
|
194
|
+
const b = checkpoints.find((checkpoint) => checkpoint.name === 'cp-list-b');
|
|
195
|
+
expect(a.requestCount).toBe(1);
|
|
196
|
+
expect(b.requestCount).toBe(0);
|
|
197
|
+
expect(checkpoints.indexOf(b)).toBeLessThan(checkpoints.indexOf(a));
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('devMockRegistry', () => {
|
|
202
|
+
test('loadMocks + getMock replay a recorded response by pageId/requestId', () => {
|
|
203
|
+
loadMocks({ pageId: 'home', mocks: { req1: { response: { ok: true }, error: null } } });
|
|
204
|
+
expect(getMock({ pageId: 'home', requestId: 'req1' })).toEqual({
|
|
205
|
+
pageId: 'home',
|
|
206
|
+
requestId: 'req1',
|
|
207
|
+
response: { ok: true },
|
|
208
|
+
error: null,
|
|
209
|
+
});
|
|
210
|
+
expect(getMock({ pageId: 'other', requestId: 'req1' })).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('listMocks reports whether each entry has a response or an error', () => {
|
|
214
|
+
loadMocks({
|
|
215
|
+
pageId: 'home',
|
|
216
|
+
mocks: {
|
|
217
|
+
req1: { response: { ok: true } },
|
|
218
|
+
req2: { error: new Error('boom') },
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
const mocks = listMocks().sort((m1, m2) => m1.requestId.localeCompare(m2.requestId));
|
|
222
|
+
expect(mocks).toEqual([
|
|
223
|
+
{ pageId: 'home', requestId: 'req1', hasResponse: true, hasError: false },
|
|
224
|
+
{ pageId: 'home', requestId: 'req2', hasResponse: false, hasError: true },
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('clearMocks removes all recorded mocks', () => {
|
|
229
|
+
loadMocks({ pageId: 'home', mocks: { req1: { response: { ok: true } } } });
|
|
230
|
+
clearMocks();
|
|
231
|
+
expect(listMocks()).toEqual([]);
|
|
232
|
+
expect(getMock({ pageId: 'home', requestId: 'req1' })).toBeUndefined();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('checkpointToMocks', () => {
|
|
237
|
+
test('converts a checkpoint into the loadStaticMocks input shape', () => {
|
|
238
|
+
writeCheckpoint({
|
|
239
|
+
name: 'cp-to-mocks',
|
|
240
|
+
snapshot: buildSnapshot({
|
|
241
|
+
requests: {
|
|
242
|
+
req1: [{ response: { ok: true } }],
|
|
243
|
+
req2: [{ error: new Error('request failed') }],
|
|
244
|
+
},
|
|
245
|
+
apiResponses: {
|
|
246
|
+
ep1: [{ response: { v: 1 } }],
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const result = checkpointToMocks({ name: 'cp-to-mocks' });
|
|
252
|
+
|
|
253
|
+
expect(result.requests.sort((a, b) => a.requestId.localeCompare(b.requestId))).toEqual([
|
|
254
|
+
{ requestId: 'req1', pageId: 'home', response: { ok: true } },
|
|
255
|
+
{ requestId: 'req2', pageId: 'home', error: 'request failed' },
|
|
256
|
+
]);
|
|
257
|
+
expect(result.api).toEqual([{ endpointId: 'ep1', response: { v: 1 } }]);
|
|
258
|
+
expect(result.yaml).toEqual(expect.any(String));
|
|
259
|
+
expect(result.yaml).toContain('requestId: req1');
|
|
260
|
+
expect(result.yaml).toContain('endpointId: ep1');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('returns an error when the checkpoint does not exist', () => {
|
|
264
|
+
expect(checkpointToMocks({ name: 'does-not-exist' })).toEqual({
|
|
265
|
+
error: 'Checkpoint "does-not-exist" not found.',
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('snapshotState', () => {
|
|
271
|
+
test('snapshots inspectState output into a checkpoint and returns its location', async () => {
|
|
272
|
+
mockInspectState.mockResolvedValue({
|
|
273
|
+
pageId: 'home',
|
|
274
|
+
state: { count: 2 },
|
|
275
|
+
requests: {},
|
|
276
|
+
urlQuery: '',
|
|
277
|
+
source: 'headless',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const result = await snapshotState({
|
|
281
|
+
origin: 'http://localhost:3111',
|
|
282
|
+
pageId: 'home',
|
|
283
|
+
name: 'cp-snapshot',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(result.error).toBeUndefined();
|
|
287
|
+
expect(result.name).toBe('cp-snapshot');
|
|
288
|
+
expect(result.source).toBe('headless');
|
|
289
|
+
expect(result.parts).toContain('state.json');
|
|
290
|
+
|
|
291
|
+
const stored = readCheckpoint({ name: 'cp-snapshot' });
|
|
292
|
+
expect(stored.state).toEqual({ count: 2 });
|
|
293
|
+
expect(stored.checkpoint.url).toBe('http://localhost:3111/home');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('returns the inspectState error instead of writing a checkpoint', async () => {
|
|
297
|
+
mockInspectState.mockResolvedValue({ error: 'No live context for page "home".' });
|
|
298
|
+
|
|
299
|
+
const result = await snapshotState({
|
|
300
|
+
origin: 'http://localhost:3111',
|
|
301
|
+
pageId: 'home',
|
|
302
|
+
name: 'cp-snapshot-error',
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(result).toEqual({ error: 'No live context for page "home".' });
|
|
306
|
+
expect(checkpointExists({ name: 'cp-snapshot-error' })).toBe(false);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('validates required fields before calling inspectState', async () => {
|
|
310
|
+
const result = await snapshotState({ origin: 'http://localhost:3111', name: 'cp-x' });
|
|
311
|
+
expect(result.error).toMatch(/requires a "pageId" string/);
|
|
312
|
+
expect(mockInspectState).not.toHaveBeenCalled();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('loadState', () => {
|
|
317
|
+
test('registry-only mode loads recorded requests into devMockRegistry and returns a bootstrap URL', async () => {
|
|
318
|
+
writeCheckpoint({
|
|
319
|
+
name: 'cp-load-registry',
|
|
320
|
+
snapshot: buildSnapshot({ urlQuery: '?a=1' }),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const result = await loadState({
|
|
324
|
+
origin: 'http://localhost:3111',
|
|
325
|
+
name: 'cp-load-registry',
|
|
326
|
+
mode: 'registry-only',
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(result.error).toBeUndefined();
|
|
330
|
+
expect(result.url).toBe('http://localhost:3111/home?a=1&_checkpoint=cp-load-registry');
|
|
331
|
+
expect(result.instructions).toMatch(/Open this URL/);
|
|
332
|
+
expect(getMock({ pageId: 'home', requestId: 'req1' })).toEqual({
|
|
333
|
+
pageId: 'home',
|
|
334
|
+
requestId: 'req1',
|
|
335
|
+
response: { ok: true },
|
|
336
|
+
error: null,
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('returns an error when the checkpoint does not exist', async () => {
|
|
341
|
+
const result = await loadState({
|
|
342
|
+
origin: 'http://localhost:3111',
|
|
343
|
+
name: 'does-not-exist',
|
|
344
|
+
mode: 'registry-only',
|
|
345
|
+
});
|
|
346
|
+
expect(result).toEqual({ error: 'Checkpoint "does-not-exist" not found.' });
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('rejects a user in registry-only mode as invalid input', async () => {
|
|
350
|
+
writeCheckpoint({
|
|
351
|
+
name: 'cp-load-registry-user',
|
|
352
|
+
snapshot: buildSnapshot({ urlQuery: '' }),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const result = await loadState({
|
|
356
|
+
origin: 'http://localhost:3111',
|
|
357
|
+
name: 'cp-load-registry-user',
|
|
358
|
+
mode: 'registry-only',
|
|
359
|
+
user: { roles: ['admin'] },
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
expect(result.invalidInput).toBe(true);
|
|
363
|
+
expect(result.error).toMatch(/cannot apply "user" in "registry-only" mode/);
|
|
364
|
+
expect(result.url).toBeUndefined();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
@@ -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 { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import { listTabs } from './tabChannel.js';
|
|
20
|
+
|
|
21
|
+
// Shared by inspectState.js and evalOperator.js to decide, when no explicit
|
|
22
|
+
// `source` is requested, whether a live browser tab exists to prefer over a
|
|
23
|
+
// fresh headless one.
|
|
24
|
+
function tabAvailable({ pageId }) {
|
|
25
|
+
return listTabs().some((tab) => type.isNone(pageId) || tab.pageId === pageId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default tabAvailable;
|
|
@@ -0,0 +1,119 @@
|
|
|
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 { randomUUID } from 'node:crypto';
|
|
18
|
+
|
|
19
|
+
import { type } from '@lowdefy/helpers';
|
|
20
|
+
|
|
21
|
+
// Live channel between the dev server and a developer's real browser tab —
|
|
22
|
+
// lets an agent read `window.lowdefy` state / evaluate operators against it.
|
|
23
|
+
// Two module-level registries, mirroring the pendingContexts pattern in
|
|
24
|
+
// src/websocket/devWebSocket.js:
|
|
25
|
+
// - tabs: connected SSE streams, one per open browser tab, keyed by tab id.
|
|
26
|
+
// - pendingRequests: in-flight request/response correlations, keyed by
|
|
27
|
+
// requestId, resolved either by resolveTabRequest (POST from the tab) or
|
|
28
|
+
// by the request's own timeout.
|
|
29
|
+
const tabs = new Map();
|
|
30
|
+
const pendingRequests = new Map();
|
|
31
|
+
|
|
32
|
+
function registerTab({ id, pageId, send }) {
|
|
33
|
+
if (type.isNone(id) || !type.isString(id)) {
|
|
34
|
+
throw new Error(`registerTab requires an "id" string. Received ${JSON.stringify(id)}.`);
|
|
35
|
+
}
|
|
36
|
+
if (!type.isFunction(send)) {
|
|
37
|
+
throw new Error('registerTab requires a "send" function.');
|
|
38
|
+
}
|
|
39
|
+
tabs.set(id, { id, pageId: pageId ?? null, send, connectedAt: new Date() });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function updateTabPage({ id, pageId }) {
|
|
43
|
+
const tab = tabs.get(id);
|
|
44
|
+
if (type.isNone(tab)) {
|
|
45
|
+
// The tab may have disconnected between the client sending the ping and
|
|
46
|
+
// it arriving — nothing to update, and not worth failing the request.
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
tab.pageId = pageId ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function unregisterTab({ id }) {
|
|
53
|
+
tabs.delete(id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function listTabs() {
|
|
57
|
+
return Array.from(tabs.values()).map(({ id, pageId, connectedAt }) => ({
|
|
58
|
+
id,
|
|
59
|
+
pageId,
|
|
60
|
+
connectedAt,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Most recently connected tab wins — Map preserves insertion order, and
|
|
65
|
+
// re-registering a tab id (reconnect) deletes then re-sets it, so the last
|
|
66
|
+
// entry is always the most recent connection.
|
|
67
|
+
function findTab({ pageId }) {
|
|
68
|
+
const candidates = Array.from(tabs.values()).filter(
|
|
69
|
+
(tab) => type.isNone(pageId) || tab.pageId === pageId
|
|
70
|
+
);
|
|
71
|
+
if (candidates.length === 0) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return candidates[candidates.length - 1];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function requestFromTab({ pageId, event, payload = {}, timeout = 5000 }) {
|
|
78
|
+
if (type.isNone(event) || !type.isString(event)) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`requestFromTab requires an "event" string. Received ${JSON.stringify(event)}.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const tab = findTab({ pageId });
|
|
84
|
+
if (type.isNone(tab)) {
|
|
85
|
+
const location = type.isNone(pageId) ? 'any page' : `page "${pageId}"`;
|
|
86
|
+
return Promise.resolve({
|
|
87
|
+
error: `No browser tab connected on ${location}. Ask the developer to open the page, or use source: "headless".`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const requestId = randomUUID();
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
pendingRequests.delete(requestId);
|
|
95
|
+
resolve({ error: `Timed out waiting ${timeout}ms for a response from the browser tab.` });
|
|
96
|
+
}, timeout);
|
|
97
|
+
|
|
98
|
+
pendingRequests.set(requestId, (result) => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
pendingRequests.delete(requestId);
|
|
101
|
+
resolve(result);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
tab.send(event, { requestId, ...payload });
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveTabRequest({ requestId, result }) {
|
|
109
|
+
const resolver = pendingRequests.get(requestId);
|
|
110
|
+
if (type.isNone(resolver)) {
|
|
111
|
+
// Answer arrived after the request already timed out (or for an unknown
|
|
112
|
+
// requestId) — nothing left to resolve.
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
resolver(result);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { listTabs, registerTab, requestFromTab, resolveTabRequest, unregisterTab, updateTabPage };
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
const { listTabs, registerTab, requestFromTab, resolveTabRequest, unregisterTab, updateTabPage } =
|
|
20
|
+
await import('./tabChannel.js');
|
|
21
|
+
|
|
22
|
+
// tabChannel keeps its registries at module scope, so clear connected tabs
|
|
23
|
+
// between tests to keep them independent.
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
listTabs().forEach((tab) => unregisterTab({ id: tab.id }));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('registerTab adds a tab visible via listTabs', () => {
|
|
29
|
+
registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
|
|
30
|
+
const tabs = listTabs();
|
|
31
|
+
expect(tabs).toHaveLength(1);
|
|
32
|
+
expect(tabs[0]).toMatchObject({ id: 'tab-1', pageId: 'home' });
|
|
33
|
+
expect(tabs[0].connectedAt).toBeInstanceOf(Date);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('registerTab throws when id is missing', () => {
|
|
37
|
+
expect(() => registerTab({ pageId: 'home', send: jest.fn() })).toThrow('registerTab requires');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('registerTab throws when send is not a function', () => {
|
|
41
|
+
expect(() => registerTab({ id: 'tab-1', pageId: 'home' })).toThrow(
|
|
42
|
+
'registerTab requires a "send" function'
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('unregisterTab removes a tab from the registry', () => {
|
|
47
|
+
registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
|
|
48
|
+
unregisterTab({ id: 'tab-1' });
|
|
49
|
+
expect(listTabs()).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('updateTabPage changes the pageId of a registered tab', () => {
|
|
53
|
+
registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
|
|
54
|
+
updateTabPage({ id: 'tab-1', pageId: 'about' });
|
|
55
|
+
expect(listTabs()[0].pageId).toEqual('about');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('updateTabPage on an unknown tab id does not throw', () => {
|
|
59
|
+
expect(() => updateTabPage({ id: 'missing', pageId: 'about' })).not.toThrow();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('requestFromTab sends the event to the matching tab and resolves via resolveTabRequest', async () => {
|
|
63
|
+
const send = jest.fn();
|
|
64
|
+
registerTab({ id: 'tab-1', pageId: 'home', send });
|
|
65
|
+
const promise = requestFromTab({ pageId: 'home', event: 'inspect-request' });
|
|
66
|
+
|
|
67
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
68
|
+
const [event, payload] = send.mock.calls[0];
|
|
69
|
+
expect(event).toEqual('inspect-request');
|
|
70
|
+
expect(payload.requestId).toEqual(expect.any(String));
|
|
71
|
+
|
|
72
|
+
const resolved = resolveTabRequest({ requestId: payload.requestId, result: 'snapshot' });
|
|
73
|
+
expect(resolved).toBe(true);
|
|
74
|
+
await expect(promise).resolves.toEqual('snapshot');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('requestFromTab targets the tab registered for the given pageId', async () => {
|
|
78
|
+
const sendHome = jest.fn();
|
|
79
|
+
const sendAbout = jest.fn();
|
|
80
|
+
registerTab({ id: 'tab-home', pageId: 'home', send: sendHome });
|
|
81
|
+
registerTab({ id: 'tab-about', pageId: 'about', send: sendAbout });
|
|
82
|
+
|
|
83
|
+
const promise = requestFromTab({ pageId: 'about', event: 'eval-request' });
|
|
84
|
+
expect(sendAbout).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(sendHome).not.toHaveBeenCalled();
|
|
86
|
+
|
|
87
|
+
const requestId = sendAbout.mock.calls[0][1].requestId;
|
|
88
|
+
resolveTabRequest({ requestId, result: { value: '1' } });
|
|
89
|
+
await expect(promise).resolves.toEqual({ value: '1' });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('requestFromTab picks the most recently registered tab when pageId is omitted', async () => {
|
|
93
|
+
registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
|
|
94
|
+
const sendLatest = jest.fn();
|
|
95
|
+
registerTab({ id: 'tab-2', pageId: 'about', send: sendLatest });
|
|
96
|
+
|
|
97
|
+
const promise = requestFromTab({ event: 'inspect-request' });
|
|
98
|
+
expect(sendLatest).toHaveBeenCalledTimes(1);
|
|
99
|
+
|
|
100
|
+
const requestId = sendLatest.mock.calls[0][1].requestId;
|
|
101
|
+
resolveTabRequest({ requestId, result: 'ok' });
|
|
102
|
+
await expect(promise).resolves.toEqual('ok');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('requestFromTab resolves with an error when no tab matches the pageId', async () => {
|
|
106
|
+
registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
|
|
107
|
+
const response = await requestFromTab({ pageId: 'missing-page', event: 'inspect-request' });
|
|
108
|
+
expect(response.error).toContain('No browser tab connected');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('requestFromTab resolves with a timeout error when no response arrives in time', async () => {
|
|
112
|
+
registerTab({ id: 'tab-1', pageId: 'home', send: jest.fn() });
|
|
113
|
+
const response = await requestFromTab({ pageId: 'home', event: 'inspect-request', timeout: 10 });
|
|
114
|
+
expect(response.error).toContain('Timed out');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('resolveTabRequest returns false for an unknown requestId', () => {
|
|
118
|
+
expect(resolveTabRequest({ requestId: 'unknown', result: 'x' })).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('resolveTabRequest returns false when called again for an already-settled requestId', async () => {
|
|
122
|
+
const send = jest.fn();
|
|
123
|
+
registerTab({ id: 'tab-1', pageId: 'home', send });
|
|
124
|
+
const promise = requestFromTab({ pageId: 'home', event: 'inspect-request' });
|
|
125
|
+
const requestId = send.mock.calls[0][1].requestId;
|
|
126
|
+
|
|
127
|
+
expect(resolveTabRequest({ requestId, result: 'first' })).toBe(true);
|
|
128
|
+
await promise;
|
|
129
|
+
expect(resolveTabRequest({ requestId, result: 'second' })).toBe(false);
|
|
130
|
+
});
|
|
@@ -14,22 +14,25 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { getAuthConfig as getApiAuthConfig } from '@lowdefy/api';
|
|
18
18
|
import { getSecretsFromEnv } from '@lowdefy/node-utils';
|
|
19
19
|
|
|
20
20
|
import adapters from '../../../build/plugins/auth/adapters.js';
|
|
21
|
+
import appMeta from '../../build/appMeta.js';
|
|
21
22
|
import authJson from '../../build/auth.js';
|
|
22
23
|
import callbacks from '../../../build/plugins/auth/callbacks.js';
|
|
23
24
|
import events from '../../../build/plugins/auth/events.js';
|
|
24
25
|
import providers from '../../../build/plugins/auth/providers.js';
|
|
25
26
|
|
|
26
|
-
function
|
|
27
|
-
return
|
|
27
|
+
function getAuthConfig({ logger }) {
|
|
28
|
+
return getApiAuthConfig({
|
|
29
|
+
appMeta,
|
|
28
30
|
authJson,
|
|
31
|
+
dev: true,
|
|
29
32
|
logger,
|
|
30
33
|
plugins: { adapters, callbacks, events, providers },
|
|
31
34
|
secrets: getSecretsFromEnv(),
|
|
32
35
|
});
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
export default
|
|
38
|
+
export default getAuthConfig;
|