@thatopen/services 0.0.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.
Files changed (80) hide show
  1. package/CONTEXT.md +258 -0
  2. package/README.md +285 -0
  3. package/dist/built-in/index.d.ts +723 -0
  4. package/dist/cli/commands/create-tests.d.ts +3 -0
  5. package/dist/cli/commands/create.d.ts +3 -0
  6. package/dist/cli/commands/local-server.d.ts +3 -0
  7. package/dist/cli/commands/login.d.ts +3 -0
  8. package/dist/cli/commands/publish.d.ts +3 -0
  9. package/dist/cli/commands/run.d.ts +3 -0
  10. package/dist/cli/commands/serve-tests.d.ts +3 -0
  11. package/dist/cli/commands/serve.d.ts +3 -0
  12. package/dist/cli/index.d.ts +1 -0
  13. package/dist/cli/lib/config.d.ts +25 -0
  14. package/dist/cli/lib/declarations.d.ts +19 -0
  15. package/dist/cli/lib/engine-script.d.ts +10 -0
  16. package/dist/cli/lib/execution-manager.d.ts +52 -0
  17. package/dist/cli/lib/zip.d.ts +6 -0
  18. package/dist/cli.js +11566 -0
  19. package/dist/core/client.d.ts +682 -0
  20. package/dist/core/client.test.d.ts +1 -0
  21. package/dist/core/platform-client.d.ts +106 -0
  22. package/dist/core/platform-client.test.d.ts +1 -0
  23. package/dist/core/request-error.d.ts +25 -0
  24. package/dist/core/request-error.test.d.ts +1 -0
  25. package/dist/index.cjs.js +2 -0
  26. package/dist/index.d.ts +12 -0
  27. package/dist/index.es.js +3310 -0
  28. package/dist/types/base.d.ts +9 -0
  29. package/dist/types/context.d.ts +20 -0
  30. package/dist/types/execution.d.ts +19 -0
  31. package/dist/types/files.d.ts +19 -0
  32. package/dist/types/item.dto.d.ts +24 -0
  33. package/dist/types/items.d.ts +57 -0
  34. package/dist/types/projects.d.ts +59 -0
  35. package/dist/types/response.d.ts +10 -0
  36. package/dist/types/storage.d.ts +11 -0
  37. package/dist/vite-env.d.ts +1 -0
  38. package/package.json +100 -0
  39. package/src/built-in/index.ts +755 -0
  40. package/src/cli/templates/bim/CONTEXT.md +244 -0
  41. package/src/cli/templates/bim/package.json +26 -0
  42. package/src/cli/templates/bim/src/app.ts +16 -0
  43. package/src/cli/templates/bim/src/bim-components/CloudRunner/index.ts +91 -0
  44. package/src/cli/templates/bim/src/bim-components/CloudRunner/src/index.ts +1 -0
  45. package/src/cli/templates/bim/src/bim-components/CloudRunner/src/types.ts +5 -0
  46. package/src/cli/templates/bim/src/bim-components/index.ts +1 -0
  47. package/src/cli/templates/bim/src/globals.ts +1 -0
  48. package/src/cli/templates/bim/src/main.ts +90 -0
  49. package/src/cli/templates/bim/src/setups/cloud-runner.ts +13 -0
  50. package/src/cli/templates/bim/src/setups/index.ts +3 -0
  51. package/src/cli/templates/bim/src/setups/ui-manager.ts +27 -0
  52. package/src/cli/templates/bim/src/setups/viewports-manager.ts +22 -0
  53. package/src/cli/templates/bim/src/ui-components/app-info-section/index.ts +26 -0
  54. package/src/cli/templates/bim/src/ui-components/app-info-section/src/index.ts +1 -0
  55. package/src/cli/templates/bim/src/ui-components/app-info-section/src/types.ts +15 -0
  56. package/src/cli/templates/bim/src/ui-components/cloud-runner-section/index.ts +37 -0
  57. package/src/cli/templates/bim/src/ui-components/cloud-runner-section/src/index.ts +1 -0
  58. package/src/cli/templates/bim/src/ui-components/cloud-runner-section/src/types.ts +14 -0
  59. package/src/cli/templates/bim/src/ui-components/index.ts +2 -0
  60. package/src/cli/templates/cloud/CONTEXT.md +205 -0
  61. package/src/cli/templates/cloud/_thatopen +5 -0
  62. package/src/cli/templates/cloud/declarations.json +4 -0
  63. package/src/cli/templates/cloud/package.json +22 -0
  64. package/src/cli/templates/cloud/src/main.ts +70 -0
  65. package/src/cli/templates/cloud-test/CONTEXT.md +56 -0
  66. package/src/cli/templates/cloud-test/_thatopen +5 -0
  67. package/src/cli/templates/cloud-test/package.json +22 -0
  68. package/src/cli/templates/cloud-test/src/main.ts +565 -0
  69. package/src/cli/templates/default/CONTEXT.md +92 -0
  70. package/src/cli/templates/default/package.json +15 -0
  71. package/src/cli/templates/default/src/main.ts +62 -0
  72. package/src/cli/templates/shared/_gitignore +4 -0
  73. package/src/cli/templates/shared/app/index.html +27 -0
  74. package/src/cli/templates/shared/app/tsconfig.json +16 -0
  75. package/src/cli/templates/shared/app/vite.config.js +23 -0
  76. package/src/cli/templates/shared/cloud/tsconfig.json +16 -0
  77. package/src/cli/templates/shared/cloud/vite.config.js +27 -0
  78. package/src/cli/templates/test/CONTEXT.md +53 -0
  79. package/src/cli/templates/test/package.json +25 -0
  80. package/src/cli/templates/test/src/main.ts +955 -0
@@ -0,0 +1,955 @@
1
+ // Platform API Test Suite
2
+ // Tests all EngineServicesClient endpoints with a BIM viewer + results panel.
3
+ // Generated by: thatopen create --template test
4
+
5
+ import * as THREE from "three";
6
+ import * as OBC from "@thatopen/components";
7
+ import * as OBF from "@thatopen/components-front";
8
+ import * as FRAGS from "@thatopen/fragments";
9
+ import * as BUI from "@thatopen/ui";
10
+ import * as CUI from "@thatopen/ui-obc";
11
+ import {
12
+ PlatformClient,
13
+ AppManager,
14
+ ViewportManager,
15
+ } from "thatopen-services";
16
+ import type { ThatOpenContext } from "thatopen-services";
17
+
18
+ declare global {
19
+ interface Window {
20
+ __THATOPEN_CONTEXT__?: ThatOpenContext;
21
+ }
22
+ }
23
+
24
+ type TestResult = {
25
+ name: string;
26
+ status: "pass" | "fail" | "skip";
27
+ message: string;
28
+ duration: number;
29
+ };
30
+
31
+ type TestGroup = {
32
+ name: string;
33
+ results: TestResult[];
34
+ };
35
+
36
+ async function runTest(
37
+ name: string,
38
+ fn: () => Promise<void>,
39
+ ): Promise<TestResult> {
40
+ const start = performance.now();
41
+ try {
42
+ await fn();
43
+ return {
44
+ name,
45
+ status: "pass",
46
+ message: "OK",
47
+ duration: performance.now() - start,
48
+ };
49
+ } catch (err) {
50
+ return {
51
+ name,
52
+ status: "fail",
53
+ message: err instanceof Error ? err.message : String(err),
54
+ duration: performance.now() - start,
55
+ };
56
+ }
57
+ }
58
+
59
+ function assert(condition: boolean, msg: string) {
60
+ if (!condition) throw new Error(msg);
61
+ }
62
+
63
+ function renderResults(
64
+ container: HTMLElement,
65
+ groups: TestGroup[],
66
+ running: boolean,
67
+ ) {
68
+ let totalPass = 0;
69
+ let totalFail = 0;
70
+ let totalSkip = 0;
71
+ for (const g of groups) {
72
+ for (const r of g.results) {
73
+ if (r.status === "pass") totalPass++;
74
+ else if (r.status === "fail") totalFail++;
75
+ else totalSkip++;
76
+ }
77
+ }
78
+ const total = totalPass + totalFail + totalSkip;
79
+
80
+ let html = "";
81
+ if (running) {
82
+ html += '<p style="color:#e08a00;font-weight:bold;">Running tests...</p>';
83
+ }
84
+ if (total > 0) {
85
+ html += '<p style="margin-bottom:16px;">';
86
+ html += '<span style="color:#16a34a;font-weight:bold;">' + totalPass + " passed</span> · ";
87
+ html += '<span style="color:#dc2626;font-weight:bold;">' + totalFail + " failed</span> · ";
88
+ html += '<span style="color:#9ca3af;font-weight:bold;">' + totalSkip + " skipped</span> · ";
89
+ html += total + " total</p>";
90
+ }
91
+
92
+ for (const group of groups) {
93
+ html += '<details open style="margin-bottom:12px;"><summary style="cursor:pointer;font-weight:bold;font-size:16px;padding:8px 0;">' + group.name + "</summary>";
94
+ html += '<table style="width:100%;border-collapse:collapse;font-size:14px;">';
95
+ for (const r of group.results) {
96
+ const icon = r.status === "pass" ? "\u2713" : r.status === "fail" ? "\u2717" : "\u25CB";
97
+ const color = r.status === "pass" ? "#16a34a" : r.status === "fail" ? "#dc2626" : "#9ca3af";
98
+ const escapedMsg = r.message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
99
+ html += '<tr style="border-bottom:1px solid #3C3C41;">';
100
+ html += '<td style="padding:6px 8px;color:' + color + ';font-weight:bold;width:24px;">' + icon + "</td>";
101
+ html += '<td style="padding:6px 8px;">' + r.name + "</td>";
102
+ html += '<td style="padding:6px 8px;color:' + color + ';max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + escapedMsg + '">' + escapedMsg + "</td>";
103
+ html += '<td style="padding:6px 8px;color:#9ca3af;text-align:right;white-space:nowrap;">' + r.duration.toFixed(0) + "ms</td>";
104
+ html += "</tr>";
105
+ }
106
+ html += "</table></details>";
107
+ }
108
+
109
+ container.innerHTML = html;
110
+ }
111
+
112
+ // ─── Execution Message Parser ───────────────────────────────────
113
+ // The cloud-test component sends messages in this format:
114
+ // "GroupName\n ✓ testName: OK\n ✗ testName: error"
115
+ // We parse each message into a TestGroup so it can be rendered
116
+ // using the same renderResults() as the local tests.
117
+
118
+ function parseExecMessage(content: string, prefix: string): TestGroup | null {
119
+ const lines = content.split("\n");
120
+ if (lines.length < 2) return null;
121
+ const groupName = lines[0].trim();
122
+ if (!groupName) return null;
123
+ const results: TestResult[] = [];
124
+ for (let i = 1; i < lines.length; i++) {
125
+ const line = lines[i].trim();
126
+ if (!line) continue;
127
+ let status: "pass" | "fail" | "skip" = "skip";
128
+ let rest = line;
129
+ if (line.startsWith("\u2713 ")) {
130
+ status = "pass";
131
+ rest = line.slice(2);
132
+ } else if (line.startsWith("\u2717 ")) {
133
+ status = "fail";
134
+ rest = line.slice(2);
135
+ } else if (line.startsWith("\u25CB ")) {
136
+ status = "skip";
137
+ rest = line.slice(2);
138
+ }
139
+ const colonIdx = rest.indexOf(": ");
140
+ if (colonIdx > 0) {
141
+ results.push({ name: rest.slice(0, colonIdx), status, message: rest.slice(colonIdx + 2), duration: 0 });
142
+ } else {
143
+ results.push({ name: rest, status, message: "", duration: 0 });
144
+ }
145
+ }
146
+ if (results.length === 0) return null;
147
+ return { name: prefix + groupName, results };
148
+ }
149
+
150
+ // ─── 1x1 transparent PNG for icon tests ────────────────────────
151
+ function createTestPng(): Blob {
152
+ const bytes = new Uint8Array([
153
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00,
154
+ 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
155
+ 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xde,
156
+ 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63,
157
+ 0xf8, 0xcf, 0xc0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21,
158
+ 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
159
+ 0x42, 0x60, 0x82,
160
+ ]);
161
+ return new Blob([bytes], { type: "image/png" });
162
+ }
163
+
164
+ async function main() {
165
+ const client = PlatformClient.fromPlatformContext();
166
+
167
+ // ─── OBC Components + Built-in Components ─────────────────────
168
+ const { components } = await client.initApp(
169
+ { OBC, OBF, BUI, CUI, THREE, FRAGS },
170
+ AppManager, ViewportManager,
171
+ );
172
+
173
+ // ─── 3D Viewport ──────────────────────────────────────────────
174
+ const viewports = components.get(ViewportManager);
175
+ const { element: viewerElement, world } = await viewports.create();
176
+
177
+ // ─── Load Model: GitHub → Storage → Scene ─────────────────────
178
+ // Fetches a .frag model, uploads to platform storage, downloads it
179
+ // back, and loads into the 3D viewport to prove the full pipeline.
180
+ const fragments = components.get(OBC.FragmentsManager);
181
+ const fragUrl = "https://thatopen.github.io/engine_components/resources/frags/school_arq.frag";
182
+ try {
183
+ const fetchRes = await fetch(fragUrl);
184
+ const fragBuffer = await fetchRes.arrayBuffer();
185
+
186
+ // Upload to platform storage
187
+ const uploadResult = await client.createFile({
188
+ file: new Blob([fragBuffer], { type: "application/octet-stream" }),
189
+ name: "_test_model_" + Date.now() + ".frag",
190
+ versionTag: "v1",
191
+ });
192
+ const modelFileId = uploadResult.item._id;
193
+
194
+ // Download from platform storage
195
+ const dlResponse = await client.downloadFile(modelFileId);
196
+ const dlBuffer = await dlResponse.arrayBuffer();
197
+ const bytes = new Uint8Array(dlBuffer);
198
+
199
+ // Load into 3D scene
200
+ await fragments.core.load(bytes, { modelId: modelFileId });
201
+ await fragments.core.update(true);
202
+ await world.camera.controls.setLookAt(68, 23, -8.5, 21.5, -5.5, 23);
203
+
204
+ // Clean up the test file from storage
205
+ try { await client.archiveFile(modelFileId); } catch { /* cleanup */ }
206
+ console.log("Model loaded: GitHub → Storage → Scene");
207
+ } catch (err) {
208
+ console.warn("Could not load model:", err);
209
+ }
210
+
211
+ // ─── Test Results Container ────────────────────────────────────
212
+ const resultsContainer = document.createElement("div");
213
+ resultsContainer.style.color = "#d5d5d5";
214
+
215
+ // ─── Execution Config Inputs ──────────────────────────────────
216
+ const inputCss = "width:100%;padding:6px 8px;background:#262528;color:#d5d5d5;border:1px solid #3C3C41;border-radius:4px;font-size:12px;box-sizing:border-box;";
217
+
218
+ const componentIdInput = document.createElement("input");
219
+ componentIdInput.type = "text";
220
+ componentIdInput.placeholder = "Paste component ID here";
221
+ componentIdInput.style.cssText = inputCss;
222
+
223
+ const localServerInput = document.createElement("input");
224
+ localServerInput.type = "text";
225
+ localServerInput.value = "http://localhost:4001";
226
+ localServerInput.style.cssText = inputCss;
227
+
228
+ // ─── Panel ────────────────────────────────────────────────────
229
+ const panel = () => {
230
+ return BUI.html`
231
+ <bim-panel label="API Test Suite">
232
+ <bim-panel-section label="Context">
233
+ <bim-label>App ID: ${client.context.appId}</bim-label>
234
+ <bim-label>Project ID: ${client.context.projectId}</bim-label>
235
+ <bim-label>API URL: ${client.context.apiUrl}</bim-label>
236
+ </bim-panel-section>
237
+ <bim-panel-section label="Execution Config" icon="solar:settings-bold">
238
+ <bim-label>Component ID (deployed):</bim-label>
239
+ ${componentIdInput}
240
+ <bim-label>Local Server URL:</bim-label>
241
+ ${localServerInput}
242
+ </bim-panel-section>
243
+ <bim-panel-section label="Controls">
244
+ <bim-button label="Run All Tests" icon="solar:play-bold"
245
+ @click=${() => runAllTests(resultsContainer, client, componentIdInput, localServerInput)}></bim-button>
246
+ </bim-panel-section>
247
+ <bim-panel-section label="Results">
248
+ ${resultsContainer}
249
+ </bim-panel-section>
250
+ </bim-panel>
251
+ `;
252
+ };
253
+
254
+ // ─── App Shell ─────────────────────────────────────────────────
255
+ const appManager = components.get(AppManager);
256
+ appManager.setup = {
257
+ elements: {
258
+ viewer: viewerElement,
259
+ panel,
260
+ },
261
+ layouts: {
262
+ Tests: {
263
+ template: `"panel viewer" 1fr / 24rem 1fr`,
264
+ icon: "solar:test-tube-bold",
265
+ },
266
+ Viewer: { template: `"viewer" 1fr / 1fr` },
267
+ },
268
+ };
269
+ appManager.init();
270
+ }
271
+
272
+ async function runAllTests(resultsEl: HTMLElement, client: PlatformClient, componentIdInput: HTMLInputElement, localServerInput: HTMLInputElement) {
273
+ const groups: TestGroup[] = [];
274
+ const ts = Date.now();
275
+
276
+ // ─── Context & Auth ────────────────────────────────────────
277
+ const ctxResults: TestResult[] = [];
278
+ ctxResults.push(
279
+ await runTest("context.appId exists", async () => {
280
+ assert(!!client.context.appId, "appId is empty");
281
+ }),
282
+ );
283
+ ctxResults.push(
284
+ await runTest("context.projectId exists", async () => {
285
+ assert(!!client.context.projectId, "projectId is empty");
286
+ }),
287
+ );
288
+ ctxResults.push(
289
+ await runTest("context.accessToken exists", async () => {
290
+ assert(!!client.context.accessToken, "accessToken is empty");
291
+ }),
292
+ );
293
+ ctxResults.push(
294
+ await runTest("context.apiUrl exists", async () => {
295
+ assert(!!client.context.apiUrl, "apiUrl is empty");
296
+ }),
297
+ );
298
+ groups.push({ name: "Context & Auth", results: ctxResults });
299
+ renderResults(resultsEl, groups, true);
300
+
301
+ // ─── Projects & Permissions ────────────────────────────────
302
+ const projResults: TestResult[] = [];
303
+ if (client.context.projectId) {
304
+ projResults.push(
305
+ await runTest("getProject", async () => {
306
+ const project = await client.getProject(client.context.projectId);
307
+ assert(!!project._id, "project._id missing");
308
+ assert(typeof project.title === "string", "project.title missing");
309
+ }),
310
+ );
311
+ projResults.push(
312
+ await runTest("getProjectData", async () => {
313
+ const data = await client.getProjectData(client.context.projectId);
314
+ assert(!!data.project, "project missing");
315
+ assert(Array.isArray(data.users), "users not array");
316
+ assert(Array.isArray(data.files), "files not array");
317
+ assert(Array.isArray(data.folders), "folders not array");
318
+ assert(Array.isArray(data.roles), "roles not array");
319
+ }),
320
+ );
321
+ projResults.push(
322
+ await runTest("checkPermission", async () => {
323
+ const result = await client.checkPermission({
324
+ resourceId: client.context.projectId,
325
+ resourceType: "PROJECT",
326
+ action: "READ",
327
+ projectId: client.context.projectId,
328
+ });
329
+ assert(typeof result.hasPermission === "boolean", "hasPermission not boolean");
330
+ }),
331
+ );
332
+ } else {
333
+ const skip = (n: string) => ({ name: n, status: "skip" as const, message: "No projectId", duration: 0 });
334
+ projResults.push(skip("getProject"), skip("getProjectData"), skip("checkPermission"));
335
+ }
336
+ groups.push({ name: "Projects & Permissions", results: projResults });
337
+ renderResults(resultsEl, groups, true);
338
+
339
+ // ─── Folders ───────────────────────────────────────────────
340
+ const folderResults: TestResult[] = [];
341
+ let testFolderId = "";
342
+ folderResults.push(
343
+ await runTest("createFolder", async () => {
344
+ const folder = await client.createFolder("_test_folder_" + ts);
345
+ assert(!!folder._id, "_id missing");
346
+ assert(!!folder.name, "name missing");
347
+ testFolderId = folder._id;
348
+ }),
349
+ );
350
+ folderResults.push(
351
+ await runTest("getFolder", async () => {
352
+ assert(!!testFolderId, "No folder created");
353
+ const folder = await client.getFolder(testFolderId);
354
+ assert(folder._id === testFolderId, "_id mismatch");
355
+ }),
356
+ );
357
+ folderResults.push(
358
+ await runTest("listFolders", async () => {
359
+ const folders = await client.listFolders();
360
+ assert(Array.isArray(folders), "Not an array");
361
+ }),
362
+ );
363
+ folderResults.push(
364
+ await runTest("updateFolder", async () => {
365
+ assert(!!testFolderId, "No folder");
366
+ const updated = await client.updateFolder(testFolderId, {
367
+ name: "_test_folder_renamed_" + ts,
368
+ });
369
+ assert(updated.name.includes("renamed"), "Name not updated");
370
+ }),
371
+ );
372
+ folderResults.push(
373
+ await runTest("archiveFolder", async () => {
374
+ assert(!!testFolderId, "No folder");
375
+ await client.archiveFolder(testFolderId);
376
+ }),
377
+ );
378
+ folderResults.push(
379
+ await runTest("recoverFolder", async () => {
380
+ assert(!!testFolderId, "No folder");
381
+ await client.recoverFolder(testFolderId);
382
+ }),
383
+ );
384
+ folderResults.push(
385
+ await runTest("downloadFolder", async () => {
386
+ assert(!!testFolderId, "No folder");
387
+ const response = await client.downloadFolder(testFolderId);
388
+ assert(!!response, "No response");
389
+ }),
390
+ );
391
+ groups.push({ name: "Folders", results: folderResults });
392
+ renderResults(resultsEl, groups, true);
393
+
394
+ // ─── Files ─────────────────────────────────────────────────
395
+ const fileResults: TestResult[] = [];
396
+ let testFileId = "";
397
+ const testContent = "test file content " + ts;
398
+ const testBlob = new Blob([testContent], { type: "text/plain" });
399
+
400
+ fileResults.push(
401
+ await runTest("createFile", async () => {
402
+ const result = await client.createFile({
403
+ file: testBlob,
404
+ name: "_test_file_" + ts + ".txt",
405
+ versionTag: "v1",
406
+ parentFolderId: testFolderId || undefined,
407
+ metadata: { testKey: "testValue" },
408
+ });
409
+ assert(!!result.item._id, "item._id missing");
410
+ assert(!!result.version, "version missing");
411
+ testFileId = result.item._id;
412
+ }),
413
+ );
414
+ fileResults.push(
415
+ await runTest("getFile (with versions)", async () => {
416
+ assert(!!testFileId, "No file");
417
+ const file = await client.getFile(testFileId, { showVersions: true });
418
+ assert(file._id === testFileId, "_id mismatch");
419
+ assert(Array.isArray(file.versions), "versions not array");
420
+ assert(file.versions.length > 0, "versions empty");
421
+ }),
422
+ );
423
+ fileResults.push(
424
+ await runTest("listFiles", async () => {
425
+ const files = await client.listFiles();
426
+ assert(Array.isArray(files), "Not an array");
427
+ }),
428
+ );
429
+ fileResults.push(
430
+ await runTest("listFiles (with folderId)", async () => {
431
+ if (!testFolderId) throw new Error("No folder");
432
+ const files = await client.listFiles({ folderId: testFolderId });
433
+ assert(Array.isArray(files), "Not an array");
434
+ }),
435
+ );
436
+ fileResults.push(
437
+ await runTest("downloadFile", async () => {
438
+ assert(!!testFileId, "No file");
439
+ const response = await client.downloadFile(testFileId);
440
+ assert(response.ok !== false, "Response not ok");
441
+ const text = await response.text();
442
+ assert(text === testContent, "Content mismatch");
443
+ }),
444
+ );
445
+ fileResults.push(
446
+ await runTest("getFileVersionMetadata", async () => {
447
+ assert(!!testFileId, "No file");
448
+ const metadata = await client.getFileVersionMetadata(testFileId, "v1");
449
+ assert(typeof metadata === "object", "metadata not object");
450
+ }),
451
+ );
452
+ fileResults.push(
453
+ await runTest("updateFileVersionMetadata", async () => {
454
+ assert(!!testFileId, "No file");
455
+ const result = await client.updateFileVersionMetadata(testFileId, "v1", {
456
+ discipline: "structural",
457
+ });
458
+ assert(typeof result === "object", "result not object");
459
+ }),
460
+ );
461
+ fileResults.push(
462
+ await runTest("deleteFileVersionMetadata", async () => {
463
+ assert(!!testFileId, "No file");
464
+ await client.deleteFileVersionMetadata(testFileId, "v1");
465
+ }),
466
+ );
467
+ fileResults.push(
468
+ await runTest("updateFile (rename + new version)", async () => {
469
+ assert(!!testFileId, "No file");
470
+ const newBlob = new Blob(["updated content"], { type: "text/plain" });
471
+ const result = await client.updateFile(testFileId, {
472
+ name: "_test_file_renamed_" + ts + ".txt",
473
+ file: newBlob,
474
+ versionTag: "v2",
475
+ });
476
+ assert(!!result.version || !!result.item, "No update result");
477
+ }),
478
+ );
479
+ fileResults.push(
480
+ await runTest("archiveFile", async () => {
481
+ assert(!!testFileId, "No file");
482
+ await client.archiveFile(testFileId);
483
+ }),
484
+ );
485
+ fileResults.push(
486
+ await runTest("recoverFile", async () => {
487
+ assert(!!testFileId, "No file");
488
+ await client.recoverFile(testFileId);
489
+ }),
490
+ );
491
+ groups.push({ name: "Files", results: fileResults });
492
+ renderResults(resultsEl, groups, true);
493
+
494
+ // ─── Hidden Files ──────────────────────────────────────────
495
+ const hiddenResults: TestResult[] = [];
496
+ let testHiddenId = "";
497
+
498
+ hiddenResults.push(
499
+ await runTest("createHiddenFile", async () => {
500
+ assert(!!testFileId, "No parent file");
501
+ const blob = new Blob(["hidden content"], { type: "text/plain" });
502
+ const result = await client.createHiddenFile(blob, testFileId);
503
+ assert(!!result.hiddenFileId, "hiddenFileId missing");
504
+ testHiddenId = result.hiddenFileId;
505
+ }),
506
+ );
507
+ hiddenResults.push(
508
+ await runTest("getHiddenFile", async () => {
509
+ assert(!!testHiddenId, "No hidden file");
510
+ const hidden = await client.getHiddenFile(testHiddenId);
511
+ assert(!!hidden._id, "_id missing");
512
+ }),
513
+ );
514
+ hiddenResults.push(
515
+ await runTest("getHiddenFilesByParent", async () => {
516
+ assert(!!testFileId, "No parent file");
517
+ const files = await client.getHiddenFilesByParent(testFileId);
518
+ assert(Array.isArray(files), "Not an array");
519
+ assert(files.length > 0, "No hidden files found");
520
+ }),
521
+ );
522
+ hiddenResults.push(
523
+ await runTest("downloadHiddenFile", async () => {
524
+ assert(!!testHiddenId, "No hidden file");
525
+ const response = await client.downloadHiddenFile(testHiddenId);
526
+ assert(!!response, "No response");
527
+ }),
528
+ );
529
+ hiddenResults.push(
530
+ await runTest("deleteHiddenFile", async () => {
531
+ assert(!!testHiddenId, "No hidden file");
532
+ await client.deleteHiddenFile(testHiddenId);
533
+ }),
534
+ );
535
+ hiddenResults.push(
536
+ await runTest("deleteHiddenFilesByParent", async () => {
537
+ assert(!!testFileId, "No parent file");
538
+ // Create a fresh hidden file, then delete all by parent
539
+ const blob = new Blob(["temp hidden"], { type: "text/plain" });
540
+ await client.createHiddenFile(blob, testFileId);
541
+ const result = await client.deleteHiddenFilesByParent(testFileId);
542
+ assert(Array.isArray(result), "Not an array");
543
+ }),
544
+ );
545
+ groups.push({ name: "Hidden Files", results: hiddenResults });
546
+ renderResults(resultsEl, groups, true);
547
+
548
+ // ─── Icons ─────────────────────────────────────────────────
549
+ const iconResults: TestResult[] = [];
550
+ const iconBlob = createTestPng();
551
+
552
+ iconResults.push(
553
+ await runTest("uploadItemIcon", async () => {
554
+ assert(!!testFileId, "No item for icon");
555
+ await client.uploadItemIcon(testFileId, iconBlob);
556
+ }),
557
+ );
558
+ iconResults.push(
559
+ await runTest("getItemIcon", async () => {
560
+ assert(!!testFileId, "No item for icon");
561
+ const response = await client.getItemIcon(testFileId);
562
+ assert(!!response, "No response");
563
+ }),
564
+ );
565
+ iconResults.push(
566
+ await runTest("removeItemIcon", async () => {
567
+ assert(!!testFileId, "No item for icon");
568
+ await client.removeItemIcon(testFileId);
569
+ }),
570
+ );
571
+ groups.push({ name: "Icons", results: iconResults });
572
+ renderResults(resultsEl, groups, true);
573
+
574
+ // ─── General Item Operations ───────────────────────────────
575
+ const generalResults: TestResult[] = [];
576
+
577
+ generalResults.push(
578
+ await runTest("updateItem (rename without version)", async () => {
579
+ assert(!!testFileId, "No item");
580
+ const item = await client.updateItem(testFileId, {
581
+ name: "_test_file_general_" + ts + ".txt",
582
+ });
583
+ assert(!!item._id, "item._id missing");
584
+ }),
585
+ );
586
+ generalResults.push(
587
+ await runTest("createVersion", async () => {
588
+ assert(!!testFileId, "No item");
589
+ const blob = new Blob(["version 3 content"], { type: "text/plain" });
590
+ const version = await client.createVersion(testFileId, blob, "v3");
591
+ assert(!!version, "version missing");
592
+ }),
593
+ );
594
+ groups.push({ name: "General Item Operations", results: generalResults });
595
+ renderResults(resultsEl, groups, true);
596
+
597
+ // Cleanup test file
598
+ if (testFileId) {
599
+ try {
600
+ await client.archiveFile(testFileId);
601
+ } catch {
602
+ /* cleanup */
603
+ }
604
+ }
605
+
606
+ // ─── Components ────────────────────────────────────────────
607
+ const compResults: TestResult[] = [];
608
+ let testComponentId = "";
609
+ const compBlob = new Blob(["// test component"], {
610
+ type: "application/javascript",
611
+ });
612
+
613
+ compResults.push(
614
+ await runTest("createComponent", async () => {
615
+ const result = await client.createComponent({
616
+ file: compBlob,
617
+ name: "_test_component_" + ts,
618
+ versionTag: "v1",
619
+ componentProps: { type: "CLOUD", tier: "FREE" },
620
+ });
621
+ assert(!!result.item._id, "item._id missing");
622
+ assert(!!result.version, "version missing");
623
+ testComponentId = result.item._id;
624
+ }),
625
+ );
626
+ compResults.push(
627
+ await runTest("getComponent (with versions)", async () => {
628
+ assert(!!testComponentId, "No component");
629
+ const comp = await client.getComponent(testComponentId, {
630
+ showVersions: true,
631
+ });
632
+ assert(comp._id === testComponentId, "_id mismatch");
633
+ assert(Array.isArray(comp.versions), "versions not array");
634
+ }),
635
+ );
636
+ compResults.push(
637
+ await runTest("listComponents", async () => {
638
+ const components = await client.listComponents();
639
+ assert(Array.isArray(components), "Not an array");
640
+ }),
641
+ );
642
+ compResults.push(
643
+ await runTest("updateComponent", async () => {
644
+ assert(!!testComponentId, "No component");
645
+ const blob = new Blob(["// updated component"], {
646
+ type: "application/javascript",
647
+ });
648
+ const result = await client.updateComponent(testComponentId, {
649
+ name: "_test_component_renamed_" + ts,
650
+ file: blob,
651
+ versionTag: "v2",
652
+ componentProps: { type: "CLOUD", tier: "FREE" },
653
+ });
654
+ assert(!!result.version || !!result.item, "No update result");
655
+ }),
656
+ );
657
+ compResults.push(
658
+ await runTest("downloadComponent", async () => {
659
+ assert(!!testComponentId, "No component");
660
+ const response = await client.downloadComponent(testComponentId);
661
+ assert(!!response, "No response");
662
+ }),
663
+ );
664
+ compResults.push(
665
+ await runTest("downloadComponentBundle", async () => {
666
+ assert(!!testComponentId, "No component");
667
+ const response = await client.downloadComponentBundle(testComponentId);
668
+ assert(!!response, "No response");
669
+ }),
670
+ );
671
+ compResults.push(
672
+ await runTest("archiveComponent", async () => {
673
+ assert(!!testComponentId, "No component");
674
+ await client.archiveComponent(testComponentId);
675
+ }),
676
+ );
677
+ compResults.push(
678
+ await runTest("recoverComponent", async () => {
679
+ assert(!!testComponentId, "No component");
680
+ await client.recoverComponent(testComponentId);
681
+ }),
682
+ );
683
+ groups.push({ name: "Components", results: compResults });
684
+ renderResults(resultsEl, groups, true);
685
+
686
+ // ─── Apps ───────────────────────────────────────────────────
687
+ const appResults: TestResult[] = [];
688
+ let testAppId = "";
689
+ const appBlob = new Blob(["// test app bundle"], {
690
+ type: "application/javascript",
691
+ });
692
+
693
+ appResults.push(
694
+ await runTest("listApps", async () => {
695
+ const apps = await client.listApps();
696
+ assert(Array.isArray(apps), "Not an array");
697
+ }),
698
+ );
699
+ appResults.push(
700
+ await runTest("createApp", async () => {
701
+ const result = await client.createApp({
702
+ file: appBlob,
703
+ name: "_test_app_" + ts,
704
+ versionTag: "v1",
705
+ });
706
+ assert(!!result.item._id, "item._id missing");
707
+ assert(!!result.version, "version missing");
708
+ testAppId = result.item._id;
709
+ }),
710
+ );
711
+ appResults.push(
712
+ await runTest("downloadApp", async () => {
713
+ assert(!!testAppId, "No app");
714
+ const response = await client.downloadApp(testAppId);
715
+ assert(!!response, "No response");
716
+ }),
717
+ );
718
+ appResults.push(
719
+ await runTest("downloadAppBundle", async () => {
720
+ assert(!!testAppId, "No app");
721
+ const response = await client.downloadAppBundle(testAppId);
722
+ assert(!!response, "No response");
723
+ }),
724
+ );
725
+ appResults.push(
726
+ await runTest("archiveApp", async () => {
727
+ assert(!!testAppId, "No app");
728
+ await client.archiveApp(testAppId);
729
+ }),
730
+ );
731
+ groups.push({ name: "Apps", results: appResults });
732
+ renderResults(resultsEl, groups, true);
733
+
734
+ // ─── Execution (Deployed) ──────────────────────────────────
735
+ // Tests execution against a published cloud component.
736
+ // Requires a Component ID entered in the panel.
737
+ const deployedResults: TestResult[] = [];
738
+ const deployedCompId = componentIdInput.value.trim();
739
+
740
+ if (deployedCompId) {
741
+ let deployedExecId = "";
742
+ deployedResults.push(
743
+ await runTest("executeComponent (deployed)", async () => {
744
+ const result = await client.executeComponent(deployedCompId, {
745
+ testParam: "deployed-test",
746
+ });
747
+ assert(!!result.executionId, "executionId missing");
748
+ deployedExecId = result.executionId;
749
+ client.onExecutionProgress(result.executionId, (data) => {
750
+ if (data.messageUpdate) {
751
+ const g = parseExecMessage(data.messageUpdate.content, "Deployed Component: ");
752
+ if (g) { groups.push(g); renderResults(resultsEl, groups, true); }
753
+ }
754
+ });
755
+ }),
756
+ );
757
+ deployedResults.push(
758
+ await runTest("getExecution", async () => {
759
+ assert(!!deployedExecId, "No execution");
760
+ const execution = await client.getExecution(deployedExecId);
761
+ assert(!!execution, "execution missing");
762
+ }),
763
+ );
764
+ deployedResults.push(
765
+ await runTest("listExecutions", async () => {
766
+ const executions = await client.listExecutions(deployedCompId);
767
+ assert(Array.isArray(executions), "Not an array");
768
+ }),
769
+ );
770
+ deployedResults.push(
771
+ await runTest("onExecutionProgress (subscribe)", async () => {
772
+ const result = await client.executeComponent(deployedCompId, {
773
+ testParam: "progress-test",
774
+ });
775
+ assert(!!result.executionId, "executionId missing");
776
+ await new Promise<void>((resolve) => {
777
+ const timeout = setTimeout(() => resolve(), 10000);
778
+ client.onExecutionProgress(result.executionId, (data) => {
779
+ if (data.messageUpdate) {
780
+ const g = parseExecMessage(data.messageUpdate.content, "Deployed Component: ");
781
+ if (g) { groups.push(g); renderResults(resultsEl, groups, true); }
782
+ }
783
+ if (data.progressUpdate?.result) {
784
+ clearTimeout(timeout);
785
+ resolve();
786
+ }
787
+ });
788
+ });
789
+ }),
790
+ );
791
+ deployedResults.push(
792
+ await runTest("abortExecution", async () => {
793
+ const result = await client.executeComponent(deployedCompId, {
794
+ testParam: "abort-test",
795
+ });
796
+ assert(!!result.executionId, "executionId missing");
797
+ try {
798
+ await client.abortExecution(result.executionId);
799
+ } catch (err) {
800
+ const status =
801
+ err && typeof err === "object" && "status" in err
802
+ ? Number((err as { status?: number }).status)
803
+ : 0;
804
+ const is4xx = status >= 400 && status < 500;
805
+ if (!is4xx) throw err;
806
+ }
807
+ }),
808
+ );
809
+ } else {
810
+ const skip = (n: string) => ({
811
+ name: n,
812
+ status: "skip" as const,
813
+ message: "Enter a Component ID above",
814
+ duration: 0,
815
+ });
816
+ deployedResults.push(
817
+ skip("executeComponent (deployed)"),
818
+ skip("getExecution"),
819
+ skip("listExecutions"),
820
+ skip("onExecutionProgress"),
821
+ skip("abortExecution"),
822
+ );
823
+ }
824
+ groups.push({ name: "Execution (Deployed)", results: deployedResults });
825
+ renderResults(resultsEl, groups, true);
826
+
827
+ // ─── Execution (Local) ─────────────────────────────────────
828
+ // Tests execution against a local component dev server.
829
+ // Requires thatopen local-server running in the component project.
830
+ const localResults: TestResult[] = [];
831
+ const localUrl = localServerInput.value.trim();
832
+
833
+ if (localUrl) {
834
+ client.localServerUrl = localUrl;
835
+ let localExecId = "";
836
+ localResults.push(
837
+ await runTest("executeComponent (local)", async () => {
838
+ const result = await client.executeComponent("local-test", {
839
+ testParam: "local-test",
840
+ });
841
+ assert(!!result.executionId, "executionId missing");
842
+ localExecId = result.executionId;
843
+ client.onExecutionProgress(result.executionId, (data) => {
844
+ if (data.messageUpdate) {
845
+ const g = parseExecMessage(data.messageUpdate.content, "Local Component: ");
846
+ if (g) { groups.push(g); renderResults(resultsEl, groups, true); }
847
+ }
848
+ });
849
+ }),
850
+ );
851
+ localResults.push(
852
+ await runTest("getExecution (local)", async () => {
853
+ assert(!!localExecId, "No local execution");
854
+ const execution = await client.getExecution(localExecId);
855
+ assert(!!execution, "execution missing");
856
+ }),
857
+ );
858
+ localResults.push(
859
+ await runTest("listExecutions (local)", async () => {
860
+ const executions = await client.listExecutions("local-test");
861
+ assert(Array.isArray(executions), "Not an array");
862
+ }),
863
+ );
864
+ localResults.push(
865
+ await runTest("onExecutionProgress (local)", async () => {
866
+ const result = await client.executeComponent("local-test", {
867
+ testParam: "progress-local",
868
+ });
869
+ assert(!!result.executionId, "executionId missing");
870
+ await new Promise<void>((resolve) => {
871
+ const timeout = setTimeout(() => resolve(), 10000);
872
+ client.onExecutionProgress(result.executionId, (data) => {
873
+ if (data.messageUpdate) {
874
+ const g = parseExecMessage(data.messageUpdate.content, "Local Component: ");
875
+ if (g) { groups.push(g); renderResults(resultsEl, groups, true); }
876
+ }
877
+ if (data.progressUpdate?.result) {
878
+ clearTimeout(timeout);
879
+ resolve();
880
+ }
881
+ });
882
+ });
883
+ }),
884
+ );
885
+ localResults.push(
886
+ await runTest("abortExecution (local)", async () => {
887
+ const result = await client.executeComponent("local-test", {
888
+ testParam: "abort-local",
889
+ });
890
+ assert(!!result.executionId, "executionId missing");
891
+ try {
892
+ await client.abortExecution(result.executionId);
893
+ } catch (err) {
894
+ const status =
895
+ err && typeof err === "object" && "status" in err
896
+ ? Number((err as { status?: number }).status)
897
+ : 0;
898
+ const is4xx = status >= 400 && status < 500;
899
+ if (!is4xx) throw err;
900
+ }
901
+ }),
902
+ );
903
+ client.localServerUrl = null;
904
+ } else {
905
+ const skip = (n: string) => ({
906
+ name: n,
907
+ status: "skip" as const,
908
+ message: "Clear Local Server URL to skip",
909
+ duration: 0,
910
+ });
911
+ localResults.push(
912
+ skip("executeComponent (local)"),
913
+ skip("getExecution (local)"),
914
+ skip("listExecutions (local)"),
915
+ skip("onExecutionProgress (local)"),
916
+ skip("abortExecution (local)"),
917
+ );
918
+ }
919
+ groups.push({ name: "Execution (Local)", results: localResults });
920
+ renderResults(resultsEl, groups, true);
921
+
922
+ // ─── Built-in Components ───────────────────────────────────
923
+ const builtInResults: TestResult[] = [];
924
+ builtInResults.push(
925
+ await runTest("getBuiltInComponent (HelloWorld)", async () => {
926
+ const source = await client.getBuiltInComponent(
927
+ "2c4ae432-fc24-43e9-9783-0c960c674e96",
928
+ );
929
+ assert(typeof source === "string", "Not a string");
930
+ assert(source.length > 0, "Empty source");
931
+ }),
932
+ );
933
+ groups.push({ name: "Built-in Components", results: builtInResults });
934
+
935
+ // ─── Cleanup ───────────────────────────────────────────────
936
+ if (testComponentId) {
937
+ try {
938
+ await client.archiveComponent(testComponentId);
939
+ } catch {
940
+ /* cleanup */
941
+ }
942
+ }
943
+ if (testFolderId) {
944
+ try {
945
+ await client.archiveFolder(testFolderId);
946
+ } catch {
947
+ /* cleanup */
948
+ }
949
+ }
950
+
951
+ renderResults(resultsEl, groups, false);
952
+ console.log("Test suite complete.");
953
+ }
954
+
955
+ main().catch(console.error);