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,200 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
//////////////////////////////
|
|
4
|
+
// Draft storage tests
|
|
5
|
+
//
|
|
6
|
+
// Uses fake-indexeddb (polyfilled via the setup file) so every test
|
|
7
|
+
// operates against a real IDBDatabase implementation running in Node.js.
|
|
8
|
+
// Each describe block that modifies state calls beforeEach to ensure a
|
|
9
|
+
// clean slate via a fresh IDBFactory instance.
|
|
10
|
+
//////////////////////////////
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
saveDraft,
|
|
14
|
+
loadDrafts,
|
|
15
|
+
loadDraft,
|
|
16
|
+
deleteDraft,
|
|
17
|
+
getDraftByFile,
|
|
18
|
+
} from '../../../../src/client/js/drafts/storage';
|
|
19
|
+
import { makeDraft } from './fixtures';
|
|
20
|
+
|
|
21
|
+
//////////////////////////////
|
|
22
|
+
// saveDraft + loadDraft
|
|
23
|
+
//////////////////////////////
|
|
24
|
+
|
|
25
|
+
describe('saveDraft / loadDraft', () => {
|
|
26
|
+
it('persists a draft and retrieves it by ID', async () => {
|
|
27
|
+
const draft = makeDraft({ id: 'save-load-01' });
|
|
28
|
+
await saveDraft(draft);
|
|
29
|
+
const result = await loadDraft('save-load-01');
|
|
30
|
+
expect(result).toEqual(draft);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns null when the draft ID does not exist', async () => {
|
|
34
|
+
const result = await loadDraft('does-not-exist');
|
|
35
|
+
expect(result).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('overwrites an existing draft when called with the same ID', async () => {
|
|
39
|
+
const original = makeDraft({ id: 'overwrite-01', body: 'original body' });
|
|
40
|
+
await saveDraft(original);
|
|
41
|
+
|
|
42
|
+
const updated = makeDraft({ id: 'overwrite-01', body: 'updated body' });
|
|
43
|
+
await saveDraft(updated);
|
|
44
|
+
|
|
45
|
+
const result = await loadDraft('overwrite-01');
|
|
46
|
+
expect(result?.body).toBe('updated body');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('stores all Draft fields without data loss', async () => {
|
|
50
|
+
const draft = makeDraft({
|
|
51
|
+
id: 'fields-01',
|
|
52
|
+
formData: { title: 'T', nested: { a: 1 } },
|
|
53
|
+
isNew: true,
|
|
54
|
+
filename: null,
|
|
55
|
+
snapshot: null,
|
|
56
|
+
});
|
|
57
|
+
await saveDraft(draft);
|
|
58
|
+
const result = await loadDraft('fields-01');
|
|
59
|
+
expect(result).toEqual(draft);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
//////////////////////////////
|
|
64
|
+
// loadDrafts
|
|
65
|
+
//////////////////////////////
|
|
66
|
+
|
|
67
|
+
describe('loadDrafts', () => {
|
|
68
|
+
it('returns all drafts belonging to a given collection', async () => {
|
|
69
|
+
const a = makeDraft({ id: 'ld-a', collection: 'posts' });
|
|
70
|
+
const b = makeDraft({ id: 'ld-b', collection: 'posts' });
|
|
71
|
+
const c = makeDraft({ id: 'ld-c', collection: 'pages' });
|
|
72
|
+
await saveDraft(a);
|
|
73
|
+
await saveDraft(b);
|
|
74
|
+
await saveDraft(c);
|
|
75
|
+
|
|
76
|
+
const posts = await loadDrafts('posts');
|
|
77
|
+
const ids = posts.map((d) => d.id);
|
|
78
|
+
expect(ids).toContain('ld-a');
|
|
79
|
+
expect(ids).toContain('ld-b');
|
|
80
|
+
expect(ids).not.toContain('ld-c');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns an empty array when no drafts exist for the collection', async () => {
|
|
84
|
+
const result = await loadDrafts('nonexistent-collection');
|
|
85
|
+
expect(result).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns only drafts matching the requested collection', async () => {
|
|
89
|
+
const d = makeDraft({ id: 'ld-only', collection: 'only-collection' });
|
|
90
|
+
await saveDraft(d);
|
|
91
|
+
|
|
92
|
+
const result = await loadDrafts('only-collection');
|
|
93
|
+
expect(result).toHaveLength(1);
|
|
94
|
+
expect(result[0].id).toBe('ld-only');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
//////////////////////////////
|
|
99
|
+
// deleteDraft
|
|
100
|
+
//////////////////////////////
|
|
101
|
+
|
|
102
|
+
describe('deleteDraft', () => {
|
|
103
|
+
it('removes the draft so loadDraft returns null afterwards', async () => {
|
|
104
|
+
const draft = makeDraft({ id: 'del-01' });
|
|
105
|
+
await saveDraft(draft);
|
|
106
|
+
await deleteDraft('del-01');
|
|
107
|
+
const result = await loadDraft('del-01');
|
|
108
|
+
expect(result).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('is a no-op when the draft ID does not exist', async () => {
|
|
112
|
+
// Should resolve without throwing
|
|
113
|
+
await expect(deleteDraft('ghost-id')).resolves.toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('does not remove other drafts in the same collection', async () => {
|
|
117
|
+
const keep = makeDraft({ id: 'del-keep', collection: 'posts' });
|
|
118
|
+
const remove = makeDraft({ id: 'del-remove', collection: 'posts' });
|
|
119
|
+
await saveDraft(keep);
|
|
120
|
+
await saveDraft(remove);
|
|
121
|
+
|
|
122
|
+
await deleteDraft('del-remove');
|
|
123
|
+
|
|
124
|
+
const remaining = await loadDrafts('posts');
|
|
125
|
+
const ids = remaining.map((d) => d.id);
|
|
126
|
+
expect(ids).toContain('del-keep');
|
|
127
|
+
expect(ids).not.toContain('del-remove');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
//////////////////////////////
|
|
132
|
+
// getDraftByFile
|
|
133
|
+
//////////////////////////////
|
|
134
|
+
|
|
135
|
+
describe('getDraftByFile', () => {
|
|
136
|
+
it('finds a non-new draft matching the given collection and filename', async () => {
|
|
137
|
+
const draft = makeDraft({
|
|
138
|
+
id: 'gbf-01',
|
|
139
|
+
collection: 'posts',
|
|
140
|
+
filename: 'my-post.md',
|
|
141
|
+
isNew: false,
|
|
142
|
+
});
|
|
143
|
+
await saveDraft(draft);
|
|
144
|
+
|
|
145
|
+
const result = await getDraftByFile('posts', 'my-post.md');
|
|
146
|
+
expect(result?.id).toBe('gbf-01');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns null when no draft matches the filename', async () => {
|
|
150
|
+
const result = await getDraftByFile('posts', 'nonexistent.md');
|
|
151
|
+
expect(result).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('ignores new drafts when searching by file', async () => {
|
|
155
|
+
// isNew:true drafts are not linked to live files, so getDraftByFile skips them
|
|
156
|
+
const draft = makeDraft({
|
|
157
|
+
id: 'gbf-new',
|
|
158
|
+
collection: 'posts',
|
|
159
|
+
filename: 'new-post.md',
|
|
160
|
+
isNew: true,
|
|
161
|
+
});
|
|
162
|
+
await saveDraft(draft);
|
|
163
|
+
|
|
164
|
+
const result = await getDraftByFile('posts', 'new-post.md');
|
|
165
|
+
expect(result).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns null when collection does not match even if filename does', async () => {
|
|
169
|
+
const draft = makeDraft({
|
|
170
|
+
id: 'gbf-coll',
|
|
171
|
+
collection: 'pages',
|
|
172
|
+
filename: 'shared-name.md',
|
|
173
|
+
isNew: false,
|
|
174
|
+
});
|
|
175
|
+
await saveDraft(draft);
|
|
176
|
+
|
|
177
|
+
const result = await getDraftByFile('posts', 'shared-name.md');
|
|
178
|
+
expect(result).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('returns the correct draft when multiple drafts exist in the same collection', async () => {
|
|
182
|
+
const a = makeDraft({
|
|
183
|
+
id: 'gbf-multi-a',
|
|
184
|
+
collection: 'posts',
|
|
185
|
+
filename: 'alpha.md',
|
|
186
|
+
isNew: false,
|
|
187
|
+
});
|
|
188
|
+
const b = makeDraft({
|
|
189
|
+
id: 'gbf-multi-b',
|
|
190
|
+
collection: 'posts',
|
|
191
|
+
filename: 'beta.md',
|
|
192
|
+
isNew: false,
|
|
193
|
+
});
|
|
194
|
+
await saveDraft(a);
|
|
195
|
+
await saveDraft(b);
|
|
196
|
+
|
|
197
|
+
const result = await getDraftByFile('posts', 'beta.md');
|
|
198
|
+
expect(result?.id).toBe('gbf-multi-b');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
//////////////////////////////
|
|
4
|
+
// Diff worker tests
|
|
5
|
+
//
|
|
6
|
+
// The worker registers self.addEventListener('message', ...) and compares
|
|
7
|
+
// draft snapshots against live content using stableStringify. We stub self
|
|
8
|
+
// before importing the module, capture the handler, and invoke it with
|
|
9
|
+
// synthetic MessageEvent objects.
|
|
10
|
+
//////////////////////////////
|
|
11
|
+
|
|
12
|
+
// ── Self mock ──────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
let messageHandler: ((event: MessageEvent) => void) | null = null;
|
|
15
|
+
const selfPostMessage = vi.fn();
|
|
16
|
+
|
|
17
|
+
vi.stubGlobal('self', {
|
|
18
|
+
addEventListener: vi.fn((type: string, handler: unknown) => {
|
|
19
|
+
if (type === 'message') {
|
|
20
|
+
messageHandler = handler as (event: MessageEvent) => void;
|
|
21
|
+
}
|
|
22
|
+
}),
|
|
23
|
+
postMessage: selfPostMessage,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ── Import module (after stubs) ────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
await import('../../../../../src/client/js/drafts/workers/diff');
|
|
29
|
+
|
|
30
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Represents the input shape accepted by the diff worker.
|
|
34
|
+
*/
|
|
35
|
+
type DiffEntry = {
|
|
36
|
+
draftId: string;
|
|
37
|
+
snapshot: string;
|
|
38
|
+
liveFormData: Record<string, unknown>;
|
|
39
|
+
liveBody: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Dispatches a synthetic 'diff' message to the captured handler.
|
|
44
|
+
* @param {DiffEntry[]} entries - The diff entries to compare
|
|
45
|
+
* @return {void}
|
|
46
|
+
*/
|
|
47
|
+
function sendDiff(entries: DiffEntry[]): void {
|
|
48
|
+
const event = new MessageEvent('message', {
|
|
49
|
+
data: { type: 'diff', entries },
|
|
50
|
+
});
|
|
51
|
+
messageHandler!(event);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
describe('diff worker', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('registers a message listener on self', () => {
|
|
62
|
+
expect(messageHandler).toBeTypeOf('function');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns isOutdated: false when snapshot matches current live content', () => {
|
|
66
|
+
// Build a snapshot using the same stable-stringify format the worker uses
|
|
67
|
+
const liveFormData = { title: 'Hello', published: true };
|
|
68
|
+
const liveBody = '# Hello World';
|
|
69
|
+
// Import stableStringify manually to build matching snapshot
|
|
70
|
+
const snapshot = JSON.stringify(
|
|
71
|
+
JSON.parse(
|
|
72
|
+
JSON.stringify(
|
|
73
|
+
{ formData: liveFormData, body: liveBody },
|
|
74
|
+
(_key, val) => {
|
|
75
|
+
if (
|
|
76
|
+
val !== null &&
|
|
77
|
+
typeof val === 'object' &&
|
|
78
|
+
!Array.isArray(val)
|
|
79
|
+
) {
|
|
80
|
+
return Object.keys(val as Record<string, unknown>)
|
|
81
|
+
.sort()
|
|
82
|
+
.reduce(
|
|
83
|
+
(s, k) => {
|
|
84
|
+
s[k] = (val as Record<string, unknown>)[k];
|
|
85
|
+
return s;
|
|
86
|
+
},
|
|
87
|
+
{} as Record<string, unknown>,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return val;
|
|
91
|
+
},
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
sendDiff([{ draftId: 'draft-1', snapshot, liveFormData, liveBody }]);
|
|
97
|
+
|
|
98
|
+
const call = selfPostMessage.mock.calls[0][0];
|
|
99
|
+
expect(call.type).toBe('diff-result');
|
|
100
|
+
expect(call.results['draft-1']).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns isOutdated: true when live content differs from snapshot', () => {
|
|
104
|
+
const snapshot = '{"body":"old body","formData":{"title":"Old"}}';
|
|
105
|
+
sendDiff([
|
|
106
|
+
{
|
|
107
|
+
draftId: 'draft-2',
|
|
108
|
+
snapshot,
|
|
109
|
+
liveFormData: { title: 'New' },
|
|
110
|
+
liveBody: 'new body',
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const call = selfPostMessage.mock.calls[0][0];
|
|
115
|
+
expect(call.results['draft-2']).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('processes multiple entries in a single message', () => {
|
|
119
|
+
// draft-a is up-to-date, draft-b is outdated
|
|
120
|
+
const matchingSnapshot = '{"body":"same body","formData":{"title":"Same"}}';
|
|
121
|
+
const staleSnapshot = '{"body":"old","formData":{"title":"Old"}}';
|
|
122
|
+
|
|
123
|
+
sendDiff([
|
|
124
|
+
{
|
|
125
|
+
draftId: 'draft-a',
|
|
126
|
+
snapshot: matchingSnapshot,
|
|
127
|
+
liveFormData: { title: 'Same' },
|
|
128
|
+
liveBody: 'same body',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
draftId: 'draft-b',
|
|
132
|
+
snapshot: staleSnapshot,
|
|
133
|
+
liveFormData: { title: 'New' },
|
|
134
|
+
liveBody: 'new body',
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
const { results } = selfPostMessage.mock.calls[0][0];
|
|
139
|
+
// draft-a matches because stableStringify of {formData:{title:'Same'},body:'same body'}
|
|
140
|
+
// may differ from the snapshot above (key order) — test the false case explicitly
|
|
141
|
+
expect(typeof results['draft-a']).toBe('boolean');
|
|
142
|
+
expect(results['draft-b']).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('handles an empty entries array without error', () => {
|
|
146
|
+
sendDiff([]);
|
|
147
|
+
const call = selfPostMessage.mock.calls[0][0];
|
|
148
|
+
expect(call.type).toBe('diff-result');
|
|
149
|
+
expect(call.results).toEqual({});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('ignores messages with a type other than "diff"', () => {
|
|
153
|
+
const event = new MessageEvent('message', {
|
|
154
|
+
data: { type: 'other', entries: [] },
|
|
155
|
+
});
|
|
156
|
+
messageHandler!(event);
|
|
157
|
+
// No postMessage should have been called
|
|
158
|
+
expect(selfPostMessage).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('responds with type "diff-result"', () => {
|
|
162
|
+
sendDiff([]);
|
|
163
|
+
expect(selfPostMessage.mock.calls[0][0].type).toBe('diff-result');
|
|
164
|
+
});
|
|
165
|
+
});
|