shadow-claw 1.52.0 → 1.52.1
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/.well-known/mcp/server-card.json +1 -1
- package/.well-known/mcp.json +1 -1
- package/README.md +9 -9
- package/dist/cli/build/build.js +1 -1
- package/dist/cli/{chunk-DYq-orHK.js → chunk-BgTv9w0K.js} +1 -1
- package/dist/cli/{chunk-C6jii2Fl.js → chunk-d5hIhj8t.js} +1 -1
- package/dist/cli/cli.js +1 -1
- package/dist/lib/components/index.js +1 -1
- package/dist/lib/components/shadow-claw-pages.js +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/{shadow-claw-pages-kA2qa-4B.js → shadow-claw-pages-CjHS50HN.js} +2 -2
- package/dist/lib/{shadow-claw-pages-kA2qa-4B.js.map → shadow-claw-pages-CjHS50HN.js.map} +1 -1
- package/dist/public/.well-known/mcp/server-card.json +1 -1
- package/dist/public/.well-known/mcp.json +1 -1
- package/dist/public/AGENTS.md +1 -1
- package/dist/public/README.md +9 -9
- package/dist/public/docs/README.md +1 -1
- package/dist/public/docs/architecture/orchestrator.md +2 -2
- package/dist/public/docs/architecture/overview.md +3 -3
- package/dist/public/docs/architecture/worker-protocol.md +29 -29
- package/dist/public/docs/example/article/index.html +1 -1
- package/dist/public/docs/example/article.html +1 -1
- package/dist/public/docs/publishing/index.html +1 -1
- package/dist/public/docs/publishing.html +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- package/dist/public/docs/skill-creator.html +1 -1
- package/dist/public/docs/subsystems/local-models.md +1 -1
- package/dist/public/docs/subsystems/providers.md +11 -4
- package/dist/public/e2e/README.md +2 -2
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- package/dist/public/llms.txt +1 -1
- package/dist/public/main/index.html +1 -1
- package/dist/public/main/memory/index.html +1 -1
- package/dist/public/main/memory.html +1 -1
- package/dist/public/main.html +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-Ba2ZFE9m.js → shadow-claw-T9oPpqxT.js} +1 -1
- package/dist/public/{shadow-claw-pages-Cy6T0G9y.js → shadow-claw-pages-IW_LNEL_.js} +1 -1
- package/package.json +1 -1
- package/src/cli/build/build.test.ts +95 -0
- package/src/cli/build/build.ts +5 -1
- package/src/cli/commands/agent.test.ts +134 -0
- package/src/cli/commands/backup.test.ts +85 -0
- package/src/cli/commands/clients.test.ts +32 -0
- package/src/cli/commands/mcp.test.ts +260 -1
- package/src/cli/commands/send-file.test.ts +73 -0
- package/src/cli/commands/send.test.ts +58 -0
- package/src/cli/commands/tasks.test.ts +78 -0
- package/src/cli/commands/webrtc-listen.test.ts +74 -0
- package/src/cli/tools/assert-version-bump.test.ts +30 -1
- package/src/cli/tools/file-search-replace.test.ts +33 -1
- package/src/cli/tools/patch-service-worker-trusted-types.test.ts +24 -1
- package/src/cli/tools/touch-nojekyll.test.ts +12 -0
- package/src/cli/tools/version.test.ts +109 -5
- package/src/cli/utils/agent-core.test.ts +40 -0
- package/src/cli/utils/control-client.test.ts +239 -2
- package/src/cli/utils/load-workspace-config.test.ts +63 -0
- package/src/cli/utils/local-models.test.ts +98 -0
- package/src/cli/utils/progress-bar.test.ts +41 -2
- package/src/cli/utils/resolve-cache-dir.test.ts +61 -0
- package/src/cli/utils/resolve-project-root.test.ts +17 -0
- package/src/cli/utils/stdin.test.ts +38 -0
- package/src/cli/utils/stdio.test.ts +51 -1
- package/src/cli/utils/suppress-warnings.test.ts +9 -0
- package/src/cli/utils/webrtc-control-client.test.ts +569 -0
- package/src/common/utils/frontmatter.test.ts +117 -0
- package/src/components/common/help/providers.test.ts +67 -0
- package/src/components/common/shadow-claw-provider-model-picker/shadow-claw-provider-model-picker.test.ts +121 -0
- package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +131 -0
- package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +130 -0
- package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +12 -0
- package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +328 -0
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +530 -8
- package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +459 -0
- package/src/components/settings/shadow-claw-networking/shadow-claw-networking.test.ts +88 -0
- package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.test.ts +92 -0
- package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +127 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +173 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +62 -0
- package/src/components/settings/shadow-claw-webvm/shadow-claw-webvm.test.ts +104 -0
- package/src/components/shadow-claw/shadow-claw.test.ts +112 -0
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +109 -0
- package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +361 -0
- package/src/components/shadow-claw-dialog/shadow-claw-dialog.test.ts +76 -0
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +307 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +398 -1
- package/src/components/shadow-claw-pages/shadow-claw-pages.test.ts +85 -0
- package/src/components/shadow-claw-pages/shadow-claw-pages.ts +2 -1
- package/src/components/shadow-claw-pdf-viewer/shadow-claw-pdf-viewer.test.ts +229 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +233 -0
- package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +223 -1
- package/src/components/shadow-claw-toast/shadow-claw-toast.test.ts +79 -0
- package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +229 -1
- package/src/server/logger.test.ts +57 -0
- package/src/server/peer.test.ts +47 -0
- package/src/server/routes/gemini.test.ts +244 -0
- package/src/server/server-peer.test.ts +245 -11
- package/src/server/services/llamafile-manager.test.ts +668 -0
- package/src/share-target/pending-shares.test.ts +90 -0
- package/src/shell/fs.test.ts +125 -0
- package/src/shell/shell.test.ts +8 -0
- package/src/subsystems/git/credentials.test.ts +260 -0
- package/src/subsystems/git/git.test.ts +64 -0
- package/src/subsystems/providers/executeNativeAiTask.test.ts +78 -0
- package/src/subsystems/providers/utils/parseLocalModelToolCall.test.ts +57 -0
- package/src/worker/tools/builtin-ai/builtin-ai.test.ts +88 -0
- package/src/worker/tools/email/email.test.ts +358 -0
- package/src/worker/tools/email/utils/resolveIntegrationEmailAuth.test.ts +120 -0
- package/src/worker/tools/email/utils/resolveUniqueAttachmentPath.test.ts +80 -0
- package/src/worker/tools/fetch-file/fetch-file.test.ts +111 -0
- package/src/worker/tools/fetch-url/fetch-url.test.ts +295 -0
|
@@ -139,4 +139,233 @@ describe("shadow-claw-pdf-viewer", () => {
|
|
|
139
139
|
|
|
140
140
|
expect(panWrap.classList.contains("is-panning")).toBe(false);
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
it("handles pagination and zoom button interactions", async () => {
|
|
144
|
+
const renderTask: any = {
|
|
145
|
+
promise: Promise.resolve(),
|
|
146
|
+
cancel: jest.fn(),
|
|
147
|
+
};
|
|
148
|
+
const page: any = {
|
|
149
|
+
getViewport: ({ scale }: any) => ({
|
|
150
|
+
width: 200 * scale,
|
|
151
|
+
height: 300 * scale,
|
|
152
|
+
}),
|
|
153
|
+
render: jest.fn(() => renderTask),
|
|
154
|
+
};
|
|
155
|
+
const pdfDocument: any = {
|
|
156
|
+
numPages: 3,
|
|
157
|
+
getPage: jest.fn(async () => page),
|
|
158
|
+
destroy: jest.fn(),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
(getDocumentMock as any).mockImplementation(() => ({
|
|
162
|
+
promise: Promise.resolve(pdfDocument),
|
|
163
|
+
destroy: jest.fn(),
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
const viewer = new ShadowClawPdfViewer();
|
|
167
|
+
document.body.appendChild(viewer);
|
|
168
|
+
|
|
169
|
+
viewer.file = {
|
|
170
|
+
name: "sample.pdf",
|
|
171
|
+
binaryContent: new Uint8Array([1, 2, 3, 4]),
|
|
172
|
+
};
|
|
173
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
174
|
+
|
|
175
|
+
const nextBtn = viewer.shadowRoot?.querySelector(
|
|
176
|
+
".pdf-next",
|
|
177
|
+
) as HTMLButtonElement;
|
|
178
|
+
const prevBtn = viewer.shadowRoot?.querySelector(
|
|
179
|
+
".pdf-prev",
|
|
180
|
+
) as HTMLButtonElement;
|
|
181
|
+
const zoomInBtn = viewer.shadowRoot?.querySelector(
|
|
182
|
+
".pdf-zoom-in",
|
|
183
|
+
) as HTMLButtonElement;
|
|
184
|
+
const zoomOutBtn = viewer.shadowRoot?.querySelector(
|
|
185
|
+
".pdf-zoom-out",
|
|
186
|
+
) as HTMLButtonElement;
|
|
187
|
+
const pageInfo = viewer.shadowRoot?.querySelector(".pdf-page");
|
|
188
|
+
|
|
189
|
+
expect(viewer._pageNumber).toBe(1);
|
|
190
|
+
expect(pageInfo?.textContent).toContain("Page 1 of 3");
|
|
191
|
+
|
|
192
|
+
// Click Next
|
|
193
|
+
nextBtn.dispatchEvent(new Event("click"));
|
|
194
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
195
|
+
expect(viewer._pageNumber).toBe(2);
|
|
196
|
+
|
|
197
|
+
// Click Next again
|
|
198
|
+
nextBtn.dispatchEvent(new Event("click"));
|
|
199
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
200
|
+
expect(viewer._pageNumber).toBe(3);
|
|
201
|
+
|
|
202
|
+
// Clicking Next when at max pages does not exceed max
|
|
203
|
+
nextBtn.dispatchEvent(new Event("click"));
|
|
204
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
205
|
+
expect(viewer._pageNumber).toBe(3);
|
|
206
|
+
|
|
207
|
+
// Click Prev
|
|
208
|
+
prevBtn.dispatchEvent(new Event("click"));
|
|
209
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
210
|
+
expect(viewer._pageNumber).toBe(2);
|
|
211
|
+
|
|
212
|
+
// Click Prev to 1
|
|
213
|
+
prevBtn.dispatchEvent(new Event("click"));
|
|
214
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
215
|
+
expect(viewer._pageNumber).toBe(1);
|
|
216
|
+
|
|
217
|
+
// Clicking Prev at page 1 does not decrement
|
|
218
|
+
prevBtn.dispatchEvent(new Event("click"));
|
|
219
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
220
|
+
expect(viewer._pageNumber).toBe(1);
|
|
221
|
+
|
|
222
|
+
// Zoom in
|
|
223
|
+
const initialScale = viewer._scale;
|
|
224
|
+
zoomInBtn.dispatchEvent(new Event("click"));
|
|
225
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
226
|
+
expect(viewer._scale).toBeGreaterThan(initialScale);
|
|
227
|
+
|
|
228
|
+
// Zoom out
|
|
229
|
+
zoomOutBtn.dispatchEvent(new Event("click"));
|
|
230
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
231
|
+
expect(viewer._scale).toBeCloseTo(initialScale);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("handles touch panning", async () => {
|
|
235
|
+
const viewer = new ShadowClawPdfViewer();
|
|
236
|
+
document.body.appendChild(viewer);
|
|
237
|
+
|
|
238
|
+
viewer.file = {
|
|
239
|
+
name: "sample.pdf",
|
|
240
|
+
binaryContent: new Uint8Array([1, 2, 3, 4]),
|
|
241
|
+
};
|
|
242
|
+
await Promise.resolve();
|
|
243
|
+
await Promise.resolve();
|
|
244
|
+
|
|
245
|
+
const wrap = viewer.shadowRoot?.querySelector(
|
|
246
|
+
".pdf-canvas-wrap",
|
|
247
|
+
) as HTMLElement;
|
|
248
|
+
wrap.scrollLeft = 50;
|
|
249
|
+
wrap.scrollTop = 50;
|
|
250
|
+
|
|
251
|
+
const touchStartEvent = new CustomEvent("touchstart", {
|
|
252
|
+
bubbles: true,
|
|
253
|
+
cancelable: true,
|
|
254
|
+
});
|
|
255
|
+
Object.defineProperty(touchStartEvent, "touches", {
|
|
256
|
+
value: [{ clientX: 100, clientY: 100 }],
|
|
257
|
+
});
|
|
258
|
+
wrap.dispatchEvent(touchStartEvent);
|
|
259
|
+
|
|
260
|
+
expect(viewer._isPanning).toBe(true);
|
|
261
|
+
|
|
262
|
+
const touchMoveEvent = new CustomEvent("touchmove", {
|
|
263
|
+
bubbles: true,
|
|
264
|
+
cancelable: true,
|
|
265
|
+
});
|
|
266
|
+
Object.defineProperty(touchMoveEvent, "touches", {
|
|
267
|
+
value: [{ clientX: 70, clientY: 60 }],
|
|
268
|
+
});
|
|
269
|
+
globalThis.dispatchEvent(touchMoveEvent);
|
|
270
|
+
|
|
271
|
+
expect(wrap.scrollLeft).toBe(80);
|
|
272
|
+
expect(wrap.scrollTop).toBe(90);
|
|
273
|
+
|
|
274
|
+
const touchEndEvent = new CustomEvent("touchend", {
|
|
275
|
+
bubbles: true,
|
|
276
|
+
cancelable: true,
|
|
277
|
+
});
|
|
278
|
+
globalThis.dispatchEvent(touchEndEvent);
|
|
279
|
+
|
|
280
|
+
expect(viewer._isPanning).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("handles empty or unavailable PDF data", async () => {
|
|
284
|
+
const viewer = new ShadowClawPdfViewer();
|
|
285
|
+
document.body.appendChild(viewer);
|
|
286
|
+
|
|
287
|
+
viewer.file = {
|
|
288
|
+
name: "empty.pdf",
|
|
289
|
+
binaryContent: new Uint8Array(0),
|
|
290
|
+
};
|
|
291
|
+
await Promise.resolve();
|
|
292
|
+
|
|
293
|
+
const status = viewer.shadowRoot?.querySelector(".pdf-status");
|
|
294
|
+
expect(status?.textContent).toBe("PDF data unavailable.");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("handles getDocument rejection during PDF load", async () => {
|
|
298
|
+
(getDocumentMock as any).mockImplementationOnce(() => ({
|
|
299
|
+
promise: Promise.reject(new Error("Corrupt PDF")),
|
|
300
|
+
destroy: jest.fn(),
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
const viewer = new ShadowClawPdfViewer();
|
|
304
|
+
document.body.appendChild(viewer);
|
|
305
|
+
|
|
306
|
+
viewer.file = {
|
|
307
|
+
name: "corrupt.pdf",
|
|
308
|
+
binaryContent: new Uint8Array([1, 2, 3]),
|
|
309
|
+
};
|
|
310
|
+
await Promise.resolve();
|
|
311
|
+
await Promise.resolve();
|
|
312
|
+
await Promise.resolve();
|
|
313
|
+
|
|
314
|
+
const status = viewer.shadowRoot?.querySelector(".pdf-status");
|
|
315
|
+
expect(status?.textContent).toBe("Failed to render PDF preview.");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("handles RenderingCancelledException gracefully", async () => {
|
|
319
|
+
const renderTask: any = {
|
|
320
|
+
promise: Promise.reject({ name: "RenderingCancelledException" }),
|
|
321
|
+
cancel: jest.fn(),
|
|
322
|
+
};
|
|
323
|
+
const page: any = {
|
|
324
|
+
getViewport: ({ scale }: any) => ({
|
|
325
|
+
width: 200 * scale,
|
|
326
|
+
height: 300 * scale,
|
|
327
|
+
}),
|
|
328
|
+
render: jest.fn(() => renderTask),
|
|
329
|
+
};
|
|
330
|
+
const pdfDocument: any = {
|
|
331
|
+
numPages: 1,
|
|
332
|
+
getPage: jest.fn(async () => page),
|
|
333
|
+
destroy: jest.fn(),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
(getDocumentMock as any).mockImplementationOnce(() => ({
|
|
337
|
+
promise: Promise.resolve(pdfDocument),
|
|
338
|
+
destroy: jest.fn(),
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
const viewer = new ShadowClawPdfViewer();
|
|
342
|
+
document.body.appendChild(viewer);
|
|
343
|
+
|
|
344
|
+
viewer.file = {
|
|
345
|
+
name: "sample.pdf",
|
|
346
|
+
binaryContent: new Uint8Array([1, 2, 3, 4]),
|
|
347
|
+
};
|
|
348
|
+
await Promise.resolve();
|
|
349
|
+
await Promise.resolve();
|
|
350
|
+
await Promise.resolve();
|
|
351
|
+
|
|
352
|
+
// Should not show error status on cancelled rendering
|
|
353
|
+
const status = viewer.shadowRoot?.querySelector(".pdf-status");
|
|
354
|
+
expect(status?.textContent).not.toBe("Failed to render PDF page.");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("cleans up on disconnectedCallback", async () => {
|
|
358
|
+
const viewer = new ShadowClawPdfViewer();
|
|
359
|
+
document.body.appendChild(viewer);
|
|
360
|
+
|
|
361
|
+
viewer.file = {
|
|
362
|
+
name: "sample.pdf",
|
|
363
|
+
binaryContent: new Uint8Array([1, 2, 3, 4]),
|
|
364
|
+
};
|
|
365
|
+
await Promise.resolve();
|
|
366
|
+
await Promise.resolve();
|
|
367
|
+
|
|
368
|
+
viewer.remove();
|
|
369
|
+
expect(viewer._pdfDocument).toBeNull();
|
|
370
|
+
});
|
|
142
371
|
});
|
|
@@ -1103,4 +1103,237 @@ describe("shadow-claw-settings", () => {
|
|
|
1103
1103
|
await el.confirmRestore();
|
|
1104
1104
|
expect(el.replaceConfigEntries).toHaveBeenCalled();
|
|
1105
1105
|
});
|
|
1106
|
+
|
|
1107
|
+
it("handles tab keyboard navigation (arrows, home, end)", async () => {
|
|
1108
|
+
const el = new ShadowClawSettings();
|
|
1109
|
+
document.body.appendChild(el);
|
|
1110
|
+
await el.render();
|
|
1111
|
+
|
|
1112
|
+
const buttons = Array.from(
|
|
1113
|
+
el.shadowRoot?.querySelectorAll<HTMLButtonElement>("[data-tab-target]") ||
|
|
1114
|
+
[],
|
|
1115
|
+
);
|
|
1116
|
+
expect(buttons.length).toBeGreaterThan(1);
|
|
1117
|
+
|
|
1118
|
+
const firstBtn = buttons[0];
|
|
1119
|
+
const lastBtn = buttons[buttons.length - 1];
|
|
1120
|
+
|
|
1121
|
+
// ArrowRight / ArrowDown from first button moves to second
|
|
1122
|
+
const arrowRightEvt = new KeyboardEvent("keydown", {
|
|
1123
|
+
key: "ArrowRight",
|
|
1124
|
+
cancelable: true,
|
|
1125
|
+
});
|
|
1126
|
+
el.handleTabKeydown(arrowRightEvt, firstBtn);
|
|
1127
|
+
expect(arrowRightEvt.defaultPrevented).toBe(true);
|
|
1128
|
+
|
|
1129
|
+
// ArrowLeft / ArrowUp from first button moves to last
|
|
1130
|
+
const arrowLeftEvt = new KeyboardEvent("keydown", {
|
|
1131
|
+
key: "ArrowLeft",
|
|
1132
|
+
cancelable: true,
|
|
1133
|
+
});
|
|
1134
|
+
el.handleTabKeydown(arrowLeftEvt, firstBtn);
|
|
1135
|
+
expect(arrowLeftEvt.defaultPrevented).toBe(true);
|
|
1136
|
+
|
|
1137
|
+
// End key moves to last button
|
|
1138
|
+
const endEvt = new KeyboardEvent("keydown", {
|
|
1139
|
+
key: "End",
|
|
1140
|
+
cancelable: true,
|
|
1141
|
+
});
|
|
1142
|
+
el.handleTabKeydown(endEvt, firstBtn);
|
|
1143
|
+
expect(endEvt.defaultPrevented).toBe(true);
|
|
1144
|
+
|
|
1145
|
+
// Home key moves to first button
|
|
1146
|
+
const homeEvt = new KeyboardEvent("keydown", {
|
|
1147
|
+
key: "Home",
|
|
1148
|
+
cancelable: true,
|
|
1149
|
+
});
|
|
1150
|
+
el.handleTabKeydown(homeEvt, lastBtn);
|
|
1151
|
+
expect(homeEvt.defaultPrevented).toBe(true);
|
|
1152
|
+
|
|
1153
|
+
// Non-navigation key is ignored
|
|
1154
|
+
const tabEvt = new KeyboardEvent("keydown", {
|
|
1155
|
+
key: "Tab",
|
|
1156
|
+
cancelable: true,
|
|
1157
|
+
});
|
|
1158
|
+
el.handleTabKeydown(tabEvt, firstBtn);
|
|
1159
|
+
expect(tabEvt.defaultPrevented).toBe(false);
|
|
1160
|
+
|
|
1161
|
+
document.body.removeChild(el);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
it("handles settings UI action buttons and dialog cancellation", async () => {
|
|
1165
|
+
const el = new ShadowClawSettings();
|
|
1166
|
+
document.body.appendChild(el);
|
|
1167
|
+
await el.render();
|
|
1168
|
+
|
|
1169
|
+
// Backup button opens backup dialog
|
|
1170
|
+
const backupBtn = el.shadowRoot?.querySelector(
|
|
1171
|
+
'[data-action="backup-settings"]',
|
|
1172
|
+
) as HTMLElement;
|
|
1173
|
+
backupBtn?.click();
|
|
1174
|
+
expect(
|
|
1175
|
+
el.shadowRoot
|
|
1176
|
+
?.querySelector(".settings__backup-dialog")
|
|
1177
|
+
?.hasAttribute("open"),
|
|
1178
|
+
).toBe(true);
|
|
1179
|
+
|
|
1180
|
+
// Cancel backup dialog
|
|
1181
|
+
const cancelBackupBtn = el.shadowRoot?.querySelector(
|
|
1182
|
+
'[data-action="cancel-backup-settings"]',
|
|
1183
|
+
) as HTMLElement;
|
|
1184
|
+
cancelBackupBtn?.click();
|
|
1185
|
+
|
|
1186
|
+
// Clear settings button opens clear dialog
|
|
1187
|
+
const clearBtn = el.shadowRoot?.querySelector(
|
|
1188
|
+
'[data-action="clear-settings"]',
|
|
1189
|
+
) as HTMLElement;
|
|
1190
|
+
clearBtn?.click();
|
|
1191
|
+
expect(
|
|
1192
|
+
el.shadowRoot
|
|
1193
|
+
?.querySelector(".settings__clear-dialog")
|
|
1194
|
+
?.hasAttribute("open"),
|
|
1195
|
+
).toBe(true);
|
|
1196
|
+
|
|
1197
|
+
// Cancel clear dialog
|
|
1198
|
+
const cancelClearBtn = el.shadowRoot?.querySelector(
|
|
1199
|
+
'[data-action="cancel-clear-settings"]',
|
|
1200
|
+
) as HTMLElement;
|
|
1201
|
+
cancelClearBtn?.click();
|
|
1202
|
+
|
|
1203
|
+
// Restore button triggers restoreInput.click()
|
|
1204
|
+
const restoreBtn = el.shadowRoot?.querySelector(
|
|
1205
|
+
'[data-action="restore-settings"]',
|
|
1206
|
+
) as HTMLElement;
|
|
1207
|
+
const restoreInput = el.shadowRoot?.querySelector(
|
|
1208
|
+
".settings__restore-input",
|
|
1209
|
+
) as HTMLInputElement;
|
|
1210
|
+
const inputClickSpy = jest.spyOn(restoreInput, "click");
|
|
1211
|
+
restoreBtn?.click();
|
|
1212
|
+
expect(inputClickSpy).toHaveBeenCalled();
|
|
1213
|
+
|
|
1214
|
+
// restoreInput change event opens restore dialog
|
|
1215
|
+
const mockFile = new File(["{}"], "my-backup.json", {
|
|
1216
|
+
type: "application/json",
|
|
1217
|
+
});
|
|
1218
|
+
Object.defineProperty(restoreInput, "files", {
|
|
1219
|
+
value: [mockFile],
|
|
1220
|
+
configurable: true,
|
|
1221
|
+
});
|
|
1222
|
+
restoreInput.dispatchEvent(new Event("change"));
|
|
1223
|
+
expect(el.pendingRestoreFile).toBe(mockFile);
|
|
1224
|
+
|
|
1225
|
+
// Cancel restore dialog clears pendingRestoreFile
|
|
1226
|
+
const cancelRestoreBtn = el.shadowRoot?.querySelector(
|
|
1227
|
+
'[data-action="cancel-restore-settings"]',
|
|
1228
|
+
) as HTMLElement;
|
|
1229
|
+
cancelRestoreBtn?.click();
|
|
1230
|
+
expect(el.pendingRestoreFile).toBeNull();
|
|
1231
|
+
|
|
1232
|
+
// Check chat-split-view-toggle
|
|
1233
|
+
const splitViewToggle = el.shadowRoot?.querySelector(
|
|
1234
|
+
'[data-setting="chat-split-view-toggle"]',
|
|
1235
|
+
) as HTMLInputElement;
|
|
1236
|
+
if (splitViewToggle) {
|
|
1237
|
+
splitViewToggle.checked = true;
|
|
1238
|
+
splitViewToggle.dispatchEvent(new Event("change"));
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Check pages-auto-refresh-input
|
|
1242
|
+
const refreshInput = el.shadowRoot?.querySelector(
|
|
1243
|
+
'[data-setting="pages-auto-refresh-input"]',
|
|
1244
|
+
) as HTMLInputElement;
|
|
1245
|
+
if (refreshInput) {
|
|
1246
|
+
refreshInput.value = "60";
|
|
1247
|
+
refreshInput.dispatchEvent(new Event("change"));
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Save dom allowed iframe hosts
|
|
1251
|
+
const saveHostsBtn = el.shadowRoot?.querySelector(
|
|
1252
|
+
'[data-action="save-dom-allowed-iframe-hosts"]',
|
|
1253
|
+
) as HTMLElement;
|
|
1254
|
+
saveHostsBtn?.click();
|
|
1255
|
+
|
|
1256
|
+
document.body.removeChild(el);
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
it("handles SQLite database operations for getAllConfigEntries and replaceConfigEntries", async () => {
|
|
1260
|
+
const el = new ShadowClawSettings();
|
|
1261
|
+
const mockAll = jest.fn(() => [
|
|
1262
|
+
{ key: "site_name", value: "My Site" },
|
|
1263
|
+
{ key: "count", value: 42 },
|
|
1264
|
+
null,
|
|
1265
|
+
]);
|
|
1266
|
+
const mockRun = jest.fn();
|
|
1267
|
+
const mockPrepare = jest.fn((_sql: string) => ({
|
|
1268
|
+
all: mockAll,
|
|
1269
|
+
run: mockRun,
|
|
1270
|
+
}));
|
|
1271
|
+
|
|
1272
|
+
el.db = {
|
|
1273
|
+
__brand: "ShadowClawSqliteDatabase",
|
|
1274
|
+
db: { prepare: mockPrepare },
|
|
1275
|
+
} as any;
|
|
1276
|
+
|
|
1277
|
+
const entries = await el.getAllConfigEntries();
|
|
1278
|
+
expect(entries).toEqual([
|
|
1279
|
+
{ key: "site_name", value: "My Site" },
|
|
1280
|
+
{ key: "count", value: 42 },
|
|
1281
|
+
]);
|
|
1282
|
+
|
|
1283
|
+
await el.replaceConfigEntries([
|
|
1284
|
+
{ key: "k1", value: "v1" },
|
|
1285
|
+
{ key: "k2", value: { obj: true } },
|
|
1286
|
+
]);
|
|
1287
|
+
expect(mockPrepare).toHaveBeenCalledWith("DELETE FROM config");
|
|
1288
|
+
expect(mockRun).toHaveBeenCalledWith("k1", "v1");
|
|
1289
|
+
expect(mockRun).toHaveBeenCalledWith("k2", JSON.stringify({ obj: true }));
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
it("handles saveAssistantName edge cases", async () => {
|
|
1293
|
+
const el = new ShadowClawSettings();
|
|
1294
|
+
document.body.appendChild(el);
|
|
1295
|
+
await el.render();
|
|
1296
|
+
|
|
1297
|
+
// Empty assistant name gives warning
|
|
1298
|
+
const nameInput = el.shadowRoot?.querySelector(
|
|
1299
|
+
'[data-setting="assistant-name-input"]',
|
|
1300
|
+
) as HTMLInputElement;
|
|
1301
|
+
nameInput.value = " ";
|
|
1302
|
+
await el.saveAssistantName();
|
|
1303
|
+
|
|
1304
|
+
// Non-empty assistant name saves successfully
|
|
1305
|
+
nameInput.value = "ShadowClaw Master";
|
|
1306
|
+
await el.saveAssistantName();
|
|
1307
|
+
|
|
1308
|
+
document.body.removeChild(el);
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
it("handles null db and error edge cases in confirmBackup, confirmClear, confirmRestore", async () => {
|
|
1312
|
+
const el = new ShadowClawSettings();
|
|
1313
|
+
el.db = null;
|
|
1314
|
+
|
|
1315
|
+
// confirmBackup with null db
|
|
1316
|
+
await el.confirmBackup();
|
|
1317
|
+
|
|
1318
|
+
// confirmClear with null db
|
|
1319
|
+
await el.confirmClear();
|
|
1320
|
+
|
|
1321
|
+
// confirmRestore with null db
|
|
1322
|
+
await el.confirmRestore();
|
|
1323
|
+
|
|
1324
|
+
// confirmRestore without file
|
|
1325
|
+
el.db = {} as any;
|
|
1326
|
+
el.pendingRestoreFile = null;
|
|
1327
|
+
await el.confirmRestore();
|
|
1328
|
+
|
|
1329
|
+
// confirmRestore with invalid JSON file
|
|
1330
|
+
el.pendingRestoreFile = {
|
|
1331
|
+
name: "broken.json",
|
|
1332
|
+
text: async () => "invalid-json{",
|
|
1333
|
+
} as any;
|
|
1334
|
+
await el.confirmRestore();
|
|
1335
|
+
|
|
1336
|
+
// activateTab for integrations
|
|
1337
|
+
await el.activateTab("integrations");
|
|
1338
|
+
});
|
|
1106
1339
|
});
|
|
@@ -8,7 +8,9 @@ jest.unstable_mockModule("../../db/saveTask.js", () => ({
|
|
|
8
8
|
saveTask: jest.fn(),
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
|
-
jest.unstable_mockModule("../../core/effect.js", () => ({
|
|
11
|
+
jest.unstable_mockModule("../../core/effect.js", () => ({
|
|
12
|
+
effect: jest.fn(() => () => {}),
|
|
13
|
+
}));
|
|
12
14
|
|
|
13
15
|
jest.unstable_mockModule("../../content/markdown.js", () => ({
|
|
14
16
|
renderMarkdown: jest.fn((x) => x),
|
|
@@ -808,5 +810,225 @@ describe("shadow-claw-tasks", () => {
|
|
|
808
810
|
|
|
809
811
|
expect(list.innerHTML).toContain("Push");
|
|
810
812
|
});
|
|
813
|
+
|
|
814
|
+
it("handles dialog close buttons and backdrop clicks", () => {
|
|
815
|
+
const component = new ShadowClawTasks();
|
|
816
|
+
document.body.appendChild(component);
|
|
817
|
+
const dialog = component.shadowRoot?.querySelector(
|
|
818
|
+
"dialog",
|
|
819
|
+
) as HTMLDialogElement;
|
|
820
|
+
dialog.close = jest.fn();
|
|
821
|
+
|
|
822
|
+
const closeBtn = component.shadowRoot?.querySelector(
|
|
823
|
+
".tasks__dialog-close",
|
|
824
|
+
) as HTMLButtonElement;
|
|
825
|
+
closeBtn?.dispatchEvent(new Event("click"));
|
|
826
|
+
expect(dialog.close).toHaveBeenCalledTimes(1);
|
|
827
|
+
|
|
828
|
+
const cancelBtn = component.shadowRoot?.querySelector(
|
|
829
|
+
".tasks__btn-cancel",
|
|
830
|
+
) as HTMLButtonElement;
|
|
831
|
+
cancelBtn?.dispatchEvent(new Event("click"));
|
|
832
|
+
expect(dialog.close).toHaveBeenCalledTimes(2);
|
|
833
|
+
|
|
834
|
+
// Click on dialog backdrop
|
|
835
|
+
Object.defineProperty(dialog, "target", { value: dialog });
|
|
836
|
+
dialog.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
837
|
+
expect(dialog.close).toHaveBeenCalledTimes(3);
|
|
838
|
+
|
|
839
|
+
document.body.removeChild(component);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it("handles restore button triggering file input click", () => {
|
|
843
|
+
const component = new ShadowClawTasks();
|
|
844
|
+
document.body.appendChild(component);
|
|
845
|
+
const restoreBtn = component.shadowRoot?.querySelector(
|
|
846
|
+
".tasks__restore-btn",
|
|
847
|
+
) as HTMLButtonElement;
|
|
848
|
+
const restoreInput = component.shadowRoot?.querySelector(
|
|
849
|
+
".tasks__hidden-restore",
|
|
850
|
+
) as HTMLInputElement;
|
|
851
|
+
|
|
852
|
+
const clickSpy = jest
|
|
853
|
+
.spyOn(restoreInput, "click")
|
|
854
|
+
.mockImplementation(() => {});
|
|
855
|
+
restoreBtn?.dispatchEvent(new Event("click"));
|
|
856
|
+
expect(clickSpy).toHaveBeenCalled();
|
|
857
|
+
clickSpy.mockRestore();
|
|
858
|
+
|
|
859
|
+
document.body.removeChild(component);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it("handles task item action buttons and drag-and-drop lifecycle", async () => {
|
|
863
|
+
const component = new ShadowClawTasks();
|
|
864
|
+
document.body.appendChild(component);
|
|
865
|
+
const editSpy = jest
|
|
866
|
+
.spyOn(component, "handleEdit")
|
|
867
|
+
.mockImplementation(() => {});
|
|
868
|
+
const deleteSpy = jest
|
|
869
|
+
.spyOn(component, "handleDelete")
|
|
870
|
+
.mockImplementation(async () => {});
|
|
871
|
+
const runSpy = jest
|
|
872
|
+
.spyOn(component, "handleRun")
|
|
873
|
+
.mockImplementation(async () => {});
|
|
874
|
+
const reorderSpy = jest
|
|
875
|
+
.spyOn(component, "handleReorder")
|
|
876
|
+
.mockImplementation(async () => {});
|
|
877
|
+
|
|
878
|
+
(orchestratorStore as any).tasks = [
|
|
879
|
+
{
|
|
880
|
+
id: "task-1",
|
|
881
|
+
name: "Task 1",
|
|
882
|
+
schedule: "0 9 * * *",
|
|
883
|
+
prompt: "Echo 1",
|
|
884
|
+
enabled: true,
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
id: "task-2",
|
|
888
|
+
name: "Task 2",
|
|
889
|
+
schedule: "0 10 * * *",
|
|
890
|
+
prompt: "Echo 2",
|
|
891
|
+
enabled: true,
|
|
892
|
+
},
|
|
893
|
+
];
|
|
894
|
+
|
|
895
|
+
await component.updateTaskList({} as any);
|
|
896
|
+
|
|
897
|
+
const items = component.shadowRoot?.querySelectorAll(".tasks__item");
|
|
898
|
+
expect(items?.length).toBe(2);
|
|
899
|
+
|
|
900
|
+
const firstItem = items![0] as HTMLElement;
|
|
901
|
+
const secondItem = items![1] as HTMLElement;
|
|
902
|
+
|
|
903
|
+
// Edit, delete, run clicks
|
|
904
|
+
const runBtn = firstItem.querySelector(".tasks__run-btn");
|
|
905
|
+
runBtn?.dispatchEvent(new Event("click"));
|
|
906
|
+
expect(runSpy).toHaveBeenCalled();
|
|
907
|
+
|
|
908
|
+
const editBtn = firstItem.querySelector(".tasks__edit-btn");
|
|
909
|
+
editBtn?.dispatchEvent(new Event("click"));
|
|
910
|
+
expect(editSpy).toHaveBeenCalled();
|
|
911
|
+
|
|
912
|
+
const deleteBtn = firstItem.querySelector(".tasks__delete-btn");
|
|
913
|
+
deleteBtn?.dispatchEvent(new Event("click"));
|
|
914
|
+
expect(deleteSpy).toHaveBeenCalled();
|
|
915
|
+
|
|
916
|
+
// Drag and drop events
|
|
917
|
+
const handleEl = firstItem.querySelector(
|
|
918
|
+
".tasks__drag-handle",
|
|
919
|
+
) as HTMLElement;
|
|
920
|
+
const dataTransfer = { setData: jest.fn() };
|
|
921
|
+
const dragStartEvent = new Event("dragstart");
|
|
922
|
+
Object.defineProperty(dragStartEvent, "dataTransfer", {
|
|
923
|
+
value: dataTransfer,
|
|
924
|
+
});
|
|
925
|
+
handleEl?.dispatchEvent(dragStartEvent);
|
|
926
|
+
expect(firstItem.classList.contains("dragging")).toBe(true);
|
|
927
|
+
|
|
928
|
+
// Dragover on second item
|
|
929
|
+
const dragOverEvent = new Event("dragover", { cancelable: true });
|
|
930
|
+
secondItem.dispatchEvent(dragOverEvent);
|
|
931
|
+
expect(secondItem.classList.contains("drag-over")).toBe(true);
|
|
932
|
+
|
|
933
|
+
// Dragleave on second item
|
|
934
|
+
secondItem.dispatchEvent(new Event("dragleave"));
|
|
935
|
+
expect(secondItem.classList.contains("drag-over")).toBe(false);
|
|
936
|
+
|
|
937
|
+
// Dragover again then drop
|
|
938
|
+
secondItem.dispatchEvent(dragOverEvent);
|
|
939
|
+
const dropEvent = new Event("drop", { cancelable: true });
|
|
940
|
+
secondItem.dispatchEvent(dropEvent);
|
|
941
|
+
expect(reorderSpy).toHaveBeenCalledWith("task-1", "task-2");
|
|
942
|
+
|
|
943
|
+
// Dragend
|
|
944
|
+
handleEl?.dispatchEvent(new Event("dragend"));
|
|
945
|
+
expect(firstItem.classList.contains("dragging")).toBe(false);
|
|
946
|
+
|
|
947
|
+
// Touch drag
|
|
948
|
+
const touchStartEvent = new CustomEvent("touchstart", {
|
|
949
|
+
cancelable: true,
|
|
950
|
+
});
|
|
951
|
+
Object.defineProperty(touchStartEvent, "touches", {
|
|
952
|
+
value: [{ identifier: 1, clientX: 10, clientY: 20 }],
|
|
953
|
+
});
|
|
954
|
+
handleEl?.dispatchEvent(touchStartEvent);
|
|
955
|
+
expect(firstItem.classList.contains("dragging")).toBe(true);
|
|
956
|
+
|
|
957
|
+
document.body.removeChild(component);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it("updates auto scroll speed correctly", () => {
|
|
961
|
+
const component = new ShadowClawTasks();
|
|
962
|
+
document.body.appendChild(component);
|
|
963
|
+
const content = component.shadowRoot?.querySelector(
|
|
964
|
+
".tasks__content",
|
|
965
|
+
) as HTMLElement;
|
|
966
|
+
if (content) {
|
|
967
|
+
jest.spyOn(content, "getBoundingClientRect").mockReturnValue({
|
|
968
|
+
top: 100,
|
|
969
|
+
bottom: 500,
|
|
970
|
+
left: 0,
|
|
971
|
+
right: 300,
|
|
972
|
+
height: 400,
|
|
973
|
+
width: 300,
|
|
974
|
+
x: 0,
|
|
975
|
+
y: 100,
|
|
976
|
+
toJSON: () => {},
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Near top (top is 100, clientY = 110)
|
|
981
|
+
(component as any)._updateAutoScrollSpeed(110);
|
|
982
|
+
expect((component as any)._autoScrollSpeed).toBeLessThan(0);
|
|
983
|
+
|
|
984
|
+
// Near bottom (bottom is 500, clientY = 490)
|
|
985
|
+
(component as any)._updateAutoScrollSpeed(490);
|
|
986
|
+
expect((component as any)._autoScrollSpeed).toBeGreaterThan(0);
|
|
987
|
+
|
|
988
|
+
// Middle (clientY = 300)
|
|
989
|
+
(component as any)._updateAutoScrollSpeed(300);
|
|
990
|
+
expect((component as any)._autoScrollSpeed).toBe(0);
|
|
991
|
+
|
|
992
|
+
// Stop auto scroll
|
|
993
|
+
(component as any)._stopAutoScroll();
|
|
994
|
+
expect((component as any)._autoScrollActive).toBe(false);
|
|
995
|
+
expect((component as any)._autoScrollSpeed).toBe(0);
|
|
996
|
+
|
|
997
|
+
document.body.removeChild(component);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it("handles switching task type radios between prompt and tools", async () => {
|
|
1001
|
+
const component = new ShadowClawTasks();
|
|
1002
|
+
document.body.appendChild(component);
|
|
1003
|
+
const typeRadios = component.shadowRoot?.querySelectorAll(
|
|
1004
|
+
"input[name='taskType']",
|
|
1005
|
+
);
|
|
1006
|
+
const toolsRadio = Array.from(typeRadios || []).find(
|
|
1007
|
+
(r: any) => r.value === "tools",
|
|
1008
|
+
) as HTMLInputElement;
|
|
1009
|
+
const promptRadio = Array.from(typeRadios || []).find(
|
|
1010
|
+
(r: any) => r.value === "prompt",
|
|
1011
|
+
) as HTMLInputElement;
|
|
1012
|
+
|
|
1013
|
+
if (toolsRadio) {
|
|
1014
|
+
toolsRadio.checked = true;
|
|
1015
|
+
toolsRadio.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1016
|
+
const promptGroup = component.shadowRoot?.querySelector(
|
|
1017
|
+
".tasks__prompt-group",
|
|
1018
|
+
);
|
|
1019
|
+
expect(promptGroup?.getAttribute("style")).toContain("display: none");
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (promptRadio) {
|
|
1023
|
+
promptRadio.checked = true;
|
|
1024
|
+
promptRadio.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1025
|
+
const toolsGroup = component.shadowRoot?.querySelector(
|
|
1026
|
+
".tasks__tools-group",
|
|
1027
|
+
);
|
|
1028
|
+
expect(toolsGroup?.getAttribute("style")).toContain("display: none");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
document.body.removeChild(component);
|
|
1032
|
+
});
|
|
811
1033
|
});
|
|
812
1034
|
});
|