@wopr-network/platform-ui-core 1.1.6 → 1.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-ui-core",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,233 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import type React from "react";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ // Mock next/navigation
7
+ vi.mock("next/navigation", () => ({
8
+ useRouter: () => ({ push: vi.fn() }),
9
+ }));
10
+
11
+ // Mock framer-motion to avoid animation issues in tests
12
+ vi.mock("framer-motion", () => ({
13
+ motion: {
14
+ div: ({
15
+ children,
16
+ variants: _variants,
17
+ initial: _initial,
18
+ animate: _animate,
19
+ whileHover: _whileHover,
20
+ whileTap: _whileTap,
21
+ transition: _transition,
22
+ style,
23
+ className,
24
+ }: React.PropsWithChildren<{
25
+ variants?: unknown;
26
+ initial?: unknown;
27
+ animate?: unknown;
28
+ whileHover?: unknown;
29
+ whileTap?: unknown;
30
+ transition?: unknown;
31
+ style?: React.CSSProperties;
32
+ className?: string;
33
+ }>) => (
34
+ <div style={style} className={className}>
35
+ {children}
36
+ </div>
37
+ ),
38
+ },
39
+ AnimatePresence: ({ children }: React.PropsWithChildren) => <>{children}</>,
40
+ }));
41
+
42
+ // Mock plugin setup chat hook
43
+ vi.mock("@/hooks/use-plugin-setup-chat", () => ({
44
+ usePluginSetupChat: () => ({
45
+ state: {
46
+ isOpen: false,
47
+ pluginName: "",
48
+ messages: [],
49
+ isConnected: false,
50
+ isTyping: false,
51
+ isComplete: false,
52
+ },
53
+ sendMessage: vi.fn(),
54
+ closeSetup: vi.fn(),
55
+ openSetup: vi.fn(),
56
+ }),
57
+ }));
58
+
59
+ // Mock brand config
60
+ vi.mock("@/lib/brand-config", () => ({
61
+ getBrandConfig: () => ({ productName: "TestProduct", brandName: "TestBrand" }),
62
+ }));
63
+
64
+ // Mock plugin-setup component
65
+ vi.mock("@/components/plugin-setup", () => ({
66
+ SetupChatPanel: () => null,
67
+ }));
68
+
69
+ const mockListMarketplacePlugins = vi.fn();
70
+
71
+ vi.mock("@/lib/marketplace-data", () => ({
72
+ listBots: vi.fn().mockResolvedValue([{ id: "bot-1", name: "Test Bot" }]),
73
+ listMarketplacePlugins: (...args: unknown[]) => mockListMarketplacePlugins(...args),
74
+ listInstalledPlugins: vi.fn().mockResolvedValue([]),
75
+ togglePluginEnabled: vi.fn(),
76
+ formatInstallCount: (n: number) => String(n),
77
+ hasHostedOption: () => false,
78
+ }));
79
+
80
+ import PluginsPage from "@/app/plugins/page";
81
+
82
+ describe("plugins catalog error state", () => {
83
+ beforeEach(() => {
84
+ vi.clearAllMocks();
85
+ });
86
+
87
+ it("shows error message with retry when catalog load fails", async () => {
88
+ mockListMarketplacePlugins.mockRejectedValue(new Error("Network error"));
89
+
90
+ render(<PluginsPage />);
91
+
92
+ // Switch to the Catalog tab
93
+ const catalogTab = await screen.findByRole("tab", { name: /catalog/i });
94
+ await userEvent.click(catalogTab);
95
+
96
+ // Should show the error message
97
+ await waitFor(() => {
98
+ expect(screen.getByText(/CATALOG LOAD FAILED/i)).toBeInTheDocument();
99
+ });
100
+
101
+ // Should show the retry button
102
+ expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
103
+ });
104
+
105
+ it("retries loading when retry button is clicked", async () => {
106
+ const catalogPlugin = {
107
+ id: "plugin-1",
108
+ name: "Test Plugin",
109
+ description: "A test plugin",
110
+ version: "1.0.0",
111
+ color: "#ff0000",
112
+ tags: [],
113
+ capabilities: [],
114
+ installCount: 100,
115
+ author: "Test",
116
+ category: "integration",
117
+ configSchema: [],
118
+ };
119
+
120
+ // First call fails, second succeeds
121
+ mockListMarketplacePlugins
122
+ .mockRejectedValueOnce(new Error("Network error"))
123
+ .mockResolvedValueOnce([catalogPlugin]);
124
+
125
+ render(<PluginsPage />);
126
+
127
+ // Switch to the Catalog tab
128
+ const catalogTab = await screen.findByRole("tab", { name: /catalog/i });
129
+ await userEvent.click(catalogTab);
130
+
131
+ // Wait for error state
132
+ await waitFor(() => {
133
+ expect(screen.getByText(/CATALOG LOAD FAILED/i)).toBeInTheDocument();
134
+ });
135
+
136
+ // Click retry
137
+ const retryButton = screen.getByRole("button", { name: /retry/i });
138
+ await userEvent.click(retryButton);
139
+
140
+ // After retry, the error should be gone and plugin visible in catalog tab (no tab reset)
141
+ await waitFor(() => {
142
+ expect(screen.getByText("Test Plugin")).toBeInTheDocument();
143
+ });
144
+ expect(screen.queryByText(/CATALOG LOAD FAILED/i)).not.toBeInTheDocument();
145
+ });
146
+
147
+ it("shows error banner and retry button when catalog error occurs", async () => {
148
+ // This test validates the corrected rendering condition:
149
+ // The error banner renders whenever catalogError=true, not only when catalogTotal===0.
150
+ // The previous condition (catalogError && catalogTotal === 0) would hide the error
151
+ // if a prior catalog existed — this test verifies the simpler, correct condition.
152
+ mockListMarketplacePlugins.mockRejectedValue(new Error("Network error"));
153
+
154
+ render(<PluginsPage />);
155
+
156
+ const catalogTab = await screen.findByRole("tab", { name: /catalog/i });
157
+ await userEvent.click(catalogTab);
158
+
159
+ // Error banner shown
160
+ await waitFor(() => {
161
+ expect(screen.getByText(/CATALOG LOAD FAILED/i)).toBeInTheDocument();
162
+ });
163
+
164
+ // Retry button visible (not hidden by incorrect catalogTotal condition)
165
+ expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
166
+
167
+ // No empty-state message rendered at the same time
168
+ expect(screen.queryByText(/NO MATCHING PLUGINS FOUND/i)).not.toBeInTheDocument();
169
+ });
170
+
171
+ it("retry does not reset active tab to Installed", async () => {
172
+ const catalogPlugin = {
173
+ id: "plugin-1",
174
+ name: "Test Plugin",
175
+ description: "A test plugin",
176
+ version: "1.0.0",
177
+ color: "#ff0000",
178
+ tags: [],
179
+ capabilities: [],
180
+ installCount: 100,
181
+ author: "Test",
182
+ category: "integration",
183
+ configSchema: [],
184
+ };
185
+
186
+ // First call fails, second succeeds
187
+ mockListMarketplacePlugins
188
+ .mockRejectedValueOnce(new Error("Network error"))
189
+ .mockResolvedValueOnce([catalogPlugin]);
190
+
191
+ render(<PluginsPage />);
192
+
193
+ // Switch to the Catalog tab
194
+ const catalogTab = await screen.findByRole("tab", { name: /catalog/i });
195
+ await userEvent.click(catalogTab);
196
+
197
+ // Wait for error state
198
+ await waitFor(() => {
199
+ expect(screen.getByText(/CATALOG LOAD FAILED/i)).toBeInTheDocument();
200
+ });
201
+
202
+ // Click retry
203
+ const retryButton = screen.getByRole("button", { name: /retry/i });
204
+ await userEvent.click(retryButton);
205
+
206
+ // After retry succeeds, the Catalog tab content should be visible
207
+ // WITHOUT having to re-click the tab (no tab reset)
208
+ await waitFor(() => {
209
+ expect(screen.getByText("Test Plugin")).toBeInTheDocument();
210
+ });
211
+
212
+ // The active tab should still be Catalog — verify the catalog content is visible
213
+ // without re-clicking the tab (i.e., no tab reset occurred)
214
+ expect(screen.queryByText(/CATALOG LOAD FAILED/i)).not.toBeInTheDocument();
215
+ });
216
+
217
+ it("shows empty state (not error) when catalog loads with zero plugins", async () => {
218
+ mockListMarketplacePlugins.mockResolvedValue([]);
219
+
220
+ render(<PluginsPage />);
221
+
222
+ // Switch to the Catalog tab
223
+ const catalogTab = await screen.findByRole("tab", { name: /catalog/i });
224
+ await userEvent.click(catalogTab);
225
+
226
+ // Should show the normal empty state, not the error state
227
+ await waitFor(() => {
228
+ expect(screen.getByText(/NO MATCHING PLUGINS FOUND/i)).toBeInTheDocument();
229
+ });
230
+
231
+ expect(screen.queryByText(/CATALOG LOAD FAILED/i)).not.toBeInTheDocument();
232
+ });
233
+ });
@@ -82,6 +82,8 @@ export default function PluginsPage() {
82
82
  const [toggling, setToggling] = useState<string | null>(null);
83
83
  const togglingRef = useRef<string | null>(null);
84
84
  const [toggleError, setToggleError] = useState<string | null>(null);
85
+ const [catalogError, setCatalogError] = useState(false);
86
+ const [catalogLoading, setCatalogLoading] = useState(false);
85
87
  const [installedPage, setInstalledPage] = useState(1);
86
88
  const [catalogPage, setCatalogPage] = useState(1);
87
89
 
@@ -114,15 +116,24 @@ export default function PluginsPage() {
114
116
  });
115
117
  }, []);
116
118
 
117
- const loadCatalog = useCallback(async () => {
118
- setLoading(true);
119
+ const loadCatalog = useCallback(async (isRetry = false) => {
120
+ setCatalogError(false);
121
+ if (isRetry) {
122
+ setCatalogLoading(true);
123
+ } else {
124
+ setLoading(true);
125
+ }
119
126
  try {
120
127
  const data = await listMarketplacePlugins();
121
128
  setCatalog(data);
122
129
  } catch {
123
- // Keep previous catalog on error
130
+ setCatalogError(true);
124
131
  } finally {
125
- setLoading(false);
132
+ if (isRetry) {
133
+ setCatalogLoading(false);
134
+ } else {
135
+ setLoading(false);
136
+ }
126
137
  }
127
138
  }, []);
128
139
 
@@ -446,7 +457,43 @@ export default function PluginsPage() {
446
457
  className="max-w-sm bg-black/50 border-terminal/30 placeholder:text-terminal/30 focus-visible:border-terminal focus-visible:ring-terminal/20"
447
458
  />
448
459
 
449
- {catalogTotal === 0 ? (
460
+ {catalogLoading ? (
461
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
462
+ {Array.from({ length: 6 }, (_, n) => `csk-${n}`).map((skId) => (
463
+ <Card key={skId}>
464
+ <CardHeader>
465
+ <div className="flex items-start gap-3">
466
+ <Skeleton className="h-10 w-10 rounded-lg" />
467
+ <div className="flex-1 space-y-2">
468
+ <Skeleton className="h-5 w-28" />
469
+ <Skeleton className="h-4 w-full" />
470
+ </div>
471
+ </div>
472
+ </CardHeader>
473
+ <CardContent>
474
+ <div className="flex items-center justify-between">
475
+ <Skeleton className="h-3 w-24" />
476
+ <Skeleton className="h-5 w-10 rounded-full" />
477
+ </div>
478
+ </CardContent>
479
+ </Card>
480
+ ))}
481
+ </div>
482
+ ) : catalogError ? (
483
+ <div className="flex h-40 flex-col items-center justify-center gap-3 rounded-sm border border-dashed border-red-500/25 bg-red-500/5">
484
+ <p className="font-mono text-sm text-red-500">
485
+ &gt; CATALOG LOAD FAILED. CHECK CONNECTION AND RETRY.
486
+ </p>
487
+ <Button
488
+ variant="outline"
489
+ size="sm"
490
+ className="border-red-500/30 text-red-500 hover:bg-red-500/10"
491
+ onClick={() => loadCatalog(true)}
492
+ >
493
+ Retry
494
+ </Button>
495
+ </div>
496
+ ) : catalogTotal === 0 ? (
450
497
  <div className="flex h-40 items-center justify-center rounded-sm border border-dashed border-terminal/20">
451
498
  <p className="font-mono text-sm text-terminal/60">&gt; NO MATCHING PLUGINS FOUND.</p>
452
499
  </div>