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
|
@@ -71,7 +71,16 @@ jest.unstable_mockModule("../../storage/moveGroupEntry.js", () => ({
|
|
|
71
71
|
}));
|
|
72
72
|
|
|
73
73
|
jest.unstable_mockModule("../../core/effect.js", () => ({
|
|
74
|
-
effect: jest.fn((
|
|
74
|
+
effect: jest.fn((cb: any) => {
|
|
75
|
+
try {
|
|
76
|
+
const cleanup = cb();
|
|
77
|
+
return () => {
|
|
78
|
+
if (typeof cleanup === "function") cleanup();
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
return () => {};
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
75
84
|
}));
|
|
76
85
|
|
|
77
86
|
jest.unstable_mockModule("../../ui/toast.js", () => ({
|
|
@@ -127,6 +136,12 @@ const { copyGroupEntry } = await import("../../storage/copyGroupEntry.js");
|
|
|
127
136
|
const { moveGroupEntry } = await import("../../storage/moveGroupEntry.js");
|
|
128
137
|
const { showSuccess, showWarning, showError } =
|
|
129
138
|
await import("../../ui/toast.js");
|
|
139
|
+
const { downloadGroupDirectoryAsZip } =
|
|
140
|
+
await import("../../storage/downloadGroupDirectoryAsZip.js");
|
|
141
|
+
const { downloadGroupFile } =
|
|
142
|
+
await import("../../storage/downloadGroupFile.js");
|
|
143
|
+
const { deleteGroupDirectory } =
|
|
144
|
+
await import("../../storage/deleteGroupDirectory.js");
|
|
130
145
|
const { orchestratorStore } = await import("../../stores/orchestrator.js");
|
|
131
146
|
const { filesUiStore } = await import("../../stores/files-ui.js");
|
|
132
147
|
|
|
@@ -1607,4 +1622,386 @@ describe("shadow-claw-files", () => {
|
|
|
1607
1622
|
document.body.removeChild(component);
|
|
1608
1623
|
});
|
|
1609
1624
|
});
|
|
1625
|
+
|
|
1626
|
+
describe("drag and drop handling", () => {
|
|
1627
|
+
it("handles drag enter, over, leave, and drop events", async () => {
|
|
1628
|
+
const component = new ShadowClawFiles();
|
|
1629
|
+
document.body.appendChild(component);
|
|
1630
|
+
await component.render();
|
|
1631
|
+
|
|
1632
|
+
const content = component.shadowRoot?.querySelector(
|
|
1633
|
+
".files__content",
|
|
1634
|
+
) as HTMLElement;
|
|
1635
|
+
|
|
1636
|
+
// Drag enter without files does nothing
|
|
1637
|
+
const noFilesDragEvent = {
|
|
1638
|
+
dataTransfer: { types: ["text/plain"] },
|
|
1639
|
+
preventDefault: jest.fn(),
|
|
1640
|
+
} as any;
|
|
1641
|
+
component.handleDragEnter(noFilesDragEvent, content);
|
|
1642
|
+
expect(filesUiStore.isDragActive).toBe(false);
|
|
1643
|
+
|
|
1644
|
+
// Drag enter with Files
|
|
1645
|
+
const filesDragEvent = {
|
|
1646
|
+
dataTransfer: { types: ["Files"] },
|
|
1647
|
+
preventDefault: jest.fn(),
|
|
1648
|
+
} as any;
|
|
1649
|
+
component.handleDragEnter(filesDragEvent, content);
|
|
1650
|
+
expect(filesUiStore.isDragActive).toBe(true);
|
|
1651
|
+
|
|
1652
|
+
// Drag over sets dropEffect
|
|
1653
|
+
const dragOverEvent = {
|
|
1654
|
+
dataTransfer: { types: ["Files"], dropEffect: "" },
|
|
1655
|
+
preventDefault: jest.fn(),
|
|
1656
|
+
} as any;
|
|
1657
|
+
component.handleDragOver(dragOverEvent, content);
|
|
1658
|
+
expect(dragOverEvent.dataTransfer.dropEffect).toBe("copy");
|
|
1659
|
+
|
|
1660
|
+
// Drag leave outside content
|
|
1661
|
+
const dragLeaveEvent = {
|
|
1662
|
+
relatedTarget: document.body,
|
|
1663
|
+
} as any;
|
|
1664
|
+
component.handleDragLeave(dragLeaveEvent, content);
|
|
1665
|
+
expect(filesUiStore.isDragActive).toBe(false);
|
|
1666
|
+
|
|
1667
|
+
// Handle drop with files
|
|
1668
|
+
const dropUploadSpy = jest
|
|
1669
|
+
.spyOn(component, "uploadFileList")
|
|
1670
|
+
.mockResolvedValue(undefined as any);
|
|
1671
|
+
const mockFile = new File(["test content"], "dropped.txt", {
|
|
1672
|
+
type: "text/plain",
|
|
1673
|
+
});
|
|
1674
|
+
const dropEvent = {
|
|
1675
|
+
dataTransfer: { types: ["Files"], files: [mockFile] },
|
|
1676
|
+
preventDefault: jest.fn(),
|
|
1677
|
+
} as any;
|
|
1678
|
+
await component.handleDrop(dropEvent, {} as any, content);
|
|
1679
|
+
expect(dropUploadSpy).toHaveBeenCalledWith({}, [mockFile]);
|
|
1680
|
+
|
|
1681
|
+
document.body.removeChild(component);
|
|
1682
|
+
});
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
describe("sync host to VM handling", () => {
|
|
1686
|
+
it("handles sync host to VM success and error", async () => {
|
|
1687
|
+
const component = new ShadowClawFiles();
|
|
1688
|
+
document.body.appendChild(component);
|
|
1689
|
+
await component.render();
|
|
1690
|
+
|
|
1691
|
+
(orchestratorStore as any).syncHostWorkspaceToVM = jest.fn();
|
|
1692
|
+
component.handleSyncHostToVM();
|
|
1693
|
+
expect(orchestratorStore.syncHostWorkspaceToVM).toHaveBeenCalled();
|
|
1694
|
+
expect(showSuccess).toHaveBeenCalledWith(
|
|
1695
|
+
expect.stringContaining("Requested host"),
|
|
1696
|
+
expect.any(Number),
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
(orchestratorStore as any).syncHostWorkspaceToVM = jest.fn(() => {
|
|
1700
|
+
throw new Error("VM not running");
|
|
1701
|
+
});
|
|
1702
|
+
component.handleSyncHostToVM();
|
|
1703
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1704
|
+
expect.stringContaining(
|
|
1705
|
+
"Failed to request host → VM sync: VM not running",
|
|
1706
|
+
),
|
|
1707
|
+
expect.any(Number),
|
|
1708
|
+
);
|
|
1709
|
+
|
|
1710
|
+
document.body.removeChild(component);
|
|
1711
|
+
});
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
describe("dialog cancellation and cancel buttons", () => {
|
|
1715
|
+
it("handles cancel events and cancel buttons respecting busy flags", async () => {
|
|
1716
|
+
const component = new ShadowClawFiles();
|
|
1717
|
+
document.body.appendChild(component);
|
|
1718
|
+
await component.render();
|
|
1719
|
+
|
|
1720
|
+
const newDialog = component.shadowRoot?.querySelector(
|
|
1721
|
+
".files__new-dialog",
|
|
1722
|
+
) as HTMLDialogElement;
|
|
1723
|
+
const newCancelBtn = component.shadowRoot?.querySelector(
|
|
1724
|
+
".files__new-cancel",
|
|
1725
|
+
) as HTMLButtonElement;
|
|
1726
|
+
const renameDialog = component.shadowRoot?.querySelector(
|
|
1727
|
+
".files__rename-dialog",
|
|
1728
|
+
) as HTMLDialogElement;
|
|
1729
|
+
const renameCancelBtn = component.shadowRoot?.querySelector(
|
|
1730
|
+
".files__rename-cancel",
|
|
1731
|
+
) as HTMLButtonElement;
|
|
1732
|
+
const pasteDialog = component.shadowRoot?.querySelector(
|
|
1733
|
+
".files__paste-dialog",
|
|
1734
|
+
) as HTMLDialogElement;
|
|
1735
|
+
const pasteCancelBtn = component.shadowRoot?.querySelector(
|
|
1736
|
+
".files__paste-cancel",
|
|
1737
|
+
) as HTMLButtonElement;
|
|
1738
|
+
|
|
1739
|
+
// newDialog cancel event when busy vs not busy
|
|
1740
|
+
const cancelEvt1 = new Event("cancel", { cancelable: true });
|
|
1741
|
+
(component as any)._isCreatingNewItem = true;
|
|
1742
|
+
newDialog.dispatchEvent(cancelEvt1);
|
|
1743
|
+
expect(cancelEvt1.defaultPrevented).toBe(true);
|
|
1744
|
+
|
|
1745
|
+
(component as any)._isCreatingNewItem = true;
|
|
1746
|
+
newCancelBtn.click(); // does not close
|
|
1747
|
+
|
|
1748
|
+
(component as any)._isCreatingNewItem = false;
|
|
1749
|
+
newCancelBtn.click(); // closes
|
|
1750
|
+
|
|
1751
|
+
// renameDialog cancel event when busy vs not busy
|
|
1752
|
+
const cancelEvt2 = new Event("cancel", { cancelable: true });
|
|
1753
|
+
(component as any)._isRenamingEntry = true;
|
|
1754
|
+
renameDialog.dispatchEvent(cancelEvt2);
|
|
1755
|
+
expect(cancelEvt2.defaultPrevented).toBe(true);
|
|
1756
|
+
|
|
1757
|
+
(component as any)._isRenamingEntry = true;
|
|
1758
|
+
renameCancelBtn.click();
|
|
1759
|
+
|
|
1760
|
+
(component as any)._isRenamingEntry = false;
|
|
1761
|
+
renameCancelBtn.click();
|
|
1762
|
+
|
|
1763
|
+
// pasteDialog cancel event when busy vs not busy
|
|
1764
|
+
const cancelEvt3 = new Event("cancel", { cancelable: true });
|
|
1765
|
+
(component as any)._isPastingEntry = true;
|
|
1766
|
+
pasteDialog.dispatchEvent(cancelEvt3);
|
|
1767
|
+
expect(cancelEvt3.defaultPrevented).toBe(true);
|
|
1768
|
+
|
|
1769
|
+
(component as any)._isPastingEntry = true;
|
|
1770
|
+
pasteCancelBtn.click();
|
|
1771
|
+
|
|
1772
|
+
(component as any)._isPastingEntry = false;
|
|
1773
|
+
pasteCancelBtn.click();
|
|
1774
|
+
|
|
1775
|
+
document.body.removeChild(component);
|
|
1776
|
+
});
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
describe("paste dialog edge cases and validations", () => {
|
|
1780
|
+
it("handles openPasteDialog edge cases", async () => {
|
|
1781
|
+
const component = new ShadowClawFiles();
|
|
1782
|
+
document.body.appendChild(component);
|
|
1783
|
+
await component.render();
|
|
1784
|
+
|
|
1785
|
+
// No clipboard
|
|
1786
|
+
filesUiStore.clearClipboard();
|
|
1787
|
+
component.openPasteDialog();
|
|
1788
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1789
|
+
"Nothing in clipboard to paste",
|
|
1790
|
+
expect.any(Number),
|
|
1791
|
+
);
|
|
1792
|
+
|
|
1793
|
+
// Invalid folder paste target
|
|
1794
|
+
(orchestratorStore as any).currentPath = "my-folder/sub";
|
|
1795
|
+
filesUiStore.setClipboard("my-folder/", "copy", true, "default");
|
|
1796
|
+
component.openPasteDialog();
|
|
1797
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1798
|
+
expect.stringContaining("Cannot paste a folder into itself"),
|
|
1799
|
+
expect.any(Number),
|
|
1800
|
+
);
|
|
1801
|
+
|
|
1802
|
+
// Already exists case in openPasteDialog
|
|
1803
|
+
(orchestratorStore as any).currentPath = ".";
|
|
1804
|
+
(orchestratorStore as any).files = ["file.txt"];
|
|
1805
|
+
filesUiStore.setClipboard("file.txt", "copy", false, "default");
|
|
1806
|
+
component.openPasteDialog();
|
|
1807
|
+
const msg = component.shadowRoot?.querySelector(
|
|
1808
|
+
".files__paste-message",
|
|
1809
|
+
)?.textContent;
|
|
1810
|
+
expect(msg).toContain("already exists");
|
|
1811
|
+
|
|
1812
|
+
document.body.removeChild(component);
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
it("handles handlePasteEntry validation and overwrite branches", async () => {
|
|
1816
|
+
const component = new ShadowClawFiles();
|
|
1817
|
+
document.body.appendChild(component);
|
|
1818
|
+
await component.render();
|
|
1819
|
+
|
|
1820
|
+
const db = {} as any;
|
|
1821
|
+
|
|
1822
|
+
// When already pasting
|
|
1823
|
+
(component as any)._isPastingEntry = true;
|
|
1824
|
+
await component.handlePasteEntry(db, false);
|
|
1825
|
+
(component as any)._isPastingEntry = false;
|
|
1826
|
+
|
|
1827
|
+
// When clipboard is null
|
|
1828
|
+
filesUiStore.clearClipboard();
|
|
1829
|
+
await component.handlePasteEntry(db, false);
|
|
1830
|
+
|
|
1831
|
+
// Invalid folder paste target
|
|
1832
|
+
(orchestratorStore as any).currentPath = "target-folder/sub";
|
|
1833
|
+
filesUiStore.setClipboard("target-folder/", "copy", true, "default");
|
|
1834
|
+
await component.handlePasteEntry(db, false);
|
|
1835
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1836
|
+
expect.stringContaining("Cannot paste a folder into itself"),
|
|
1837
|
+
expect.any(Number),
|
|
1838
|
+
);
|
|
1839
|
+
|
|
1840
|
+
// Rename input empty
|
|
1841
|
+
(orchestratorStore as any).currentPath = ".";
|
|
1842
|
+
(orchestratorStore as any).files = ["test.txt"];
|
|
1843
|
+
filesUiStore.setClipboard("test.txt", "copy", false, "default");
|
|
1844
|
+
component.openPasteDialog();
|
|
1845
|
+
const renameInput = component.shadowRoot?.querySelector(
|
|
1846
|
+
".files__paste-rename-input",
|
|
1847
|
+
) as HTMLInputElement;
|
|
1848
|
+
renameInput.value = " ";
|
|
1849
|
+
await component.handlePasteEntry(db, false);
|
|
1850
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1851
|
+
"Please enter a name",
|
|
1852
|
+
expect.any(Number),
|
|
1853
|
+
);
|
|
1854
|
+
|
|
1855
|
+
// Rename input contains slash
|
|
1856
|
+
renameInput.value = "invalid/name";
|
|
1857
|
+
await component.handlePasteEntry(db, false);
|
|
1858
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1859
|
+
"Use only a name, not a path",
|
|
1860
|
+
expect.any(Number),
|
|
1861
|
+
);
|
|
1862
|
+
|
|
1863
|
+
// Target already exists and not overwrite
|
|
1864
|
+
renameInput.value = "test.txt";
|
|
1865
|
+
await component.handlePasteEntry(db, false);
|
|
1866
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
1867
|
+
expect.stringContaining("already exists"),
|
|
1868
|
+
expect.any(Number),
|
|
1869
|
+
);
|
|
1870
|
+
|
|
1871
|
+
// Overwrite folder
|
|
1872
|
+
filesUiStore.setClipboard("old-dir/", "copy", true, "default");
|
|
1873
|
+
(orchestratorStore as any).files = ["old-dir/"];
|
|
1874
|
+
(copyGroupEntry as any).mockResolvedValue(undefined);
|
|
1875
|
+
await component.handlePasteEntry(db, true);
|
|
1876
|
+
expect(deleteGroupDirectory).toHaveBeenCalled();
|
|
1877
|
+
|
|
1878
|
+
// Overwrite file
|
|
1879
|
+
filesUiStore.setClipboard("old-file.txt", "copy", false, "default");
|
|
1880
|
+
(orchestratorStore as any).files = ["old-file.txt"];
|
|
1881
|
+
await component.handlePasteEntry(db, true);
|
|
1882
|
+
expect(deleteGroupFile).toHaveBeenCalled();
|
|
1883
|
+
|
|
1884
|
+
document.body.removeChild(component);
|
|
1885
|
+
});
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
describe("download buttons in file and directory items", () => {
|
|
1889
|
+
it("handles file and directory download button clicks", async () => {
|
|
1890
|
+
const component = new ShadowClawFiles();
|
|
1891
|
+
document.body.appendChild(component);
|
|
1892
|
+
await component.render();
|
|
1893
|
+
|
|
1894
|
+
(orchestratorStore as any).activeGroupId = "default";
|
|
1895
|
+
(orchestratorStore as any).currentPath = ".";
|
|
1896
|
+
(orchestratorStore as any).files = ["doc.txt", "assets/"];
|
|
1897
|
+
component.updateFileList({} as any);
|
|
1898
|
+
|
|
1899
|
+
const items = component.shadowRoot?.querySelectorAll(".files__item");
|
|
1900
|
+
const fileDownloadBtn = items?.[0]?.querySelector(
|
|
1901
|
+
".files__download",
|
|
1902
|
+
) as HTMLButtonElement;
|
|
1903
|
+
const dirDownloadBtn = items?.[1]?.querySelector(
|
|
1904
|
+
".files__download",
|
|
1905
|
+
) as HTMLButtonElement;
|
|
1906
|
+
|
|
1907
|
+
// File download success
|
|
1908
|
+
(downloadGroupFile as any).mockResolvedValue(undefined);
|
|
1909
|
+
fileDownloadBtn.click();
|
|
1910
|
+
await Promise.resolve();
|
|
1911
|
+
await Promise.resolve();
|
|
1912
|
+
expect(downloadGroupFile).toHaveBeenCalledWith({}, "default", "doc.txt");
|
|
1913
|
+
expect(showSuccess).toHaveBeenCalledWith(
|
|
1914
|
+
"Downloaded file: doc.txt",
|
|
1915
|
+
2500,
|
|
1916
|
+
);
|
|
1917
|
+
|
|
1918
|
+
// Dir download success
|
|
1919
|
+
(downloadGroupDirectoryAsZip as any).mockResolvedValue(undefined);
|
|
1920
|
+
dirDownloadBtn.click();
|
|
1921
|
+
await Promise.resolve();
|
|
1922
|
+
await Promise.resolve();
|
|
1923
|
+
expect(downloadGroupDirectoryAsZip).toHaveBeenCalledWith(
|
|
1924
|
+
{},
|
|
1925
|
+
"default",
|
|
1926
|
+
"assets/",
|
|
1927
|
+
);
|
|
1928
|
+
expect(showSuccess).toHaveBeenCalledWith(
|
|
1929
|
+
"Downloaded folder: assets",
|
|
1930
|
+
2500,
|
|
1931
|
+
);
|
|
1932
|
+
|
|
1933
|
+
// Download failure
|
|
1934
|
+
(downloadGroupFile as any).mockRejectedValueOnce(
|
|
1935
|
+
new Error("Network fail"),
|
|
1936
|
+
);
|
|
1937
|
+
fileDownloadBtn.click();
|
|
1938
|
+
await Promise.resolve();
|
|
1939
|
+
await Promise.resolve();
|
|
1940
|
+
expect(showError).toHaveBeenCalledWith(
|
|
1941
|
+
"Failed to download doc.txt: Network fail",
|
|
1942
|
+
6000,
|
|
1943
|
+
);
|
|
1944
|
+
|
|
1945
|
+
document.body.removeChild(component);
|
|
1946
|
+
});
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
describe("restore and form events", () => {
|
|
1950
|
+
it("handles restore button and form submission events", async () => {
|
|
1951
|
+
const component = new ShadowClawFiles();
|
|
1952
|
+
document.body.appendChild(component);
|
|
1953
|
+
await component.render();
|
|
1954
|
+
|
|
1955
|
+
const restoreBtn = component.shadowRoot?.querySelector(
|
|
1956
|
+
".files__restore-btn",
|
|
1957
|
+
) as HTMLButtonElement;
|
|
1958
|
+
const restoreInput = component.shadowRoot?.querySelector(
|
|
1959
|
+
".files__hidden-restore",
|
|
1960
|
+
) as HTMLInputElement;
|
|
1961
|
+
const inputClickSpy = jest.spyOn(restoreInput, "click");
|
|
1962
|
+
restoreBtn.click();
|
|
1963
|
+
expect(inputClickSpy).toHaveBeenCalled();
|
|
1964
|
+
|
|
1965
|
+
const handleRestoreSpy = jest
|
|
1966
|
+
.spyOn(component, "handleRestore")
|
|
1967
|
+
.mockImplementation(() => {});
|
|
1968
|
+
restoreInput.dispatchEvent(new Event("change"));
|
|
1969
|
+
expect(handleRestoreSpy).toHaveBeenCalled();
|
|
1970
|
+
|
|
1971
|
+
// Form submit handlers
|
|
1972
|
+
const newForm = component.shadowRoot?.querySelector(".files__new-form");
|
|
1973
|
+
const handleCreateSpy = jest
|
|
1974
|
+
.spyOn(component, "handleCreateNewFile")
|
|
1975
|
+
.mockResolvedValue();
|
|
1976
|
+
newForm?.dispatchEvent(new Event("submit", { cancelable: true }));
|
|
1977
|
+
expect(handleCreateSpy).toHaveBeenCalled();
|
|
1978
|
+
|
|
1979
|
+
const renameForm = component.shadowRoot?.querySelector(
|
|
1980
|
+
".files__rename-form",
|
|
1981
|
+
);
|
|
1982
|
+
const handleRenameSpy = jest
|
|
1983
|
+
.spyOn(component, "handleRenameEntry")
|
|
1984
|
+
.mockResolvedValue();
|
|
1985
|
+
renameForm?.dispatchEvent(new Event("submit", { cancelable: true }));
|
|
1986
|
+
expect(handleRenameSpy).toHaveBeenCalled();
|
|
1987
|
+
|
|
1988
|
+
const pasteForm =
|
|
1989
|
+
component.shadowRoot?.querySelector(".files__paste-form");
|
|
1990
|
+
const handlePasteSpy = jest
|
|
1991
|
+
.spyOn(component, "handlePasteEntry")
|
|
1992
|
+
.mockResolvedValue();
|
|
1993
|
+
pasteForm?.dispatchEvent(new Event("submit", { cancelable: true }));
|
|
1994
|
+
expect(handlePasteSpy).toHaveBeenCalledWith(expect.anything(), false);
|
|
1995
|
+
|
|
1996
|
+
const pasteOverwriteBtn = component.shadowRoot?.querySelector(
|
|
1997
|
+
".files__paste-overwrite",
|
|
1998
|
+
) as HTMLButtonElement;
|
|
1999
|
+
pasteOverwriteBtn.dispatchEvent(
|
|
2000
|
+
new MouseEvent("click", { cancelable: true }),
|
|
2001
|
+
);
|
|
2002
|
+
expect(handlePasteSpy).toHaveBeenCalledWith(expect.anything(), true);
|
|
2003
|
+
|
|
2004
|
+
document.body.removeChild(component);
|
|
2005
|
+
});
|
|
2006
|
+
});
|
|
1610
2007
|
});
|
|
@@ -1814,5 +1814,90 @@ describe("shadow-claw-pages", () => {
|
|
|
1814
1814
|
|
|
1815
1815
|
(orchestratorStore.isPageAllowed as jest.Mock).mockReturnValue(true);
|
|
1816
1816
|
});
|
|
1817
|
+
|
|
1818
|
+
it("updates header when frontmatter title is present in renderPageList", async () => {
|
|
1819
|
+
const component = new ShadowClawPages();
|
|
1820
|
+
await component.connectedCallback();
|
|
1821
|
+
const root = component.shadowRoot;
|
|
1822
|
+
if (!root) return;
|
|
1823
|
+
|
|
1824
|
+
component.selectedPage = { groupId: "group-1", path: "docs/page.md" };
|
|
1825
|
+
component.pageFrontmatter.set({ title: "Amazing Guide" } as any);
|
|
1826
|
+
component.renderPageList(
|
|
1827
|
+
[{ groupId: "group-1", path: "docs/page.md" }],
|
|
1828
|
+
[],
|
|
1829
|
+
);
|
|
1830
|
+
|
|
1831
|
+
const header = root.querySelector("shadow-claw-page-header");
|
|
1832
|
+
expect(header?.getAttribute("title")).toBe("Amazing Guide");
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
it("handles drag and drop events on page rows", async () => {
|
|
1836
|
+
const component = new ShadowClawPages();
|
|
1837
|
+
await component.connectedCallback();
|
|
1838
|
+
component.db = {} as any;
|
|
1839
|
+
const root = component.shadowRoot;
|
|
1840
|
+
if (!root) return;
|
|
1841
|
+
|
|
1842
|
+
const pages = [
|
|
1843
|
+
{ groupId: "group-1", path: "docs/1.md" },
|
|
1844
|
+
{ groupId: "group-1", path: "docs/2.md" },
|
|
1845
|
+
];
|
|
1846
|
+
component.renderPageList(pages, []);
|
|
1847
|
+
|
|
1848
|
+
const rows = root.querySelectorAll(".pages__list-item");
|
|
1849
|
+
expect(rows.length).toBeGreaterThanOrEqual(2);
|
|
1850
|
+
|
|
1851
|
+
const dragHandle0 = rows[0].querySelector(".pages__drag-handle");
|
|
1852
|
+
const dragEvent = new Event("dragstart", { bubbles: true }) as any;
|
|
1853
|
+
dragEvent.dataTransfer = { effectAllowed: "" };
|
|
1854
|
+
dragHandle0?.dispatchEvent(dragEvent);
|
|
1855
|
+
expect((component as any).draggedPageIndex).toBe(0);
|
|
1856
|
+
|
|
1857
|
+
const dragOverEvent = new Event("dragover", { bubbles: true }) as any;
|
|
1858
|
+
dragOverEvent.preventDefault = jest.fn();
|
|
1859
|
+
rows[1].dispatchEvent(dragOverEvent);
|
|
1860
|
+
expect(rows[1].classList.contains("drag-over")).toBe(true);
|
|
1861
|
+
|
|
1862
|
+
const dragLeaveEvent = new Event("dragleave", { bubbles: true });
|
|
1863
|
+
rows[1].dispatchEvent(dragLeaveEvent);
|
|
1864
|
+
expect(rows[1].classList.contains("drag-over")).toBe(false);
|
|
1865
|
+
|
|
1866
|
+
const dropEvent = new Event("drop", { bubbles: true }) as any;
|
|
1867
|
+
dropEvent.preventDefault = jest.fn();
|
|
1868
|
+
rows[1].dispatchEvent(dropEvent);
|
|
1869
|
+
|
|
1870
|
+
const dragEndEvent = new Event("dragend", { bubbles: true });
|
|
1871
|
+
rows[0].dispatchEvent(dragEndEvent);
|
|
1872
|
+
expect((component as any).draggedPageIndex).toBeNull();
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
it("handles setupAutoRefreshTimer lifecycle", async () => {
|
|
1876
|
+
const component = new ShadowClawPages();
|
|
1877
|
+
(component as any).autoRefreshTimer = setInterval(() => {}, 10000);
|
|
1878
|
+
component.db = null as any;
|
|
1879
|
+
|
|
1880
|
+
await component.setupAutoRefreshTimer();
|
|
1881
|
+
expect((component as any).autoRefreshTimer).toBeNull();
|
|
1882
|
+
});
|
|
1883
|
+
|
|
1884
|
+
it("handles resolveRelativeImagesInHtml with empty and present html", async () => {
|
|
1885
|
+
const component = new ShadowClawPages();
|
|
1886
|
+
component.db = {} as any;
|
|
1887
|
+
|
|
1888
|
+
const emptyRes = await component.resolveRelativeImagesInHtml(
|
|
1889
|
+
"",
|
|
1890
|
+
"doc.html",
|
|
1891
|
+
"group-1",
|
|
1892
|
+
);
|
|
1893
|
+
expect(emptyRes).toBe("");
|
|
1894
|
+
|
|
1895
|
+
const htmlRes = await component.resolveRelativeImagesInHtml(
|
|
1896
|
+
"<p>Hello</p>",
|
|
1897
|
+
"doc.html",
|
|
1898
|
+
"group-1",
|
|
1899
|
+
);
|
|
1900
|
+
expect(htmlRes).toContain("<p>Hello</p>");
|
|
1901
|
+
});
|
|
1817
1902
|
});
|
|
1818
1903
|
});
|
|
@@ -1546,7 +1546,8 @@ export class ShadowClawPages extends ShadowClawElement {
|
|
|
1546
1546
|
}
|
|
1547
1547
|
|
|
1548
1548
|
const root = this.shadowRoot;
|
|
1549
|
-
const rendered = root?.querySelector("
|
|
1549
|
+
const rendered = (root?.querySelector("[data-pages-rendered]") ||
|
|
1550
|
+
root?.querySelector("#rendered")) as HTMLElement;
|
|
1550
1551
|
if (!root || !rendered) {
|
|
1551
1552
|
return;
|
|
1552
1553
|
}
|