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,166 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
FileEntry,
|
|
4
|
+
FileWrite,
|
|
5
|
+
StorageAdapter,
|
|
6
|
+
} from '../../../src/client/js/storage/adapter';
|
|
7
|
+
|
|
8
|
+
//////////////////////////////
|
|
9
|
+
// Sample content for pre-populated collections
|
|
10
|
+
//////////////////////////////
|
|
11
|
+
|
|
12
|
+
/** Sample posts used to pre-populate the mock adapter. */
|
|
13
|
+
const SAMPLE_POSTS: FileEntry[] = [
|
|
14
|
+
{
|
|
15
|
+
filename: 'hello-world.md',
|
|
16
|
+
content: [
|
|
17
|
+
'---',
|
|
18
|
+
'title: Hello World',
|
|
19
|
+
'published: 2024-01-15T10:00:00Z',
|
|
20
|
+
'draft: false',
|
|
21
|
+
'---',
|
|
22
|
+
'',
|
|
23
|
+
'Welcome to the blog. This is the first post.',
|
|
24
|
+
].join('\n'),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
filename: 'second-post.md',
|
|
28
|
+
content: [
|
|
29
|
+
'---',
|
|
30
|
+
'title: Second Post',
|
|
31
|
+
'published: 2024-02-20T14:30:00Z',
|
|
32
|
+
'draft: false',
|
|
33
|
+
'---',
|
|
34
|
+
'',
|
|
35
|
+
'This is the second post with more content.',
|
|
36
|
+
].join('\n'),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
filename: 'draft-ideas.md',
|
|
40
|
+
content: [
|
|
41
|
+
'---',
|
|
42
|
+
'title: Draft Ideas',
|
|
43
|
+
'published: 2024-03-01T09:00:00Z',
|
|
44
|
+
'draft: true',
|
|
45
|
+
'---',
|
|
46
|
+
'',
|
|
47
|
+
'Some ideas for future posts.',
|
|
48
|
+
].join('\n'),
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/** Sample pages for a second collection. */
|
|
53
|
+
const SAMPLE_PAGES: FileEntry[] = [
|
|
54
|
+
{
|
|
55
|
+
filename: 'about.md',
|
|
56
|
+
content: ['---', 'title: About', '---', '', 'This is the about page.'].join(
|
|
57
|
+
'\n',
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates an in-memory StorageAdapter backed by a Map. Pre-populated with
|
|
64
|
+
* sample "posts" and "pages" collections containing markdown files with
|
|
65
|
+
* YAML frontmatter.
|
|
66
|
+
* @return {{ adapter: StorageAdapter, store: Map<string, Map<string, string>> }} The adapter and its backing store for assertions
|
|
67
|
+
*/
|
|
68
|
+
export function createMockAdapter(): {
|
|
69
|
+
adapter: StorageAdapter;
|
|
70
|
+
store: Map<string, Map<string, string>>;
|
|
71
|
+
} {
|
|
72
|
+
// Outer map: collection name -> inner map (filename -> content)
|
|
73
|
+
const store = new Map<string, Map<string, string>>();
|
|
74
|
+
|
|
75
|
+
// Pre-populate posts
|
|
76
|
+
const postsMap = new Map<string, string>();
|
|
77
|
+
for (const entry of SAMPLE_POSTS) {
|
|
78
|
+
postsMap.set(entry.filename, entry.content);
|
|
79
|
+
}
|
|
80
|
+
store.set('posts', postsMap);
|
|
81
|
+
|
|
82
|
+
// Pre-populate pages
|
|
83
|
+
const pagesMap = new Map<string, string>();
|
|
84
|
+
for (const entry of SAMPLE_PAGES) {
|
|
85
|
+
pagesMap.set(entry.filename, entry.content);
|
|
86
|
+
}
|
|
87
|
+
store.set('pages', pagesMap);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Gets or creates the inner map for a collection.
|
|
91
|
+
* @param {string} collection - The collection name
|
|
92
|
+
* @return {Map<string, string>} The file map for the collection
|
|
93
|
+
*/
|
|
94
|
+
function getCollection(collection: string): Map<string, string> {
|
|
95
|
+
let col = store.get(collection);
|
|
96
|
+
if (!col) {
|
|
97
|
+
col = new Map<string, string>();
|
|
98
|
+
store.set(collection, col);
|
|
99
|
+
}
|
|
100
|
+
return col;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const adapter: StorageAdapter = {
|
|
104
|
+
listFiles: vi.fn(
|
|
105
|
+
async (
|
|
106
|
+
collection: string,
|
|
107
|
+
extensions: string[],
|
|
108
|
+
): Promise<FileEntry[]> => {
|
|
109
|
+
const col = store.get(collection);
|
|
110
|
+
if (!col) return [];
|
|
111
|
+
return Array.from(col.entries())
|
|
112
|
+
.filter(([filename]) =>
|
|
113
|
+
extensions.some((ext) => filename.endsWith(ext)),
|
|
114
|
+
)
|
|
115
|
+
.map(([filename, content]) => ({
|
|
116
|
+
filename,
|
|
117
|
+
content,
|
|
118
|
+
}));
|
|
119
|
+
},
|
|
120
|
+
),
|
|
121
|
+
|
|
122
|
+
readFile: vi.fn(
|
|
123
|
+
async (collection: string, filename: string): Promise<string> => {
|
|
124
|
+
const col = store.get(collection);
|
|
125
|
+
if (!col) throw new Error(`Collection "${collection}" not found`);
|
|
126
|
+
const content = col.get(filename);
|
|
127
|
+
if (content === undefined)
|
|
128
|
+
throw new Error(`File "${filename}" not found in "${collection}"`);
|
|
129
|
+
return content;
|
|
130
|
+
},
|
|
131
|
+
),
|
|
132
|
+
|
|
133
|
+
writeFile: vi.fn(
|
|
134
|
+
async (
|
|
135
|
+
collection: string,
|
|
136
|
+
filename: string,
|
|
137
|
+
content: string,
|
|
138
|
+
): Promise<void> => {
|
|
139
|
+
const col = getCollection(collection);
|
|
140
|
+
col.set(filename, content);
|
|
141
|
+
},
|
|
142
|
+
),
|
|
143
|
+
|
|
144
|
+
writeFiles: vi.fn(async (files: FileWrite[]): Promise<void> => {
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const col = getCollection(file.collection);
|
|
147
|
+
col.set(file.filename, file.content);
|
|
148
|
+
}
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
deleteFile: vi.fn(
|
|
152
|
+
async (collection: string, filename: string): Promise<void> => {
|
|
153
|
+
const col = store.get(collection);
|
|
154
|
+
if (!col) throw new Error(`Collection "${collection}" not found`);
|
|
155
|
+
if (!col.has(filename))
|
|
156
|
+
throw new Error(`File "${filename}" not found in "${collection}"`);
|
|
157
|
+
col.delete(filename);
|
|
158
|
+
},
|
|
159
|
+
),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return { adapter, store };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Exported sample data for test assertions. */
|
|
166
|
+
export { SAMPLE_POSTS, SAMPLE_PAGES };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { Mock } from 'vitest';
|
|
2
|
+
|
|
3
|
+
//////////////////////////////
|
|
4
|
+
// Mock state type
|
|
5
|
+
//
|
|
6
|
+
// Each test file creates its own mocks via vi.hoisted(). This type
|
|
7
|
+
// describes the shape so helper functions can manipulate mock return
|
|
8
|
+
// values without being tightly coupled to any single test file.
|
|
9
|
+
// Mock names match the getter properties on the exported state objects
|
|
10
|
+
// (backend, content, nav, schema, drafts, editor).
|
|
11
|
+
//////////////////////////////
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shape of the mocks object created by vi.hoisted() in each test file.
|
|
15
|
+
* Each field is a Vitest Mock function controlling a reactive export.
|
|
16
|
+
*/
|
|
17
|
+
export type E2EMocks = {
|
|
18
|
+
mockBackendReady: Mock<() => boolean>;
|
|
19
|
+
mockRoute: Mock;
|
|
20
|
+
mockCollections: Mock<() => string[]>;
|
|
21
|
+
mockContentList: Mock;
|
|
22
|
+
mockLoading: Mock<() => boolean>;
|
|
23
|
+
mockError: Mock<() => string | null>;
|
|
24
|
+
mockDrafts: Mock;
|
|
25
|
+
mockOutdatedMap: Mock<() => Record<string, boolean>>;
|
|
26
|
+
mockActiveTab: Mock<() => string>;
|
|
27
|
+
mockGetEditorFile: Mock;
|
|
28
|
+
mockSchema: Mock;
|
|
29
|
+
mockCollectionHasDates: Mock<() => boolean>;
|
|
30
|
+
mockComputePublishDisabled: Mock<() => boolean>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resets all mock return values to their disconnected defaults.
|
|
35
|
+
* Call in beforeEach() to avoid state leaking between tests.
|
|
36
|
+
* @param {E2EMocks} m - The mocks object to reset
|
|
37
|
+
* @return {void}
|
|
38
|
+
*/
|
|
39
|
+
export function resetMocks(m: E2EMocks): void {
|
|
40
|
+
m.mockBackendReady.mockReturnValue(false);
|
|
41
|
+
m.mockRoute.mockReturnValue({ view: 'home' });
|
|
42
|
+
m.mockCollections.mockReturnValue([]);
|
|
43
|
+
m.mockContentList.mockReturnValue([]);
|
|
44
|
+
m.mockLoading.mockReturnValue(false);
|
|
45
|
+
m.mockError.mockReturnValue(null);
|
|
46
|
+
m.mockDrafts.mockReturnValue([]);
|
|
47
|
+
m.mockOutdatedMap.mockReturnValue({});
|
|
48
|
+
m.mockActiveTab.mockReturnValue('metadata');
|
|
49
|
+
m.mockGetEditorFile.mockReturnValue(null);
|
|
50
|
+
m.mockSchema.mockReturnValue(null);
|
|
51
|
+
m.mockCollectionHasDates.mockReturnValue(false);
|
|
52
|
+
m.mockComputePublishDisabled.mockReturnValue(false);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Configures mocks to show the backend as connected with collections visible.
|
|
57
|
+
* @param {E2EMocks} m - The mocks object
|
|
58
|
+
* @param {string[]} collections - Collection names to show
|
|
59
|
+
* @return {void}
|
|
60
|
+
*/
|
|
61
|
+
export function configureConnected(
|
|
62
|
+
m: E2EMocks,
|
|
63
|
+
collections: string[] = ['pages', 'posts'],
|
|
64
|
+
): void {
|
|
65
|
+
m.mockBackendReady.mockReturnValue(true);
|
|
66
|
+
m.mockCollections.mockReturnValue(collections);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Configures mocks to show a collection selected with content items.
|
|
71
|
+
* @param {E2EMocks} m - The mocks object
|
|
72
|
+
* @param {string} collection - The active collection name
|
|
73
|
+
* @param {Array<{ filename: string, data: Record<string, unknown> }>} items - Content items
|
|
74
|
+
* @return {void}
|
|
75
|
+
*/
|
|
76
|
+
export function configureCollection(
|
|
77
|
+
m: E2EMocks,
|
|
78
|
+
collection: string,
|
|
79
|
+
items: Array<{ filename: string; data: Record<string, unknown> }> = [],
|
|
80
|
+
): void {
|
|
81
|
+
configureConnected(m);
|
|
82
|
+
m.mockRoute.mockReturnValue({ view: 'collection', collection });
|
|
83
|
+
m.mockContentList.mockReturnValue(items);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Configures mocks to show a file open in the editor.
|
|
88
|
+
* @param {E2EMocks} m - The mocks object
|
|
89
|
+
* @param {string} collection - The active collection
|
|
90
|
+
* @param {string} slug - The file slug (filename without extension)
|
|
91
|
+
* @param {{ filename: string, body: string, formData: Record<string, unknown>, dirty?: boolean, draftId?: string | null, isNewDraft?: boolean }} file - Editor file state
|
|
92
|
+
* @return {void}
|
|
93
|
+
*/
|
|
94
|
+
export function configureFileOpen(
|
|
95
|
+
m: E2EMocks,
|
|
96
|
+
collection: string,
|
|
97
|
+
slug: string,
|
|
98
|
+
file: {
|
|
99
|
+
filename: string;
|
|
100
|
+
body: string;
|
|
101
|
+
formData: Record<string, unknown>;
|
|
102
|
+
dirty?: boolean;
|
|
103
|
+
draftId?: string | null;
|
|
104
|
+
isNewDraft?: boolean;
|
|
105
|
+
},
|
|
106
|
+
): void {
|
|
107
|
+
configureConnected(m);
|
|
108
|
+
m.mockRoute.mockReturnValue({ view: 'file', collection, slug });
|
|
109
|
+
m.mockContentList.mockReturnValue([
|
|
110
|
+
{ filename: file.filename, data: file.formData },
|
|
111
|
+
]);
|
|
112
|
+
m.mockGetEditorFile.mockReturnValue({
|
|
113
|
+
filename: file.filename,
|
|
114
|
+
body: file.body,
|
|
115
|
+
formData: file.formData,
|
|
116
|
+
dirty: file.dirty ?? false,
|
|
117
|
+
saving: false,
|
|
118
|
+
bodyLoaded: true,
|
|
119
|
+
draftId: file.draftId ?? null,
|
|
120
|
+
isNewDraft: file.isNewDraft ?? false,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Configures mocks to show a draft open in the editor.
|
|
126
|
+
* @param {E2EMocks} m - The mocks object
|
|
127
|
+
* @param {string} collection - The active collection
|
|
128
|
+
* @param {string} draftId - The draft UUID
|
|
129
|
+
* @param {{ body: string, formData: Record<string, unknown>, dirty?: boolean, filename?: string }} draft - Draft state
|
|
130
|
+
* @return {void}
|
|
131
|
+
*/
|
|
132
|
+
export function configureDraftOpen(
|
|
133
|
+
m: E2EMocks,
|
|
134
|
+
collection: string,
|
|
135
|
+
draftId: string,
|
|
136
|
+
draft: {
|
|
137
|
+
body: string;
|
|
138
|
+
formData: Record<string, unknown>;
|
|
139
|
+
dirty?: boolean;
|
|
140
|
+
filename?: string;
|
|
141
|
+
},
|
|
142
|
+
): void {
|
|
143
|
+
configureConnected(m);
|
|
144
|
+
m.mockRoute.mockReturnValue({ view: 'draft', collection, draftId });
|
|
145
|
+
m.mockGetEditorFile.mockReturnValue({
|
|
146
|
+
filename: draft.filename ?? '',
|
|
147
|
+
body: draft.body,
|
|
148
|
+
formData: draft.formData,
|
|
149
|
+
dirty: draft.dirty ?? false,
|
|
150
|
+
saving: false,
|
|
151
|
+
bodyLoaded: true,
|
|
152
|
+
draftId,
|
|
153
|
+
isNewDraft: true,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
|
2
|
+
import { render, cleanup } from '@testing-library/svelte';
|
|
3
|
+
import {
|
|
4
|
+
resetMocks,
|
|
5
|
+
configureConnected,
|
|
6
|
+
configureCollection,
|
|
7
|
+
configureFileOpen,
|
|
8
|
+
} from './helpers/test-app';
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
//////////////////////////////
|
|
12
|
+
// Hoisted mocks
|
|
13
|
+
//////////////////////////////
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const mocks = vi.hoisted(() => ({
|
|
17
|
+
mockBackendReady: vi.fn(() => false),
|
|
18
|
+
mockRoute: vi.fn(() => ({ view: 'home' as const })),
|
|
19
|
+
mockCollections: vi.fn(() => [] as string[]),
|
|
20
|
+
mockContentList: vi.fn(
|
|
21
|
+
() => [] as Array<{ filename: string; data: Record<string, unknown> }>,
|
|
22
|
+
),
|
|
23
|
+
mockLoading: vi.fn(() => false),
|
|
24
|
+
mockError: vi.fn(() => null as string | null),
|
|
25
|
+
mockDrafts: vi.fn(() => []),
|
|
26
|
+
mockOutdatedMap: vi.fn(() => ({}) as Record<string, boolean>),
|
|
27
|
+
mockActiveTab: vi.fn(() => 'metadata'),
|
|
28
|
+
mockGetEditorFile: vi.fn(() => null),
|
|
29
|
+
mockSchema: vi.fn(() => null),
|
|
30
|
+
mockCollectionHasDates: vi.fn(() => false),
|
|
31
|
+
mockComputePublishDisabled: vi.fn(() => false),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
//////////////////////////////
|
|
36
|
+
// Module mocks
|
|
37
|
+
//////////////////////////////
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
vi.mock('virtual:nebula/collections', () => ({
|
|
41
|
+
default: {
|
|
42
|
+
pages: '/collections/pages.schema.json',
|
|
43
|
+
posts: '/collections/posts.schema.json',
|
|
44
|
+
},
|
|
45
|
+
}));
|
|
46
|
+
vi.mock('virtual:nebula/config', () => ({
|
|
47
|
+
default: { basePath: '/admin', collectionsPath: '/collections' },
|
|
48
|
+
}));
|
|
49
|
+
vi.mock('../../src/client/js/state/state.svelte', () => ({
|
|
50
|
+
backend: {
|
|
51
|
+
get type() {
|
|
52
|
+
return null;
|
|
53
|
+
},
|
|
54
|
+
get ready() {
|
|
55
|
+
return mocks.mockBackendReady();
|
|
56
|
+
},
|
|
57
|
+
get permission() {
|
|
58
|
+
return 'denied';
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
content: {
|
|
62
|
+
get list() {
|
|
63
|
+
return mocks.mockContentList();
|
|
64
|
+
},
|
|
65
|
+
get loading() {
|
|
66
|
+
return mocks.mockLoading();
|
|
67
|
+
},
|
|
68
|
+
get error() {
|
|
69
|
+
return mocks.mockError();
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
get collections() {
|
|
73
|
+
return mocks.mockCollections();
|
|
74
|
+
},
|
|
75
|
+
storageClient: null,
|
|
76
|
+
drafts: {
|
|
77
|
+
get all() {
|
|
78
|
+
return mocks.mockDrafts();
|
|
79
|
+
},
|
|
80
|
+
get outdated() {
|
|
81
|
+
return mocks.mockOutdatedMap();
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
restoreBackend: vi.fn(async () => {}),
|
|
85
|
+
loadCollection: vi.fn(),
|
|
86
|
+
reloadCollection: vi.fn(),
|
|
87
|
+
disconnect: vi.fn(),
|
|
88
|
+
refreshDrafts: vi.fn(async () => {}),
|
|
89
|
+
updateContentItem: vi.fn(),
|
|
90
|
+
pickDirectory: vi.fn(),
|
|
91
|
+
requestPermission: vi.fn(),
|
|
92
|
+
connectGitHub: vi.fn(async () => {}),
|
|
93
|
+
}));
|
|
94
|
+
vi.mock('../../src/client/js/state/router.svelte', () => ({
|
|
95
|
+
initRouter: vi.fn(),
|
|
96
|
+
nav: {
|
|
97
|
+
get route() {
|
|
98
|
+
return mocks.mockRoute();
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
navigate: vi.fn(),
|
|
102
|
+
registerDirtyChecker: vi.fn(),
|
|
103
|
+
adminPath: vi.fn((...segments) =>
|
|
104
|
+
segments.length === 0 ? '/admin' : '/admin/' + segments.join('/'),
|
|
105
|
+
),
|
|
106
|
+
}));
|
|
107
|
+
vi.mock('../../src/client/js/state/schema.svelte', () => ({
|
|
108
|
+
fetchSchema: vi.fn(async () => {}),
|
|
109
|
+
schema: {
|
|
110
|
+
get active() {
|
|
111
|
+
return mocks.mockSchema();
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
clearSchema: vi.fn(),
|
|
115
|
+
prefetchAllSchemas: vi.fn(async () => {}),
|
|
116
|
+
collectionHasDates: mocks.mockCollectionHasDates,
|
|
117
|
+
getCollectionTitle: vi.fn(() => null),
|
|
118
|
+
getCollectionDescription: vi.fn(() => null),
|
|
119
|
+
}));
|
|
120
|
+
vi.mock('../../src/client/js/editor/editor.svelte', () => ({
|
|
121
|
+
preloadFile: vi.fn(async () => {}),
|
|
122
|
+
loadFileBody: vi.fn(async () => {}),
|
|
123
|
+
clearEditor: vi.fn(),
|
|
124
|
+
editor: {
|
|
125
|
+
get tab() {
|
|
126
|
+
return mocks.mockActiveTab();
|
|
127
|
+
},
|
|
128
|
+
get data() {
|
|
129
|
+
return {};
|
|
130
|
+
},
|
|
131
|
+
get originalFilename() {
|
|
132
|
+
return '';
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
setActiveTab: vi.fn(),
|
|
136
|
+
getEditorFile: mocks.mockGetEditorFile,
|
|
137
|
+
loadDraftById: vi.fn(async () => {}),
|
|
138
|
+
setFilename: vi.fn(),
|
|
139
|
+
updateBody: vi.fn(),
|
|
140
|
+
updateFormField: vi.fn(),
|
|
141
|
+
saveDraftToIDB: vi.fn(async () => {}),
|
|
142
|
+
saveFile: vi.fn(async () => {}),
|
|
143
|
+
publishFile: vi.fn(async () => {}),
|
|
144
|
+
deleteCurrentDraft: vi.fn(async () => {}),
|
|
145
|
+
applyEditorState: vi.fn(),
|
|
146
|
+
_getDraftState: vi.fn(() => ({})),
|
|
147
|
+
_setDraftState: vi.fn(),
|
|
148
|
+
changeFileFormat: vi.fn(),
|
|
149
|
+
setDefaultFormat: vi.fn(),
|
|
150
|
+
}));
|
|
151
|
+
vi.mock('../../src/client/js/handlers/admin', async (importOriginal) => {
|
|
152
|
+
const actual =
|
|
153
|
+
await importOriginal<typeof import('../../src/client/js/handlers/admin')>();
|
|
154
|
+
return {
|
|
155
|
+
...actual,
|
|
156
|
+
handleSave: vi.fn(async () => {}),
|
|
157
|
+
handlePublish: vi.fn(async () => ({ status: 'ok' })),
|
|
158
|
+
handleDeleteDraft: vi.fn(async () => {}),
|
|
159
|
+
handleFilenameConfirm: vi.fn(async () => {}),
|
|
160
|
+
computePublishDisabled: mocks.mockComputePublishDisabled,
|
|
161
|
+
// Override buildCollectionItems to read from mockCollections — the
|
|
162
|
+
// module-level getter for `collections` is not a live binding in
|
|
163
|
+
// vitest's browser mode, so the real function would see an empty array.
|
|
164
|
+
buildCollectionItems: () =>
|
|
165
|
+
mocks.mockCollections().map((name: string) => ({
|
|
166
|
+
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
167
|
+
href: '/admin/' + name,
|
|
168
|
+
})),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
vi.mock('../../src/client/js/utils/sort', () => ({
|
|
172
|
+
toSortDate: vi.fn(() => undefined),
|
|
173
|
+
readSortMode: vi.fn(() => 'alpha'),
|
|
174
|
+
writeSortMode: vi.fn(),
|
|
175
|
+
createComparator: vi.fn(() => () => 0),
|
|
176
|
+
SORT_MODES: {
|
|
177
|
+
alpha: { icon: 'sort_by_alpha', label: 'Alphabetical' },
|
|
178
|
+
'date-asc': { icon: 'hourglass_arrow_down', label: 'Oldest first' },
|
|
179
|
+
'date-desc': { icon: 'hourglass_arrow_up', label: 'Newest first' },
|
|
180
|
+
},
|
|
181
|
+
SORT_ORDER: ['alpha', 'date-asc', 'date-desc'],
|
|
182
|
+
}));
|
|
183
|
+
vi.mock('../../src/client/js/drafts/storage', () => ({
|
|
184
|
+
saveDraft: vi.fn(async () => {}),
|
|
185
|
+
getDraftByFile: vi.fn(async () => null),
|
|
186
|
+
loadDrafts: vi.fn(async () => []),
|
|
187
|
+
loadDraft: vi.fn(async () => null),
|
|
188
|
+
deleteDraft: vi.fn(async () => {}),
|
|
189
|
+
}));
|
|
190
|
+
vi.mock('../../src/client/js/utils/schema-utils', () => ({
|
|
191
|
+
extractTabs: vi.fn(() => []),
|
|
192
|
+
getFieldsForTab: vi.fn(() => []),
|
|
193
|
+
resolveFieldType: vi.fn(() => ({ kind: 'string' })),
|
|
194
|
+
createDefaultValue: vi.fn(() => ''),
|
|
195
|
+
getByPath: vi.fn(),
|
|
196
|
+
setByPath: vi.fn(),
|
|
197
|
+
isReadOnly: vi.fn(() => false),
|
|
198
|
+
isNullable: vi.fn(() => false),
|
|
199
|
+
getProperties: vi.fn(
|
|
200
|
+
(schema: Record<string, unknown>) => schema['properties'],
|
|
201
|
+
),
|
|
202
|
+
getRequiredFields: vi.fn((schema: Record<string, unknown>) =>
|
|
203
|
+
Array.isArray(schema['required']) ? schema['required'] : [],
|
|
204
|
+
),
|
|
205
|
+
getLabel: vi.fn((schema: Record<string, unknown>, name: string) =>
|
|
206
|
+
typeof schema['title'] === 'string' ? schema['title'] : name,
|
|
207
|
+
),
|
|
208
|
+
}));
|
|
209
|
+
vi.mock('../../src/client/js/drafts/merge.svelte', () => ({
|
|
210
|
+
drafts: {
|
|
211
|
+
get all() {
|
|
212
|
+
return mocks.mockDrafts();
|
|
213
|
+
},
|
|
214
|
+
get outdated() {
|
|
215
|
+
return mocks.mockOutdatedMap();
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
mergeDrafts: vi.fn(async () => {}),
|
|
219
|
+
refreshDrafts: vi.fn(async () => {}),
|
|
220
|
+
resetDraftMerge: vi.fn(),
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
vi.mock('../../src/client/js/state/theme.svelte', () => ({
|
|
224
|
+
initTheme: vi.fn(() => () => {}),
|
|
225
|
+
cycleTheme: vi.fn(),
|
|
226
|
+
theme: { resolved: 'dark', icon: 'brightness_auto', label: 'Auto' },
|
|
227
|
+
}));
|
|
228
|
+
vi.mock('../../src/client/js/state/dialogs.svelte', () => ({
|
|
229
|
+
dialog: {
|
|
230
|
+
get active() {
|
|
231
|
+
return null;
|
|
232
|
+
},
|
|
233
|
+
open: vi.fn(),
|
|
234
|
+
close: vi.fn(),
|
|
235
|
+
},
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
import Admin from '../../src/client/Admin.svelte';
|
|
239
|
+
|
|
240
|
+
afterEach(() => cleanup());
|
|
241
|
+
beforeEach(() => resetMocks(mocks));
|
|
242
|
+
|
|
243
|
+
describe('Navigation', () => {
|
|
244
|
+
it('shows both sidebars when a collection is selected', () => {
|
|
245
|
+
configureCollection(mocks, 'posts');
|
|
246
|
+
|
|
247
|
+
const { container } = render(Admin);
|
|
248
|
+
|
|
249
|
+
const sidebars = container.querySelectorAll('.sidebar');
|
|
250
|
+
expect(sidebars.length).toBe(2);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('does not render editor area in collection view', () => {
|
|
254
|
+
configureCollection(mocks, 'posts');
|
|
255
|
+
|
|
256
|
+
const { container } = render(Admin);
|
|
257
|
+
|
|
258
|
+
expect(container.querySelector('.editor-area')).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('shows content items in the collection sidebar', () => {
|
|
262
|
+
configureCollection(mocks, 'posts', [
|
|
263
|
+
{ filename: 'hello-world.md', data: { title: 'Hello World' } },
|
|
264
|
+
{ filename: 'second-post.md', data: { title: 'Second Post' } },
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
const { container } = render(Admin);
|
|
268
|
+
|
|
269
|
+
// Second sidebar has the content items
|
|
270
|
+
const sidebars = container.querySelectorAll('.sidebar');
|
|
271
|
+
const contentSidebar = sidebars[1];
|
|
272
|
+
const links = contentSidebar.querySelectorAll('.sidebar-link');
|
|
273
|
+
expect(links.length).toBe(2);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('renders content item titles from frontmatter data', () => {
|
|
277
|
+
configureCollection(mocks, 'posts', [
|
|
278
|
+
{ filename: 'hello.md', data: { title: 'Hello World' } },
|
|
279
|
+
{ filename: 'bye.md', data: { title: 'Goodbye' } },
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
const { container } = render(Admin);
|
|
283
|
+
|
|
284
|
+
const sidebars = container.querySelectorAll('.sidebar');
|
|
285
|
+
const links = sidebars[1].querySelectorAll('.sidebar-link');
|
|
286
|
+
const labels = Array.from(links).map((el) =>
|
|
287
|
+
el.querySelector('.item-label-text')?.textContent?.trim(),
|
|
288
|
+
);
|
|
289
|
+
expect(labels).toContain('Hello World');
|
|
290
|
+
expect(labels).toContain('Goodbye');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('falls back to filename when title is missing', () => {
|
|
294
|
+
configureCollection(mocks, 'posts', [
|
|
295
|
+
{ filename: 'no-title.md', data: {} },
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
const { container } = render(Admin);
|
|
299
|
+
|
|
300
|
+
const sidebars = container.querySelectorAll('.sidebar');
|
|
301
|
+
const links = sidebars[1].querySelectorAll('.sidebar-link');
|
|
302
|
+
const label = links[0]
|
|
303
|
+
.querySelector('.item-label-text')
|
|
304
|
+
?.textContent?.trim();
|
|
305
|
+
expect(label).toBe('no-title.md');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('renders editor area when a file is open', () => {
|
|
309
|
+
configureFileOpen(mocks, 'posts', 'hello-world', {
|
|
310
|
+
filename: 'hello-world.md',
|
|
311
|
+
body: 'Hello content',
|
|
312
|
+
formData: { title: 'Hello World' },
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const { container } = render(Admin);
|
|
316
|
+
|
|
317
|
+
expect(container.querySelector('.editor-area')).not.toBeNull();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('applies admin--file-open class when editing a file', () => {
|
|
321
|
+
configureFileOpen(mocks, 'posts', 'hello-world', {
|
|
322
|
+
filename: 'hello-world.md',
|
|
323
|
+
body: 'Hello content',
|
|
324
|
+
formData: { title: 'Hello World' },
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const { container } = render(Admin);
|
|
328
|
+
|
|
329
|
+
expect(container.querySelector('.admin--file-open')).not.toBeNull();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('shows both sidebars and editor area in file view', () => {
|
|
333
|
+
configureFileOpen(mocks, 'posts', 'hello-world', {
|
|
334
|
+
filename: 'hello-world.md',
|
|
335
|
+
body: '',
|
|
336
|
+
formData: { title: 'Hello World' },
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const { container } = render(Admin);
|
|
340
|
+
|
|
341
|
+
const sidebars = container.querySelectorAll('.sidebar');
|
|
342
|
+
expect(sidebars.length).toBe(2);
|
|
343
|
+
expect(container.querySelector('.editor-area')).not.toBeNull();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('highlights active collection in the sidebar via aria-current', () => {
|
|
347
|
+
configureCollection(mocks, 'posts');
|
|
348
|
+
mocks.mockCollections.mockReturnValue(['pages', 'posts']);
|
|
349
|
+
|
|
350
|
+
const { container } = render(Admin);
|
|
351
|
+
|
|
352
|
+
// Active collection is marked with aria-current="page"
|
|
353
|
+
const activeLink = container.querySelector(
|
|
354
|
+
'.sidebar-link[aria-current="page"]',
|
|
355
|
+
);
|
|
356
|
+
expect(activeLink).not.toBeNull();
|
|
357
|
+
});
|
|
358
|
+
});
|