nebula-cms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +42 -0
- package/.github/workflows/ci.yml +31 -0
- package/.mcp.json +12 -0
- package/.prettierignore +5 -0
- package/.prettierrc.cjs +22 -0
- package/AGENTS.md +183 -0
- package/LICENSE +201 -0
- package/README.md +128 -0
- package/package.json +74 -0
- package/playground/.claude/settings.local.json +5 -0
- package/playground/astro.config.mjs +7 -0
- package/playground/node_modules/.bin/astro +21 -0
- package/playground/node_modules/.bin/rollup +21 -0
- package/playground/node_modules/.bin/tsc +21 -0
- package/playground/node_modules/.bin/tsserver +21 -0
- package/playground/node_modules/.bin/vite +21 -0
- package/playground/node_modules/.vite/_svelte_metadata.json +1 -0
- package/playground/node_modules/.vite/deps/@astrojs_svelte_client__js.js +80 -0
- package/playground/node_modules/.vite/deps/@astrojs_svelte_client__js.js.map +7 -0
- package/playground/node_modules/.vite/deps/_metadata.json +184 -0
- package/playground/node_modules/.vite/deps/astro___aria-query.js +6776 -0
- package/playground/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
- package/playground/node_modules/.vite/deps/astro___axobject-query.js +3754 -0
- package/playground/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
- package/playground/node_modules/.vite/deps/astro___html-escaper.js +34 -0
- package/playground/node_modules/.vite/deps/astro___html-escaper.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-AJXJMYAF.js +0 -0
- package/playground/node_modules/.vite/deps/chunk-AJXJMYAF.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ALJIOON6.js +1005 -0
- package/playground/node_modules/.vite/deps/chunk-ALJIOON6.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-BUSYA2B4.js +8 -0
- package/playground/node_modules/.vite/deps/chunk-BUSYA2B4.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-CNYJBM5F.js +21 -0
- package/playground/node_modules/.vite/deps/chunk-CNYJBM5F.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-DBPNBGEI.js +223 -0
- package/playground/node_modules/.vite/deps/chunk-DBPNBGEI.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-G3C2FXJT.js +204 -0
- package/playground/node_modules/.vite/deps/chunk-G3C2FXJT.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-GKDKFWC5.js +27 -0
- package/playground/node_modules/.vite/deps/chunk-GKDKFWC5.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-HNCLEOC5.js +4376 -0
- package/playground/node_modules/.vite/deps/chunk-HNCLEOC5.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-JICYXBFU.js +688 -0
- package/playground/node_modules/.vite/deps/chunk-JICYXBFU.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-KCUTL6DD.js +5099 -0
- package/playground/node_modules/.vite/deps/chunk-KCUTL6DD.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ZP4UNCSN.js +23 -0
- package/playground/node_modules/.vite/deps/chunk-ZP4UNCSN.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ZREFNRZZ.js +148 -0
- package/playground/node_modules/.vite/deps/chunk-ZREFNRZZ.js.map +7 -0
- package/playground/node_modules/.vite/deps/package.json +3 -0
- package/playground/node_modules/.vite/deps/smol-toml.js +843 -0
- package/playground/node_modules/.vite/deps/smol-toml.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte.js +55 -0
- package/playground/node_modules/.vite/deps/svelte.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte___clsx.js +9 -0
- package/playground/node_modules/.vite/deps/svelte___clsx.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_animate.js +57 -0
- package/playground/node_modules/.vite/deps/svelte_animate.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_attachments.js +15 -0
- package/playground/node_modules/.vite/deps/svelte_attachments.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_easing.js +67 -0
- package/playground/node_modules/.vite/deps/svelte_easing.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_events.js +11 -0
- package/playground/node_modules/.vite/deps/svelte_events.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal.js +5 -0
- package/playground/node_modules/.vite/deps/svelte_internal.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_client.js +402 -0
- package/playground/node_modules/.vite/deps/svelte_internal_client.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_disclose-version.js +10 -0
- package/playground/node_modules/.vite/deps/svelte_internal_disclose-version.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_async.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_async.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_legacy.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_legacy.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_tracing.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_tracing.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_legacy.js +35 -0
- package/playground/node_modules/.vite/deps/svelte_legacy.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_motion.js +545 -0
- package/playground/node_modules/.vite/deps/svelte_motion.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity.js +29 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity_window.js +127 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity_window.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_store.js +103 -0
- package/playground/node_modules/.vite/deps/svelte_store.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_transition.js +208 -0
- package/playground/node_modules/.vite/deps/svelte_transition.js.map +7 -0
- package/playground/package.json +16 -0
- package/playground/pnpm-lock.yaml +3167 -0
- package/playground/src/content/authors/jane-doe.json +8 -0
- package/playground/src/content/config/build.toml +2 -0
- package/playground/src/content/courses/web-fundamentals.json +29 -0
- package/playground/src/content/docs/advanced.mdx +6 -0
- package/playground/src/content/docs/intro.md +6 -0
- package/playground/src/content/guides/getting-started.mdx +6 -0
- package/playground/src/content/posts/hello-world.md +7 -0
- package/playground/src/content/products/t-shirt.json +16 -0
- package/playground/src/content/recipes/pancakes.mdoc +8 -0
- package/playground/src/content/settings/site.yml +2 -0
- package/playground/src/content.config.ts +198 -0
- package/playground/src/env.d.ts +1 -0
- package/playground/src/pages/index.astro +11 -0
- package/playground/src/pages/nebula.astro +14 -0
- package/pnpm-workspace.yaml +2 -0
- package/scripts/subset-icons.mjs +178 -0
- package/src/astro/index.ts +295 -0
- package/src/client/Admin.svelte +283 -0
- package/src/client/components/BackendPicker.svelte +291 -0
- package/src/client/components/DraftChip.svelte +46 -0
- package/src/client/components/MetadataForm.svelte +56 -0
- package/src/client/components/ThemeToggle.svelte +18 -0
- package/src/client/components/dialogs/DeleteDraftDialog.svelte +51 -0
- package/src/client/components/dialogs/FilenameDialog.svelte +129 -0
- package/src/client/components/editor/EditorPane.svelte +227 -0
- package/src/client/components/editor/EditorTabs.svelte +81 -0
- package/src/client/components/editor/EditorToolbar.svelte +131 -0
- package/src/client/components/editor/FormatSelector.svelte +66 -0
- package/src/client/components/editor/Toolbar.svelte +17 -0
- package/src/client/components/fields/ArrayField.svelte +339 -0
- package/src/client/components/fields/ArrayItem.svelte +325 -0
- package/src/client/components/fields/BooleanField.svelte +114 -0
- package/src/client/components/fields/DateField.svelte +82 -0
- package/src/client/components/fields/EnumField.svelte +74 -0
- package/src/client/components/fields/FieldWrapper.svelte +96 -0
- package/src/client/components/fields/NumberField.svelte +99 -0
- package/src/client/components/fields/ObjectField.svelte +121 -0
- package/src/client/components/fields/SchemaField.svelte +107 -0
- package/src/client/components/fields/StringField.svelte +104 -0
- package/src/client/components/sidebar/AdminSidebar.svelte +339 -0
- package/src/client/components/sidebar/AdminSidebarSort.svelte +123 -0
- package/src/client/css/a11y.css +14 -0
- package/src/client/css/btn.css +113 -0
- package/src/client/css/dialog.css +29 -0
- package/src/client/css/field-input.css +39 -0
- package/src/client/css/reset.css +59 -0
- package/src/client/css/theme.css +77 -0
- package/src/client/index.ts +1 -0
- package/src/client/js/drafts/merge.svelte.ts +121 -0
- package/src/client/js/drafts/ops.svelte.ts +227 -0
- package/src/client/js/drafts/storage.ts +108 -0
- package/src/client/js/drafts/workers/diff.ts +40 -0
- package/src/client/js/editor/editor.svelte.ts +343 -0
- package/src/client/js/editor/languages.ts +98 -0
- package/src/client/js/editor/link-wrap.ts +45 -0
- package/src/client/js/editor/markdown-shortcuts.ts +261 -0
- package/src/client/js/handlers/admin.ts +246 -0
- package/src/client/js/state/dialogs.svelte.ts +35 -0
- package/src/client/js/state/router.svelte.ts +156 -0
- package/src/client/js/state/schema.svelte.ts +140 -0
- package/src/client/js/state/state.svelte.ts +334 -0
- package/src/client/js/state/theme.svelte.ts +173 -0
- package/src/client/js/storage/adapter.ts +102 -0
- package/src/client/js/storage/client.ts +150 -0
- package/src/client/js/storage/db.ts +36 -0
- package/src/client/js/storage/fsa.ts +110 -0
- package/src/client/js/storage/github.ts +297 -0
- package/src/client/js/storage/storage.ts +83 -0
- package/src/client/js/storage/workers/frontmatter.ts +320 -0
- package/src/client/js/storage/workers/storage.ts +177 -0
- package/src/client/js/storage/workers/toml-parser.ts +106 -0
- package/src/client/js/storage/workers/yaml-parser.ts +132 -0
- package/src/client/js/utils/file-types.ts +192 -0
- package/src/client/js/utils/format.ts +16 -0
- package/src/client/js/utils/frontmatter.ts +38 -0
- package/src/client/js/utils/schema-utils.ts +295 -0
- package/src/client/js/utils/slug.ts +18 -0
- package/src/client/js/utils/sort.ts +84 -0
- package/src/client/js/utils/stable-stringify.ts +27 -0
- package/src/client/js/utils/url-utils.ts +38 -0
- package/src/types.ts +25 -0
- package/src/virtual.d.ts +22 -0
- package/svelte.config.js +4 -0
- package/tests/astro/build.test.ts +63 -0
- package/tests/astro/index.test.ts +689 -0
- package/tests/client/components/Admin.test.ts +446 -0
- package/tests/client/components/BackendPicker.test.ts +239 -0
- package/tests/client/components/DraftChip.test.ts +53 -0
- package/tests/client/components/MetadataForm.test.ts +164 -0
- package/tests/client/components/dialogs/DeleteDraftDialog.test.ts +91 -0
- package/tests/client/components/dialogs/FilenameDialog.test.ts +209 -0
- package/tests/client/components/dialogs/dialog-stubs.ts +19 -0
- package/tests/client/components/editor/EditorPane.test.ts +100 -0
- package/tests/client/components/editor/EditorTabs.test.ts +253 -0
- package/tests/client/components/editor/EditorToolbar.test.ts +252 -0
- package/tests/client/components/editor/fixtures.ts +31 -0
- package/tests/client/components/fields/ArrayField.test.ts +197 -0
- package/tests/client/components/fields/BooleanField.test.ts +206 -0
- package/tests/client/components/fields/DateField.test.ts +210 -0
- package/tests/client/components/fields/EnumField.test.ts +246 -0
- package/tests/client/components/fields/NumberField.test.ts +240 -0
- package/tests/client/components/fields/ObjectField.test.ts +157 -0
- package/tests/client/components/fields/SchemaField.test.ts +190 -0
- package/tests/client/components/fields/StringField.test.ts +223 -0
- package/tests/client/components/sidebar/AdminSidebar.test.ts +285 -0
- package/tests/client/components/sidebar/AdminSidebarSort.test.ts +135 -0
- package/tests/client/components/sidebar/sort-mock.ts +23 -0
- package/tests/client/js/drafts/fixtures.ts +22 -0
- package/tests/client/js/drafts/merge.test.ts +282 -0
- package/tests/client/js/drafts/ops.test.ts +658 -0
- package/tests/client/js/drafts/storage.test.ts +200 -0
- package/tests/client/js/drafts/workers/diff.test.ts +165 -0
- package/tests/client/js/editor/editor.test.ts +616 -0
- package/tests/client/js/editor/link-wrap.test.ts +225 -0
- package/tests/client/js/editor/markdown-shortcuts.test.ts +370 -0
- package/tests/client/js/handlers/admin.test.ts +467 -0
- package/tests/client/js/state/router.test.ts +619 -0
- package/tests/client/js/state/schema.test.ts +266 -0
- package/tests/client/js/state/state.test.ts +328 -0
- package/tests/client/js/storage/adapter.test.ts +115 -0
- package/tests/client/js/storage/client.test.ts +250 -0
- package/tests/client/js/storage/db.test.ts +59 -0
- package/tests/client/js/storage/fsa.test.ts +284 -0
- package/tests/client/js/storage/github.test.ts +349 -0
- package/tests/client/js/storage/mock-port.ts +95 -0
- package/tests/client/js/storage/storage.test.ts +77 -0
- package/tests/client/js/storage/workers/frontmatter.test.ts +479 -0
- package/tests/client/js/storage/workers/storage.test.ts +299 -0
- package/tests/client/js/storage/workers/toml-parser.test.ts +169 -0
- package/tests/client/js/storage/workers/yaml-parser.test.ts +168 -0
- package/tests/client/js/utils/file-types.test.ts +268 -0
- package/tests/client/js/utils/frontmatter.test.ts +87 -0
- package/tests/client/js/utils/schema-utils.test.ts +318 -0
- package/tests/client/js/utils/slug.test.ts +58 -0
- package/tests/client/js/utils/sort.test.ts +276 -0
- package/tests/client/js/utils/stable-stringify.test.ts +68 -0
- package/tests/client/js/utils/url-utils.test.ts +70 -0
- package/tests/e2e/backend-connection.test.ts +301 -0
- package/tests/e2e/draft-lifecycle.test.ts +388 -0
- package/tests/e2e/editing.test.ts +355 -0
- package/tests/e2e/github-adapter.test.ts +330 -0
- package/tests/e2e/helpers/mock-adapter.ts +166 -0
- package/tests/e2e/helpers/test-app.ts +155 -0
- package/tests/e2e/navigation.test.ts +358 -0
- package/tests/e2e/publishing.test.ts +345 -0
- package/tests/e2e/unsaved-changes.test.ts +317 -0
- package/tests/setup.ts +2 -0
- package/tests/stubs/codemirror.ts +197 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +178 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
//////////////////////////////
|
|
4
|
+
// adapter.ts type-export tests
|
|
5
|
+
//
|
|
6
|
+
// adapter.ts is a pure type/interface file — there is no runtime code to
|
|
7
|
+
// exercise. These tests confirm that the named exports are importable and
|
|
8
|
+
// that the module resolves without errors, providing a compile-time
|
|
9
|
+
// regression guard.
|
|
10
|
+
//////////////////////////////
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
FileEntry,
|
|
14
|
+
FileWrite,
|
|
15
|
+
StorageAdapter,
|
|
16
|
+
StorageRequest,
|
|
17
|
+
StorageResponse,
|
|
18
|
+
} from '../../../../src/client/js/storage/adapter';
|
|
19
|
+
|
|
20
|
+
describe('adapter type exports', () => {
|
|
21
|
+
it('FileEntry type is usable as a value shape', () => {
|
|
22
|
+
// Constructing a conformant object validates the type at compile time
|
|
23
|
+
const entry: FileEntry = { filename: 'post.md', content: '# Hello' };
|
|
24
|
+
expect(entry.filename).toBe('post.md');
|
|
25
|
+
expect(entry.content).toBe('# Hello');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('FileWrite type is usable as a value shape', () => {
|
|
29
|
+
const write: FileWrite = {
|
|
30
|
+
collection: 'posts',
|
|
31
|
+
filename: 'post.md',
|
|
32
|
+
content: '# Hello',
|
|
33
|
+
};
|
|
34
|
+
expect(write.collection).toBe('posts');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('StorageAdapter interface shape has the expected method names', () => {
|
|
38
|
+
// Verify the interface contract can be satisfied by a plain object
|
|
39
|
+
const adapter: StorageAdapter = {
|
|
40
|
+
listFiles: async () => [],
|
|
41
|
+
readFile: async () => '',
|
|
42
|
+
writeFile: async () => undefined,
|
|
43
|
+
writeFiles: async () => undefined,
|
|
44
|
+
deleteFile: async () => undefined,
|
|
45
|
+
};
|
|
46
|
+
expect(typeof adapter.listFiles).toBe('function');
|
|
47
|
+
expect(typeof adapter.readFile).toBe('function');
|
|
48
|
+
expect(typeof adapter.writeFile).toBe('function');
|
|
49
|
+
expect(typeof adapter.writeFiles).toBe('function');
|
|
50
|
+
expect(typeof adapter.deleteFile).toBe('function');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('StorageRequest union members resolve without type errors', () => {
|
|
54
|
+
const init: StorageRequest = {
|
|
55
|
+
type: 'init',
|
|
56
|
+
backend: { type: 'github', token: 'tok', repo: 'owner/repo' },
|
|
57
|
+
};
|
|
58
|
+
const list: StorageRequest = {
|
|
59
|
+
type: 'listFiles',
|
|
60
|
+
collection: 'posts',
|
|
61
|
+
extensions: ['.md', '.mdx'],
|
|
62
|
+
};
|
|
63
|
+
const read: StorageRequest = {
|
|
64
|
+
type: 'readFile',
|
|
65
|
+
collection: 'posts',
|
|
66
|
+
filename: 'a.md',
|
|
67
|
+
};
|
|
68
|
+
const write: StorageRequest = {
|
|
69
|
+
type: 'writeFile',
|
|
70
|
+
collection: 'posts',
|
|
71
|
+
filename: 'a.md',
|
|
72
|
+
content: 'body',
|
|
73
|
+
};
|
|
74
|
+
const writeMany: StorageRequest = {
|
|
75
|
+
type: 'writeFiles',
|
|
76
|
+
files: [{ collection: 'posts', filename: 'a.md', content: 'body' }],
|
|
77
|
+
};
|
|
78
|
+
const del: StorageRequest = {
|
|
79
|
+
type: 'deleteFile',
|
|
80
|
+
collection: 'posts',
|
|
81
|
+
filename: 'old.md',
|
|
82
|
+
};
|
|
83
|
+
const teardown: StorageRequest = { type: 'teardown' };
|
|
84
|
+
|
|
85
|
+
expect(init.type).toBe('init');
|
|
86
|
+
expect(list.type).toBe('listFiles');
|
|
87
|
+
expect(read.type).toBe('readFile');
|
|
88
|
+
expect(write.type).toBe('writeFile');
|
|
89
|
+
expect(writeMany.type).toBe('writeFiles');
|
|
90
|
+
expect(del.type).toBe('deleteFile');
|
|
91
|
+
expect(teardown.type).toBe('teardown');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('StorageResponse union members resolve without type errors', () => {
|
|
95
|
+
const ok: StorageResponse = { type: 'init', ok: true };
|
|
96
|
+
const fail: StorageResponse = {
|
|
97
|
+
type: 'init',
|
|
98
|
+
ok: false,
|
|
99
|
+
error: 'bad token',
|
|
100
|
+
};
|
|
101
|
+
const delOk: StorageResponse = { type: 'deleteFile', ok: true };
|
|
102
|
+
const delFail: StorageResponse = {
|
|
103
|
+
type: 'deleteFile',
|
|
104
|
+
ok: false,
|
|
105
|
+
error: 'not found',
|
|
106
|
+
};
|
|
107
|
+
const portConn: StorageResponse = { type: 'port-connected' };
|
|
108
|
+
|
|
109
|
+
expect(ok.type).toBe('init');
|
|
110
|
+
expect(fail.ok).toBe(false);
|
|
111
|
+
expect(delOk.type).toBe('deleteFile');
|
|
112
|
+
expect(delFail.ok).toBe(false);
|
|
113
|
+
expect(portConn.type).toBe('port-connected');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
//////////////////////////////
|
|
4
|
+
// StorageClient tests
|
|
5
|
+
//
|
|
6
|
+
// StorageClient wraps a MessagePort with request/response correlation.
|
|
7
|
+
// We simulate the port with a plain EventTarget + a manual postMessage spy
|
|
8
|
+
// so we can fire synthetic message events and verify that calls are
|
|
9
|
+
// matched back to their originating promise.
|
|
10
|
+
//////////////////////////////
|
|
11
|
+
|
|
12
|
+
import { StorageClient } from '../../../../src/client/js/storage/client';
|
|
13
|
+
import type { StorageResponse } from '../../../../src/client/js/storage/adapter';
|
|
14
|
+
import { makeClientMockPort as makeMockPort } from './mock-port';
|
|
15
|
+
|
|
16
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe('StorageClient', () => {
|
|
19
|
+
let postSpy: ReturnType<typeof vi.fn>;
|
|
20
|
+
let respond: (data: StorageResponse & { _id?: string }) => void;
|
|
21
|
+
let client: StorageClient;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
const mock = makeMockPort();
|
|
25
|
+
postSpy = mock.postSpy;
|
|
26
|
+
respond = mock.respond;
|
|
27
|
+
client = new StorageClient(mock.port);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('calls port.start() in the constructor', () => {
|
|
31
|
+
const mock = makeMockPort();
|
|
32
|
+
// start() is called during construction
|
|
33
|
+
new StorageClient(mock.port);
|
|
34
|
+
expect(
|
|
35
|
+
(mock.port.start as ReturnType<typeof vi.fn>).mock.calls.length,
|
|
36
|
+
).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('init', () => {
|
|
40
|
+
it('sends an init message and resolves when a matching ok response arrives', async () => {
|
|
41
|
+
const promise = client.init({
|
|
42
|
+
type: 'init',
|
|
43
|
+
backend: { type: 'github', token: 'tok', repo: 'owner/repo' },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Grab the _id from the sent message
|
|
47
|
+
const sentMsg = postSpy.mock.calls[0][0] as { _id: string };
|
|
48
|
+
respond({ type: 'init', ok: true, _id: sentMsg._id } as any);
|
|
49
|
+
|
|
50
|
+
await expect(promise).resolves.toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('rejects when the worker responds with ok: false', async () => {
|
|
54
|
+
const promise = client.init({
|
|
55
|
+
type: 'init',
|
|
56
|
+
backend: { type: 'github', token: 'tok', repo: 'owner/repo' },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const sentMsg = postSpy.mock.calls[0][0] as { _id: string };
|
|
60
|
+
respond({
|
|
61
|
+
type: 'init',
|
|
62
|
+
ok: false,
|
|
63
|
+
error: 'bad credentials',
|
|
64
|
+
_id: sentMsg._id,
|
|
65
|
+
} as any);
|
|
66
|
+
|
|
67
|
+
await expect(promise).rejects.toThrow('bad credentials');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('listFiles', () => {
|
|
72
|
+
it('resolves with the files array from the worker response', async () => {
|
|
73
|
+
const files = [{ filename: 'a.md', content: 'A' }];
|
|
74
|
+
const promise = client.listFiles('posts', ['.md', '.mdx']);
|
|
75
|
+
|
|
76
|
+
const sentMsg = postSpy.mock.calls[0][0] as {
|
|
77
|
+
_id: string;
|
|
78
|
+
type: string;
|
|
79
|
+
collection: string;
|
|
80
|
+
extensions: string[];
|
|
81
|
+
};
|
|
82
|
+
expect(sentMsg.type).toBe('listFiles');
|
|
83
|
+
expect(sentMsg.collection).toBe('posts');
|
|
84
|
+
expect(sentMsg.extensions).toEqual(['.md', '.mdx']);
|
|
85
|
+
|
|
86
|
+
respond({
|
|
87
|
+
type: 'listFiles',
|
|
88
|
+
ok: true,
|
|
89
|
+
files,
|
|
90
|
+
_id: sentMsg._id,
|
|
91
|
+
} as any);
|
|
92
|
+
|
|
93
|
+
const result = await promise;
|
|
94
|
+
expect(result).toEqual(files);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects on error response', async () => {
|
|
98
|
+
const promise = client.listFiles('posts', ['.md', '.mdx']);
|
|
99
|
+
const sentMsg = postSpy.mock.calls[0][0] as { _id: string };
|
|
100
|
+
respond({
|
|
101
|
+
type: 'listFiles',
|
|
102
|
+
ok: false,
|
|
103
|
+
error: 'storage error',
|
|
104
|
+
_id: sentMsg._id,
|
|
105
|
+
} as any);
|
|
106
|
+
await expect(promise).rejects.toThrow('storage error');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('deleteFile', () => {
|
|
111
|
+
it('resolves when the worker responds with ok: true', async () => {
|
|
112
|
+
const promise = client.deleteFile('posts', 'old.md');
|
|
113
|
+
const sentMsg = postSpy.mock.calls[0][0] as {
|
|
114
|
+
_id: string;
|
|
115
|
+
type: string;
|
|
116
|
+
collection: string;
|
|
117
|
+
filename: string;
|
|
118
|
+
};
|
|
119
|
+
expect(sentMsg.type).toBe('deleteFile');
|
|
120
|
+
expect(sentMsg.collection).toBe('posts');
|
|
121
|
+
expect(sentMsg.filename).toBe('old.md');
|
|
122
|
+
respond({ type: 'deleteFile', ok: true, _id: sentMsg._id } as any);
|
|
123
|
+
await expect(promise).resolves.toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('rejects on error response', async () => {
|
|
127
|
+
const promise = client.deleteFile('posts', 'missing.md');
|
|
128
|
+
const sentMsg = postSpy.mock.calls[0][0] as { _id: string };
|
|
129
|
+
respond({
|
|
130
|
+
type: 'deleteFile',
|
|
131
|
+
ok: false,
|
|
132
|
+
error: 'File not found',
|
|
133
|
+
_id: sentMsg._id,
|
|
134
|
+
} as any);
|
|
135
|
+
await expect(promise).rejects.toThrow('File not found');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('readFile', () => {
|
|
140
|
+
it('resolves with the content from the worker response', async () => {
|
|
141
|
+
const promise = client.readFile('posts', 'hello.md');
|
|
142
|
+
|
|
143
|
+
const sentMsg = postSpy.mock.calls[0][0] as {
|
|
144
|
+
_id: string;
|
|
145
|
+
collection: string;
|
|
146
|
+
filename: string;
|
|
147
|
+
};
|
|
148
|
+
expect(sentMsg.collection).toBe('posts');
|
|
149
|
+
expect(sentMsg.filename).toBe('hello.md');
|
|
150
|
+
|
|
151
|
+
respond({
|
|
152
|
+
type: 'readFile',
|
|
153
|
+
ok: true,
|
|
154
|
+
content: '# Hello',
|
|
155
|
+
_id: sentMsg._id,
|
|
156
|
+
} as any);
|
|
157
|
+
|
|
158
|
+
const result = await promise;
|
|
159
|
+
expect(result).toBe('# Hello');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('writeFile', () => {
|
|
164
|
+
it('resolves when the worker responds with ok: true', async () => {
|
|
165
|
+
const promise = client.writeFile('posts', 'test.md', 'body');
|
|
166
|
+
const sentMsg = postSpy.mock.calls[0][0] as { _id: string };
|
|
167
|
+
respond({ type: 'writeFile', ok: true, _id: sentMsg._id } as any);
|
|
168
|
+
await expect(promise).resolves.toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('writeFiles', () => {
|
|
173
|
+
it('sends the files array and resolves on ok response', async () => {
|
|
174
|
+
const files = [
|
|
175
|
+
{ collection: 'posts', filename: 'a.md', content: 'A' },
|
|
176
|
+
{ collection: 'posts', filename: 'b.md', content: 'B' },
|
|
177
|
+
];
|
|
178
|
+
const promise = client.writeFiles(files);
|
|
179
|
+
|
|
180
|
+
const sentMsg = postSpy.mock.calls[0][0] as {
|
|
181
|
+
_id: string;
|
|
182
|
+
type: string;
|
|
183
|
+
files: typeof files;
|
|
184
|
+
};
|
|
185
|
+
expect(sentMsg.type).toBe('writeFiles');
|
|
186
|
+
expect(sentMsg.files).toEqual(files);
|
|
187
|
+
|
|
188
|
+
respond({ type: 'writeFiles', ok: true, _id: sentMsg._id } as any);
|
|
189
|
+
await expect(promise).resolves.toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('teardown', () => {
|
|
194
|
+
it('sends a teardown message and resolves on ok response', async () => {
|
|
195
|
+
const promise = client.teardown();
|
|
196
|
+
const sentMsg = postSpy.mock.calls[0][0] as {
|
|
197
|
+
_id: string;
|
|
198
|
+
type: string;
|
|
199
|
+
};
|
|
200
|
+
expect(sentMsg.type).toBe('teardown');
|
|
201
|
+
respond({ type: 'teardown', ok: true, _id: sentMsg._id } as any);
|
|
202
|
+
await expect(promise).resolves.toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('message correlation', () => {
|
|
207
|
+
it('ignores broadcast messages without an _id', async () => {
|
|
208
|
+
// port-connected has no _id and should not reject any pending promise
|
|
209
|
+
const promise = client.listFiles('posts', ['.md', '.mdx']);
|
|
210
|
+
respond({ type: 'port-connected' } as any);
|
|
211
|
+
// Promise should still be pending — resolve it now with a proper response
|
|
212
|
+
const sentMsg = postSpy.mock.calls[0][0] as { _id: string };
|
|
213
|
+
respond({
|
|
214
|
+
type: 'listFiles',
|
|
215
|
+
ok: true,
|
|
216
|
+
files: [],
|
|
217
|
+
_id: sentMsg._id,
|
|
218
|
+
} as any);
|
|
219
|
+
await expect(promise).resolves.toEqual([]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('increments _id for each request so concurrent calls are independent', async () => {
|
|
223
|
+
const p1 = client.listFiles('posts', ['.md', '.mdx']);
|
|
224
|
+
const p2 = client.readFile('posts', 'a.md');
|
|
225
|
+
|
|
226
|
+
const msg1 = postSpy.mock.calls[0][0] as { _id: string };
|
|
227
|
+
const msg2 = postSpy.mock.calls[1][0] as { _id: string };
|
|
228
|
+
expect(msg1._id).not.toBe(msg2._id);
|
|
229
|
+
|
|
230
|
+
// Resolve in reverse order to prove there's no ordering dependency
|
|
231
|
+
respond({
|
|
232
|
+
type: 'readFile',
|
|
233
|
+
ok: true,
|
|
234
|
+
content: 'content',
|
|
235
|
+
_id: msg2._id,
|
|
236
|
+
} as any);
|
|
237
|
+
respond({
|
|
238
|
+
type: 'listFiles',
|
|
239
|
+
ok: true,
|
|
240
|
+
files: [{ filename: 'x.md', content: 'x' }],
|
|
241
|
+
_id: msg1._id,
|
|
242
|
+
} as any);
|
|
243
|
+
|
|
244
|
+
const list = await p1;
|
|
245
|
+
const read = await p2;
|
|
246
|
+
expect(list[0].filename).toBe('x.md');
|
|
247
|
+
expect(read).toBe('content');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
//////////////////////////////
|
|
4
|
+
// openDB tests
|
|
5
|
+
//
|
|
6
|
+
// Uses fake-indexeddb (polyfilled via the setup file) to exercise the
|
|
7
|
+
// actual IDB open/upgrade logic in a Node.js environment.
|
|
8
|
+
//////////////////////////////
|
|
9
|
+
|
|
10
|
+
import { openDB } from '../../../../src/client/js/storage/db';
|
|
11
|
+
|
|
12
|
+
describe('openDB', () => {
|
|
13
|
+
it('resolves to an IDBDatabase instance', async () => {
|
|
14
|
+
const db = await openDB();
|
|
15
|
+
expect(db).toBeDefined();
|
|
16
|
+
expect(typeof db.transaction).toBe('function');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('creates a "handles" object store', async () => {
|
|
20
|
+
const db = await openDB();
|
|
21
|
+
expect(db.objectStoreNames.contains('handles')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('creates a "drafts" object store', async () => {
|
|
25
|
+
const db = await openDB();
|
|
26
|
+
expect(db.objectStoreNames.contains('drafts')).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns a usable database that can perform readwrite transactions on "handles"', async () => {
|
|
30
|
+
const db = await openDB();
|
|
31
|
+
// A successful transaction confirms the store was created with correct configuration
|
|
32
|
+
await new Promise<void>((resolve, reject) => {
|
|
33
|
+
const tx = db.transaction('handles', 'readwrite');
|
|
34
|
+
tx.objectStore('handles').put({ test: true }, 'probe-key');
|
|
35
|
+
tx.oncomplete = () => resolve();
|
|
36
|
+
tx.onerror = () => reject(tx.error);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns a usable database that can perform readwrite transactions on "drafts"', async () => {
|
|
41
|
+
const db = await openDB();
|
|
42
|
+
// The drafts store uses { keyPath: 'id' }, so the object must include an id field
|
|
43
|
+
await new Promise<void>((resolve, reject) => {
|
|
44
|
+
const tx = db.transaction('drafts', 'readwrite');
|
|
45
|
+
tx.objectStore('drafts').put({ id: 'probe-draft', data: 'x' });
|
|
46
|
+
tx.oncomplete = () => resolve();
|
|
47
|
+
tx.onerror = () => reject(tx.error);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('subsequent calls return a database with the same stores', async () => {
|
|
52
|
+
const db1 = await openDB();
|
|
53
|
+
const db2 = await openDB();
|
|
54
|
+
expect(db2.objectStoreNames.contains('handles')).toBe(true);
|
|
55
|
+
expect(db2.objectStoreNames.contains('drafts')).toBe(true);
|
|
56
|
+
db1.close();
|
|
57
|
+
db2.close();
|
|
58
|
+
});
|
|
59
|
+
});
|