@jant/core 0.3.36 → 0.3.37
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/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4012 -3276
- package/dist/index.js +10285 -5809
- package/package.json +11 -3
- package/src/__tests__/helpers/app.ts +9 -9
- package/src/__tests__/helpers/db.ts +91 -93
- package/src/app.tsx +157 -27
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/client/avatar-upload.ts +3 -2
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
- package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
- package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +7 -9
- package/src/client/components/compose-types.ts +101 -4
- package/src/client/components/jant-collection-form.ts +43 -7
- package/src/client/components/jant-collection-sidebar.ts +88 -84
- package/src/client/components/jant-compose-dialog.ts +1655 -219
- package/src/client/components/jant-compose-editor.ts +732 -168
- package/src/client/components/jant-compose-fullscreen.ts +23 -78
- package/src/client/components/jant-media-lightbox.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +24 -284
- package/src/client/components/jant-post-form.ts +89 -9
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/client/components/jant-settings-avatar.ts +3 -3
- package/src/client/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/client/components/nav-manager-types.ts +4 -19
- package/src/client/components/post-form-template.ts +107 -12
- package/src/client/components/post-form-types.ts +11 -4
- package/src/client/compose-bridge.ts +410 -109
- package/src/client/image-processor.ts +26 -8
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/client/post-form-bridge.ts +52 -1
- package/src/client/settings-bridge.ts +0 -12
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/create-editor.ts +46 -0
- package/src/client/tiptap/extensions.ts +5 -0
- package/src/client/tiptap/image-node.ts +2 -8
- package/src/client/tiptap/paste-image.ts +2 -13
- package/src/client/tiptap/slash-commands.ts +173 -63
- package/src/client/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +15 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +5 -2
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -145
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +487 -1217
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +613 -996
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +624 -1007
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/schemas.test.ts +181 -63
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/view.test.ts +141 -66
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +885 -68
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +78 -32
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +12 -3
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +20 -2
- package/src/lib/resolve-config.ts +6 -2
- package/src/lib/schemas.ts +224 -55
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +66 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +74 -34
- package/src/lib/tiptap-render.ts +5 -10
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +190 -29
- package/src/lib/url.ts +31 -0
- package/src/lib/view.ts +204 -37
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +45 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +51 -42
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +43 -39
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +85 -19
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/setup.tsx +26 -33
- package/src/routes/auth/signin.tsx +3 -7
- package/src/routes/compose.tsx +10 -55
- package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +304 -232
- package/src/routes/feed/__tests__/rss.test.ts +27 -28
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +41 -22
- package/src/routes/pages/archive.tsx +175 -39
- package/src/routes/pages/collection.tsx +22 -10
- package/src/routes/pages/collections.tsx +3 -3
- package/src/routes/pages/featured.tsx +28 -4
- package/src/routes/pages/home.tsx +16 -15
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +713 -234
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +687 -154
- package/src/services/search.ts +160 -75
- package/src/styles/components.css +58 -7
- package/src/styles/tokens.css +84 -6
- package/src/styles/ui.css +2964 -457
- package/src/types/bindings.ts +4 -1
- package/src/types/config.ts +12 -4
- package/src/types/constants.ts +15 -3
- package/src/types/entities.ts +74 -35
- package/src/types/operations.ts +11 -24
- package/src/types/props.ts +51 -16
- package/src/types/views.ts +45 -22
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +239 -17
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +3 -1
- package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
- package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
- package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
- package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +3 -57
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +8 -0
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +105 -92
- package/src/ui/pages/ArchivePage.tsx +923 -98
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +181 -37
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +47 -37
- package/src/ui/shared/MediaGallery.tsx +445 -149
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/dist/client/assets/url-8Dj-5CLW.js +0 -1
- package/src/client/media-upload.ts +0 -161
- package/src/client/page-slug-bridge.ts +0 -42
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/index.tsx +0 -109
- package/src/routes/dash/media.tsx +0 -135
- package/src/routes/dash/pages.tsx +0 -245
- package/src/routes/dash/posts.tsx +0 -338
- package/src/routes/dash/redirects.tsx +0 -263
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -216
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/ui/dash/PageForm.tsx +0 -187
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/media/MediaListContent.tsx +0 -206
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -75
- package/src/ui/dash/posts/PostForm.tsx +0 -260
- package/src/ui/layouts/DashLayout.tsx +0 -247
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
3
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
4
4
|
import type {
|
|
5
5
|
ComposeLabels,
|
|
6
6
|
ComposeCollection,
|
|
@@ -48,6 +48,7 @@ const labels: ComposeLabels = {
|
|
|
48
48
|
collection: "Collection",
|
|
49
49
|
searchCollections: "Search...",
|
|
50
50
|
noCollections: "No collections found.",
|
|
51
|
+
emptyCollections: "Create a collection to get started.",
|
|
51
52
|
post: "Post",
|
|
52
53
|
addAlt: "+ ALT",
|
|
53
54
|
addAltTitle: "Add alt text",
|
|
@@ -56,12 +57,59 @@ const labels: ComposeLabels = {
|
|
|
56
57
|
addMore: "Add",
|
|
57
58
|
uploading: "Uploading...",
|
|
58
59
|
published: "Published!",
|
|
59
|
-
|
|
60
|
+
view: "View",
|
|
61
|
+
retryAll: "Tap to retry",
|
|
62
|
+
editPost: "Edit post",
|
|
63
|
+
update: "Done",
|
|
64
|
+
confirmCloseTitle: "Save to drafts?",
|
|
65
|
+
confirmCloseSubtitle: "Save to drafts to edit and post at a later time.",
|
|
66
|
+
confirmCloseSave: "Save",
|
|
67
|
+
confirmCloseCancel: "Cancel",
|
|
68
|
+
confirmCloseDiscard: "Don't save",
|
|
69
|
+
confirmEditTitle: "You have unsaved changes",
|
|
70
|
+
confirmEditSubtitle: "Do you want to publish your changes or discard them?",
|
|
71
|
+
confirmEditPublish: "Publish",
|
|
72
|
+
confirmEditDiscard: "Discard",
|
|
73
|
+
drafts: "Drafts",
|
|
74
|
+
draftsEmpty: "No drafts yet. Save a draft to find it here.",
|
|
75
|
+
deleteDraft: "Delete Draft",
|
|
76
|
+
draftDeleted: "Draft deleted.",
|
|
77
|
+
publishFailedDraft: "Couldn't publish. Saved as draft.",
|
|
78
|
+
uploadFailedDraft: "Some uploads failed. Saved as draft.",
|
|
79
|
+
addCollection: "Add Collection",
|
|
80
|
+
collectionCountLabel: "%name% + %count% more",
|
|
81
|
+
draftRestored: "Draft restored.",
|
|
82
|
+
reply: "Reply",
|
|
83
|
+
publishFeatured: "Post as Featured",
|
|
84
|
+
publishUnlisted: "Post Unlisted",
|
|
85
|
+
publishPrivate: "Post as Private",
|
|
86
|
+
showMore: "Show more",
|
|
87
|
+
showLess: "Show less",
|
|
88
|
+
collectionFormLabels: {
|
|
89
|
+
titleLabel: "Title",
|
|
90
|
+
titlePlaceholder: "My Collection",
|
|
91
|
+
slugLabel: "Slug",
|
|
92
|
+
slugHelp: "URL-safe identifier",
|
|
93
|
+
descriptionLabel: "Description (optional)",
|
|
94
|
+
descriptionPlaceholder: "What's this collection about?",
|
|
95
|
+
removeIcon: "Remove",
|
|
96
|
+
iconsTab: "Icons",
|
|
97
|
+
emojisTab: "Emojis",
|
|
98
|
+
searchIconsPlaceholder: "Search icons...",
|
|
99
|
+
searchEmojisPlaceholder: "Search emojis...",
|
|
100
|
+
sortOrderLabel: "Sort Order",
|
|
101
|
+
sortNewest: "Newest first",
|
|
102
|
+
sortOldest: "Oldest first",
|
|
103
|
+
sortRatingDesc: "Highest rated",
|
|
104
|
+
sortRatingAsc: "Lowest rated",
|
|
105
|
+
submitLabel: "Save",
|
|
106
|
+
cancelLabel: "Cancel",
|
|
107
|
+
},
|
|
60
108
|
};
|
|
61
109
|
|
|
62
110
|
const collections: ComposeCollection[] = [
|
|
63
|
-
{ id: 1, title: "Books", iconHtml: "" },
|
|
64
|
-
{ id: 2, title: "Movies", iconHtml: "<span>🎬</span>" },
|
|
111
|
+
{ id: "col-1", title: "Books", iconHtml: "" },
|
|
112
|
+
{ id: "col-2", title: "Movies", iconHtml: "<span>🎬</span>" },
|
|
65
113
|
];
|
|
66
114
|
|
|
67
115
|
async function createElement(
|
|
@@ -96,9 +144,9 @@ describe("JantComposeDialog", () => {
|
|
|
96
144
|
expect(segmentedItems[1].textContent?.trim()).toBe("Link");
|
|
97
145
|
expect(segmentedItems[2].textContent?.trim()).toBe("Quote");
|
|
98
146
|
|
|
99
|
-
// Post button present
|
|
147
|
+
// Post button present (split button with visibility dropdown)
|
|
100
148
|
const postBtn = requireElement(
|
|
101
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
149
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
102
150
|
"expected post button",
|
|
103
151
|
);
|
|
104
152
|
expect(postBtn.textContent?.trim()).toBe("Post");
|
|
@@ -131,7 +179,7 @@ describe("JantComposeDialog", () => {
|
|
|
131
179
|
);
|
|
132
180
|
});
|
|
133
181
|
|
|
134
|
-
it("submit dispatches jant:compose-submit with correct payload", async () => {
|
|
182
|
+
it("submit dispatches jant:compose-submit-deferred with correct payload", async () => {
|
|
135
183
|
const el = await createElement();
|
|
136
184
|
const editor = requireElement(
|
|
137
185
|
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
@@ -145,26 +193,34 @@ describe("JantComposeDialog", () => {
|
|
|
145
193
|
};
|
|
146
194
|
await editor.updateComplete;
|
|
147
195
|
|
|
148
|
-
let receivedDetail:
|
|
149
|
-
|
|
150
|
-
|
|
196
|
+
let receivedDetail:
|
|
197
|
+
| (ComposeSubmitDetail & { pendingAttachments: unknown[] })
|
|
198
|
+
| null = null;
|
|
199
|
+
el.addEventListener("jant:compose-submit-deferred", (event) => {
|
|
200
|
+
const customEvent = event as CustomEvent<
|
|
201
|
+
ComposeSubmitDetail & { pendingAttachments: unknown[] }
|
|
202
|
+
>;
|
|
151
203
|
receivedDetail = customEvent.detail;
|
|
152
204
|
});
|
|
153
205
|
|
|
154
206
|
// Click post button
|
|
155
207
|
requireElement(
|
|
156
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
208
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
157
209
|
"expected post button",
|
|
158
210
|
).click();
|
|
159
211
|
|
|
160
212
|
expect(receivedDetail).not.toBeNull();
|
|
161
|
-
const detail = receivedDetail as unknown as ComposeSubmitDetail
|
|
213
|
+
const detail = receivedDetail as unknown as ComposeSubmitDetail & {
|
|
214
|
+
pendingAttachments: unknown[];
|
|
215
|
+
};
|
|
162
216
|
expect(detail.format).toBe("note");
|
|
163
217
|
expect(detail.body).toContain("Hello world");
|
|
164
218
|
expect(detail.status).toBe("published");
|
|
219
|
+
expect(detail.visibility).toBe("public");
|
|
165
220
|
expect(detail.collectionIds).toEqual([]);
|
|
166
221
|
expect(detail.mediaIds).toEqual([]);
|
|
167
222
|
expect(detail.mediaAlts).toEqual({});
|
|
223
|
+
expect(detail.pendingAttachments).toEqual([]);
|
|
168
224
|
});
|
|
169
225
|
|
|
170
226
|
it("collection selector toggles IDs", async () => {
|
|
@@ -186,30 +242,32 @@ describe("JantComposeDialog", () => {
|
|
|
186
242
|
// Select first collection
|
|
187
243
|
options[0].click();
|
|
188
244
|
await el.updateComplete;
|
|
189
|
-
expect(el._collectionIds).toEqual([1]);
|
|
245
|
+
expect(el._collectionIds).toEqual(["col-1"]);
|
|
190
246
|
|
|
191
247
|
// Select second collection
|
|
192
248
|
options[1].click();
|
|
193
249
|
await el.updateComplete;
|
|
194
|
-
expect(el._collectionIds).toEqual([1, 2]);
|
|
250
|
+
expect(el._collectionIds).toEqual(["col-1", "col-2"]);
|
|
195
251
|
|
|
196
252
|
// Deselect first
|
|
197
253
|
options[0].click();
|
|
198
254
|
await el.updateComplete;
|
|
199
|
-
expect(el._collectionIds).toEqual([2]);
|
|
255
|
+
expect(el._collectionIds).toEqual(["col-2"]);
|
|
200
256
|
});
|
|
201
257
|
|
|
202
258
|
it("reset restores initial state", async () => {
|
|
203
259
|
const el = await createElement();
|
|
204
260
|
el._format = "link";
|
|
205
|
-
el._collectionIds = [1, 2];
|
|
261
|
+
el._collectionIds = ["col-1", "col-2"];
|
|
206
262
|
el._loading = true;
|
|
263
|
+
el._draftSourceId = "abc123";
|
|
207
264
|
|
|
208
265
|
el.reset();
|
|
209
266
|
|
|
210
267
|
expect(el._format).toBe("note");
|
|
211
268
|
expect(el._collectionIds).toEqual([]);
|
|
212
269
|
expect(el._loading).toBe(false);
|
|
270
|
+
expect(el._draftSourceId).toBeNull();
|
|
213
271
|
});
|
|
214
272
|
|
|
215
273
|
it("loading state disables submit button", async () => {
|
|
@@ -218,23 +276,22 @@ describe("JantComposeDialog", () => {
|
|
|
218
276
|
await el.updateComplete;
|
|
219
277
|
|
|
220
278
|
const postBtn = requireElement(
|
|
221
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
279
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
222
280
|
"expected post button",
|
|
223
281
|
);
|
|
224
282
|
expect(postBtn.disabled).toBe(true);
|
|
225
283
|
});
|
|
226
284
|
|
|
227
|
-
it("renders without collections", async () => {
|
|
285
|
+
it("renders collection selector even without collections", async () => {
|
|
228
286
|
const el = await createElement([]);
|
|
229
287
|
|
|
230
|
-
//
|
|
231
|
-
expect(el.querySelector(".compose-collection-trigger")).toBeNull();
|
|
232
|
-
// Spacer div present instead
|
|
288
|
+
// Collection trigger is still shown so users can create new collections
|
|
289
|
+
expect(el.querySelector(".compose-collection-trigger")).not.toBeNull();
|
|
233
290
|
const actionRow = el.querySelector(".compose-action-row");
|
|
234
291
|
expect(actionRow).not.toBeNull();
|
|
235
292
|
});
|
|
236
293
|
|
|
237
|
-
it("draft button
|
|
294
|
+
it("draft button with content shows confirm panel", async () => {
|
|
238
295
|
const el = await createElement();
|
|
239
296
|
const editor = requireElement(
|
|
240
297
|
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
@@ -251,22 +308,47 @@ describe("JantComposeDialog", () => {
|
|
|
251
308
|
};
|
|
252
309
|
await editor.updateComplete;
|
|
253
310
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
311
|
+
// Click the draft header button — should show confirm panel
|
|
312
|
+
const draftBtn = requireElement(
|
|
313
|
+
el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
|
|
314
|
+
"expected draft button",
|
|
315
|
+
);
|
|
316
|
+
draftBtn.click();
|
|
317
|
+
await el.updateComplete;
|
|
259
318
|
|
|
260
|
-
|
|
319
|
+
expect(el._confirmPanelOpen).toBe(true);
|
|
320
|
+
expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("draft button without content opens drafts panel", async () => {
|
|
324
|
+
const el = await createElement();
|
|
325
|
+
|
|
326
|
+
// Mock fetch for drafts list
|
|
327
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
328
|
+
new Response(JSON.stringify({ posts: [] }), {
|
|
329
|
+
status: 200,
|
|
330
|
+
headers: { "Content-Type": "application/json" },
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Click the draft header button — should open drafts panel
|
|
261
335
|
const draftBtn = requireElement(
|
|
262
336
|
el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
|
|
263
337
|
"expected draft button",
|
|
264
338
|
);
|
|
265
339
|
draftBtn.click();
|
|
340
|
+
await el.updateComplete;
|
|
266
341
|
|
|
267
|
-
expect(
|
|
268
|
-
|
|
269
|
-
|
|
342
|
+
expect(el._draftsPanelOpen).toBe(true);
|
|
343
|
+
|
|
344
|
+
// Wait for fetch to resolve
|
|
345
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
346
|
+
await el.updateComplete;
|
|
347
|
+
|
|
348
|
+
expect(el._draftsLoading).toBe(false);
|
|
349
|
+
expect(el.querySelector(".compose-drafts-panel")).not.toBeNull();
|
|
350
|
+
|
|
351
|
+
fetchSpy.mockRestore();
|
|
270
352
|
});
|
|
271
353
|
|
|
272
354
|
it("does not dispatch submit when loading", async () => {
|
|
@@ -275,12 +357,12 @@ describe("JantComposeDialog", () => {
|
|
|
275
357
|
await el.updateComplete;
|
|
276
358
|
|
|
277
359
|
let dispatched = false;
|
|
278
|
-
el.addEventListener("jant:compose-submit", () => {
|
|
360
|
+
el.addEventListener("jant:compose-submit-deferred", () => {
|
|
279
361
|
dispatched = true;
|
|
280
362
|
});
|
|
281
363
|
|
|
282
364
|
const postBtn = requireElement(
|
|
283
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
365
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
284
366
|
"expected post button",
|
|
285
367
|
);
|
|
286
368
|
postBtn.click();
|
|
@@ -293,7 +375,7 @@ describe("JantComposeDialog", () => {
|
|
|
293
375
|
el._loading = true;
|
|
294
376
|
await el.updateComplete;
|
|
295
377
|
|
|
296
|
-
const spinner = el.querySelector(".compose-
|
|
378
|
+
const spinner = el.querySelector(".compose-publish-main .animate-spin");
|
|
297
379
|
expect(spinner).not.toBeNull();
|
|
298
380
|
});
|
|
299
381
|
|
|
@@ -322,11 +404,15 @@ describe("JantComposeDialog", () => {
|
|
|
322
404
|
file,
|
|
323
405
|
previewUrl,
|
|
324
406
|
status: "done",
|
|
407
|
+
progress: null,
|
|
325
408
|
mediaId: "media-1",
|
|
326
409
|
alt: "",
|
|
327
410
|
error: null,
|
|
411
|
+
summary: null,
|
|
412
|
+
chars: null,
|
|
328
413
|
},
|
|
329
414
|
];
|
|
415
|
+
editor._attachmentOrder = ["test-id-1"];
|
|
330
416
|
await editor.updateComplete;
|
|
331
417
|
|
|
332
418
|
// Thumbnail strip should be visible
|
|
@@ -361,11 +447,15 @@ describe("JantComposeDialog", () => {
|
|
|
361
447
|
file,
|
|
362
448
|
previewUrl,
|
|
363
449
|
status: "done",
|
|
450
|
+
progress: null,
|
|
364
451
|
mediaId: "media-1",
|
|
365
452
|
alt: "",
|
|
366
453
|
error: null,
|
|
454
|
+
summary: null,
|
|
455
|
+
chars: null,
|
|
367
456
|
},
|
|
368
457
|
];
|
|
458
|
+
editor._attachmentOrder = ["test-id-1"];
|
|
369
459
|
await editor.updateComplete;
|
|
370
460
|
|
|
371
461
|
// Click remove button
|
|
@@ -398,11 +488,15 @@ describe("JantComposeDialog", () => {
|
|
|
398
488
|
file,
|
|
399
489
|
previewUrl,
|
|
400
490
|
status: "done",
|
|
491
|
+
progress: null,
|
|
401
492
|
mediaId: "media-1",
|
|
402
493
|
alt: "",
|
|
403
494
|
error: null,
|
|
495
|
+
summary: null,
|
|
496
|
+
chars: null,
|
|
404
497
|
},
|
|
405
498
|
];
|
|
499
|
+
editor._attachmentOrder = ["test-id-1"];
|
|
406
500
|
await editor.updateComplete;
|
|
407
501
|
|
|
408
502
|
// Click ALT button
|
|
@@ -448,9 +542,12 @@ describe("JantComposeDialog", () => {
|
|
|
448
542
|
file,
|
|
449
543
|
previewUrl,
|
|
450
544
|
status: "done",
|
|
545
|
+
progress: null,
|
|
451
546
|
mediaId: "media-1",
|
|
452
547
|
alt: "A test image",
|
|
453
548
|
error: null,
|
|
549
|
+
summary: null,
|
|
550
|
+
chars: null,
|
|
454
551
|
},
|
|
455
552
|
];
|
|
456
553
|
editor._bodyJson = {
|
|
@@ -464,21 +561,28 @@ describe("JantComposeDialog", () => {
|
|
|
464
561
|
};
|
|
465
562
|
await editor.updateComplete;
|
|
466
563
|
|
|
467
|
-
let receivedDetail:
|
|
468
|
-
|
|
469
|
-
|
|
564
|
+
let receivedDetail:
|
|
565
|
+
| (ComposeSubmitDetail & { pendingAttachments: unknown[] })
|
|
566
|
+
| null = null;
|
|
567
|
+
el.addEventListener("jant:compose-submit-deferred", (event) => {
|
|
568
|
+
const customEvent = event as CustomEvent<
|
|
569
|
+
ComposeSubmitDetail & { pendingAttachments: unknown[] }
|
|
570
|
+
>;
|
|
470
571
|
receivedDetail = customEvent.detail;
|
|
471
572
|
});
|
|
472
573
|
|
|
473
574
|
requireElement(
|
|
474
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
575
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
475
576
|
"expected post button",
|
|
476
577
|
).click();
|
|
477
578
|
|
|
478
579
|
expect(receivedDetail).not.toBeNull();
|
|
479
|
-
const detail = receivedDetail as unknown as ComposeSubmitDetail
|
|
580
|
+
const detail = receivedDetail as unknown as ComposeSubmitDetail & {
|
|
581
|
+
pendingAttachments: unknown[];
|
|
582
|
+
};
|
|
480
583
|
expect(detail.mediaIds).toEqual(["media-1"]);
|
|
481
584
|
expect(detail.mediaAlts).toEqual({ "media-1": "A test image" });
|
|
585
|
+
expect(detail.pendingAttachments).toEqual([]);
|
|
482
586
|
|
|
483
587
|
URL.revokeObjectURL(previewUrl);
|
|
484
588
|
});
|
|
@@ -500,9 +604,12 @@ describe("JantComposeDialog", () => {
|
|
|
500
604
|
file,
|
|
501
605
|
previewUrl,
|
|
502
606
|
status: "uploading",
|
|
607
|
+
progress: null,
|
|
503
608
|
mediaId: null,
|
|
504
609
|
alt: "Alt for pending",
|
|
505
610
|
error: null,
|
|
611
|
+
summary: null,
|
|
612
|
+
chars: null,
|
|
506
613
|
},
|
|
507
614
|
];
|
|
508
615
|
editor._bodyJson = {
|
|
@@ -521,24 +628,513 @@ describe("JantComposeDialog", () => {
|
|
|
521
628
|
deferredEvent = event as CustomEvent;
|
|
522
629
|
});
|
|
523
630
|
|
|
524
|
-
// Prevent dialog.close() from throwing (no parent dialog in test)
|
|
525
|
-
let submitEvent: CustomEvent | null = null;
|
|
526
|
-
el.addEventListener("jant:compose-submit", (event) => {
|
|
527
|
-
submitEvent = event as CustomEvent;
|
|
528
|
-
});
|
|
529
|
-
|
|
530
631
|
requireElement(
|
|
531
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
632
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
532
633
|
"expected post button",
|
|
533
634
|
).click();
|
|
534
635
|
|
|
535
|
-
// Should have dispatched deferred, not regular submit
|
|
536
636
|
expect(deferredEvent).not.toBeNull();
|
|
537
|
-
expect(submitEvent).toBeNull();
|
|
538
637
|
expect(
|
|
539
638
|
(deferredEvent as unknown as CustomEvent).detail.pendingAttachments,
|
|
540
|
-
).
|
|
639
|
+
).toHaveLength(1);
|
|
541
640
|
|
|
542
641
|
URL.revokeObjectURL(previewUrl);
|
|
543
642
|
});
|
|
643
|
+
|
|
644
|
+
// ── Close confirmation ─────────────────────────────────────────────
|
|
645
|
+
|
|
646
|
+
it("requestClose on empty form closes immediately without confirmation", async () => {
|
|
647
|
+
const el = await createElement();
|
|
648
|
+
|
|
649
|
+
// Ensure no confirmation panel appears
|
|
650
|
+
el.requestClose();
|
|
651
|
+
await el.updateComplete;
|
|
652
|
+
|
|
653
|
+
expect(el._confirmPanelOpen).toBe(false);
|
|
654
|
+
expect(el.querySelector(".compose-confirm-panel")).toBeNull();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("beforeunload does not warn when dialog was only opened", async () => {
|
|
658
|
+
const el = await createElement();
|
|
659
|
+
vi.spyOn(el, "closest").mockReturnValue({
|
|
660
|
+
open: true,
|
|
661
|
+
addEventListener: vi.fn(),
|
|
662
|
+
removeEventListener: vi.fn(),
|
|
663
|
+
} as unknown as HTMLDialogElement);
|
|
664
|
+
|
|
665
|
+
const event = new Event("beforeunload", {
|
|
666
|
+
cancelable: true,
|
|
667
|
+
}) as globalThis.BeforeUnloadEvent;
|
|
668
|
+
|
|
669
|
+
window.dispatchEvent(event);
|
|
670
|
+
|
|
671
|
+
expect(event.defaultPrevented).toBe(false);
|
|
672
|
+
expect(
|
|
673
|
+
(
|
|
674
|
+
el as unknown as { _hasUnsavedChanges: () => boolean }
|
|
675
|
+
)._hasUnsavedChanges(),
|
|
676
|
+
).toBe(false);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("beforeunload warns after compose content changes", async () => {
|
|
680
|
+
const el = await createElement();
|
|
681
|
+
vi.spyOn(el, "closest").mockReturnValue({
|
|
682
|
+
open: true,
|
|
683
|
+
addEventListener: vi.fn(),
|
|
684
|
+
removeEventListener: vi.fn(),
|
|
685
|
+
} as unknown as HTMLDialogElement);
|
|
686
|
+
const editor = requireElement(
|
|
687
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
688
|
+
"expected compose editor",
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
editor._bodyJson = {
|
|
692
|
+
type: "doc",
|
|
693
|
+
content: [
|
|
694
|
+
{ type: "paragraph", content: [{ type: "text", text: "Unsaved" }] },
|
|
695
|
+
],
|
|
696
|
+
};
|
|
697
|
+
await editor.updateComplete;
|
|
698
|
+
|
|
699
|
+
const event = new Event("beforeunload", {
|
|
700
|
+
cancelable: true,
|
|
701
|
+
}) as globalThis.BeforeUnloadEvent;
|
|
702
|
+
|
|
703
|
+
window.dispatchEvent(event);
|
|
704
|
+
|
|
705
|
+
expect(event.defaultPrevented).toBe(true);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it("requestClose with content shows confirmation panel", async () => {
|
|
709
|
+
const el = await createElement();
|
|
710
|
+
const editor = requireElement(
|
|
711
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
712
|
+
"expected compose editor",
|
|
713
|
+
);
|
|
714
|
+
editor._bodyJson = {
|
|
715
|
+
type: "doc",
|
|
716
|
+
content: [
|
|
717
|
+
{ type: "paragraph", content: [{ type: "text", text: "Some text" }] },
|
|
718
|
+
],
|
|
719
|
+
};
|
|
720
|
+
await editor.updateComplete;
|
|
721
|
+
|
|
722
|
+
el.requestClose();
|
|
723
|
+
await el.updateComplete;
|
|
724
|
+
|
|
725
|
+
expect(el._confirmPanelOpen).toBe(true);
|
|
726
|
+
expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
|
|
727
|
+
expect(
|
|
728
|
+
el.querySelector(".compose-confirm-title")?.textContent?.trim(),
|
|
729
|
+
).toBe("Save to drafts?");
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it("confirm save draft dispatches submit-deferred with draft status", async () => {
|
|
733
|
+
const el = await createElement();
|
|
734
|
+
const editor = requireElement(
|
|
735
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
736
|
+
"expected compose editor",
|
|
737
|
+
);
|
|
738
|
+
editor._bodyJson = {
|
|
739
|
+
type: "doc",
|
|
740
|
+
content: [
|
|
741
|
+
{ type: "paragraph", content: [{ type: "text", text: "Draft me" }] },
|
|
742
|
+
],
|
|
743
|
+
};
|
|
744
|
+
await editor.updateComplete;
|
|
745
|
+
|
|
746
|
+
el.requestClose();
|
|
747
|
+
await el.updateComplete;
|
|
748
|
+
|
|
749
|
+
let receivedDetail: ComposeSubmitDetail | null = null;
|
|
750
|
+
el.addEventListener("jant:compose-submit-deferred", (event) => {
|
|
751
|
+
receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
const saveBtn = requireElement(
|
|
755
|
+
el.querySelector<HTMLButtonElement>(".compose-confirm-save"),
|
|
756
|
+
"expected save draft button",
|
|
757
|
+
);
|
|
758
|
+
saveBtn.click();
|
|
759
|
+
await el.updateComplete;
|
|
760
|
+
|
|
761
|
+
expect(receivedDetail).not.toBeNull();
|
|
762
|
+
expect((receivedDetail as unknown as ComposeSubmitDetail).status).toBe(
|
|
763
|
+
"draft",
|
|
764
|
+
);
|
|
765
|
+
expect(el._confirmPanelOpen).toBe(false);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it("confirm cancel returns to editor without closing", async () => {
|
|
769
|
+
const el = await createElement();
|
|
770
|
+
const editor = requireElement(
|
|
771
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
772
|
+
"expected compose editor",
|
|
773
|
+
);
|
|
774
|
+
editor._bodyJson = {
|
|
775
|
+
type: "doc",
|
|
776
|
+
content: [
|
|
777
|
+
{
|
|
778
|
+
type: "paragraph",
|
|
779
|
+
content: [{ type: "text", text: "Keep editing" }],
|
|
780
|
+
},
|
|
781
|
+
],
|
|
782
|
+
};
|
|
783
|
+
await editor.updateComplete;
|
|
784
|
+
|
|
785
|
+
el.requestClose();
|
|
786
|
+
await el.updateComplete;
|
|
787
|
+
|
|
788
|
+
expect(el._confirmPanelOpen).toBe(true);
|
|
789
|
+
|
|
790
|
+
const cancelBtn = requireElement(
|
|
791
|
+
el.querySelector<HTMLButtonElement>(".compose-confirm-cancel"),
|
|
792
|
+
"expected cancel button",
|
|
793
|
+
);
|
|
794
|
+
const focusSpy = vi.spyOn(editor, "focusInput");
|
|
795
|
+
cancelBtn.click();
|
|
796
|
+
await el.updateComplete;
|
|
797
|
+
|
|
798
|
+
expect(el._confirmPanelOpen).toBe(false);
|
|
799
|
+
expect(focusSpy).toHaveBeenCalled();
|
|
800
|
+
// Editor content should be preserved
|
|
801
|
+
expect(editor._bodyJson).toEqual({
|
|
802
|
+
type: "doc",
|
|
803
|
+
content: [
|
|
804
|
+
{
|
|
805
|
+
type: "paragraph",
|
|
806
|
+
content: [{ type: "text", text: "Keep editing" }],
|
|
807
|
+
},
|
|
808
|
+
],
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it("requestClose on confirm panel dismisses it (Escape = Cancel)", async () => {
|
|
813
|
+
const el = await createElement();
|
|
814
|
+
const editor = requireElement(
|
|
815
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
816
|
+
"expected compose editor",
|
|
817
|
+
);
|
|
818
|
+
editor._bodyJson = {
|
|
819
|
+
type: "doc",
|
|
820
|
+
content: [
|
|
821
|
+
{ type: "paragraph", content: [{ type: "text", text: "Esc test" }] },
|
|
822
|
+
],
|
|
823
|
+
};
|
|
824
|
+
await editor.updateComplete;
|
|
825
|
+
|
|
826
|
+
el.requestClose();
|
|
827
|
+
await el.updateComplete;
|
|
828
|
+
expect(el._confirmPanelOpen).toBe(true);
|
|
829
|
+
|
|
830
|
+
// Second requestClose (same path as Escape via dialog oncancel)
|
|
831
|
+
el.requestClose();
|
|
832
|
+
await el.updateComplete;
|
|
833
|
+
|
|
834
|
+
expect(el._confirmPanelOpen).toBe(false);
|
|
835
|
+
// Content should be preserved (not discarded)
|
|
836
|
+
expect(editor._bodyJson).toEqual({
|
|
837
|
+
type: "doc",
|
|
838
|
+
content: [
|
|
839
|
+
{ type: "paragraph", content: [{ type: "text", text: "Esc test" }] },
|
|
840
|
+
],
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it("confirm discard closes and resets", async () => {
|
|
845
|
+
const el = await createElement();
|
|
846
|
+
const editor = requireElement(
|
|
847
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
848
|
+
"expected compose editor",
|
|
849
|
+
);
|
|
850
|
+
editor._bodyJson = {
|
|
851
|
+
type: "doc",
|
|
852
|
+
content: [
|
|
853
|
+
{
|
|
854
|
+
type: "paragraph",
|
|
855
|
+
content: [{ type: "text", text: "Will discard" }],
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
};
|
|
859
|
+
await editor.updateComplete;
|
|
860
|
+
|
|
861
|
+
el.requestClose();
|
|
862
|
+
await el.updateComplete;
|
|
863
|
+
|
|
864
|
+
const discardBtn = requireElement(
|
|
865
|
+
el.querySelector<HTMLButtonElement>(".compose-confirm-discard"),
|
|
866
|
+
"expected discard button",
|
|
867
|
+
);
|
|
868
|
+
discardBtn.click();
|
|
869
|
+
await el.updateComplete;
|
|
870
|
+
|
|
871
|
+
expect(el._confirmPanelOpen).toBe(false);
|
|
872
|
+
expect(el._format).toBe("note");
|
|
873
|
+
expect(el._collectionIds).toEqual([]);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it("loaded draft shows format switcher and Post button, not edit mode", async () => {
|
|
877
|
+
const el = await createElement();
|
|
878
|
+
|
|
879
|
+
// Simulate what _loadDraft sets (without fetching)
|
|
880
|
+
el._draftSourceId = "draft123";
|
|
881
|
+
el._format = "note";
|
|
882
|
+
await el.updateComplete;
|
|
883
|
+
|
|
884
|
+
// Format switcher should be visible (not "Edit post" title)
|
|
885
|
+
expect(el.querySelector(".compose-segmented")).not.toBeNull();
|
|
886
|
+
expect(el.querySelector(".compose-dialog-title")).toBeNull();
|
|
887
|
+
|
|
888
|
+
// Button should say "Post", not "Done"
|
|
889
|
+
const postBtn = requireElement(
|
|
890
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
891
|
+
"expected post button",
|
|
892
|
+
);
|
|
893
|
+
expect(postBtn.textContent?.trim()).toBe("Post");
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it("discard on loaded draft sends DELETE request", async () => {
|
|
897
|
+
const el = await createElement();
|
|
898
|
+
const editor = requireElement(
|
|
899
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
900
|
+
"expected compose editor",
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
// Simulate loaded draft with content
|
|
904
|
+
el._draftSourceId = "draft456";
|
|
905
|
+
editor._bodyJson = {
|
|
906
|
+
type: "doc",
|
|
907
|
+
content: [
|
|
908
|
+
{
|
|
909
|
+
type: "paragraph",
|
|
910
|
+
content: [{ type: "text", text: "Draft content" }],
|
|
911
|
+
},
|
|
912
|
+
],
|
|
913
|
+
};
|
|
914
|
+
await editor.updateComplete;
|
|
915
|
+
|
|
916
|
+
const fetchSpy = vi
|
|
917
|
+
.spyOn(globalThis, "fetch")
|
|
918
|
+
.mockResolvedValue(new Response(null, { status: 200 }));
|
|
919
|
+
|
|
920
|
+
el.requestClose();
|
|
921
|
+
await el.updateComplete;
|
|
922
|
+
|
|
923
|
+
// Click "Don't save" (discard)
|
|
924
|
+
const discardBtn = requireElement(
|
|
925
|
+
el.querySelector<HTMLButtonElement>(".compose-confirm-discard"),
|
|
926
|
+
"expected discard button",
|
|
927
|
+
);
|
|
928
|
+
discardBtn.click();
|
|
929
|
+
await el.updateComplete;
|
|
930
|
+
|
|
931
|
+
expect(fetchSpy).toHaveBeenCalledWith("/api/posts/draft456", {
|
|
932
|
+
method: "DELETE",
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
fetchSpy.mockRestore();
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it("submit from loaded draft includes draftSourceId as editPostId", async () => {
|
|
939
|
+
const el = await createElement();
|
|
940
|
+
const editor = requireElement(
|
|
941
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
942
|
+
"expected compose editor",
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
el._draftSourceId = "draft789";
|
|
946
|
+
editor._bodyJson = {
|
|
947
|
+
type: "doc",
|
|
948
|
+
content: [
|
|
949
|
+
{
|
|
950
|
+
type: "paragraph",
|
|
951
|
+
content: [{ type: "text", text: "Publish this draft" }],
|
|
952
|
+
},
|
|
953
|
+
],
|
|
954
|
+
};
|
|
955
|
+
await editor.updateComplete;
|
|
956
|
+
|
|
957
|
+
let receivedDetail: ComposeSubmitDetail | null = null;
|
|
958
|
+
el.addEventListener("jant:compose-submit-deferred", (event) => {
|
|
959
|
+
receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
requireElement(
|
|
963
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
964
|
+
"expected post button",
|
|
965
|
+
).click();
|
|
966
|
+
|
|
967
|
+
expect(receivedDetail).not.toBeNull();
|
|
968
|
+
expect((receivedDetail as unknown as ComposeSubmitDetail).editPostId).toBe(
|
|
969
|
+
"draft789",
|
|
970
|
+
);
|
|
971
|
+
expect((receivedDetail as unknown as ComposeSubmitDetail).status).toBe(
|
|
972
|
+
"published",
|
|
973
|
+
);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it("draft button confirm save dispatches draft then opens drafts panel", async () => {
|
|
977
|
+
const el = await createElement();
|
|
978
|
+
const editor = requireElement(
|
|
979
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
980
|
+
"expected compose editor",
|
|
981
|
+
);
|
|
982
|
+
editor._bodyJson = {
|
|
983
|
+
type: "doc",
|
|
984
|
+
content: [
|
|
985
|
+
{
|
|
986
|
+
type: "paragraph",
|
|
987
|
+
content: [{ type: "text", text: "Save then browse" }],
|
|
988
|
+
},
|
|
989
|
+
],
|
|
990
|
+
};
|
|
991
|
+
await editor.updateComplete;
|
|
992
|
+
|
|
993
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
994
|
+
new Response(JSON.stringify({ posts: [] }), {
|
|
995
|
+
status: 200,
|
|
996
|
+
headers: { "Content-Type": "application/json" },
|
|
997
|
+
}),
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
let receivedDetail: ComposeSubmitDetail | null = null;
|
|
1001
|
+
el.addEventListener("jant:compose-submit-deferred", (event) => {
|
|
1002
|
+
receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// Click draft button → confirm panel
|
|
1006
|
+
const draftBtn = requireElement(
|
|
1007
|
+
el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
|
|
1008
|
+
"expected draft button",
|
|
1009
|
+
);
|
|
1010
|
+
draftBtn.click();
|
|
1011
|
+
await el.updateComplete;
|
|
1012
|
+
expect(el._confirmPanelOpen).toBe(true);
|
|
1013
|
+
|
|
1014
|
+
// Click "Save"
|
|
1015
|
+
requireElement(
|
|
1016
|
+
el.querySelector<HTMLButtonElement>(".compose-confirm-save"),
|
|
1017
|
+
"expected save button",
|
|
1018
|
+
).click();
|
|
1019
|
+
await el.updateComplete;
|
|
1020
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1021
|
+
await el.updateComplete;
|
|
1022
|
+
|
|
1023
|
+
// Draft submitted
|
|
1024
|
+
expect(receivedDetail).not.toBeNull();
|
|
1025
|
+
expect((receivedDetail as unknown as ComposeSubmitDetail).status).toBe(
|
|
1026
|
+
"draft",
|
|
1027
|
+
);
|
|
1028
|
+
// Drafts panel opened instead of dialog closing
|
|
1029
|
+
expect(el._draftsPanelOpen).toBe(true);
|
|
1030
|
+
expect(el._confirmPanelOpen).toBe(false);
|
|
1031
|
+
|
|
1032
|
+
fetchSpy.mockRestore();
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
it("draft button confirm discard opens drafts panel without saving", async () => {
|
|
1036
|
+
const el = await createElement();
|
|
1037
|
+
const editor = requireElement(
|
|
1038
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
1039
|
+
"expected compose editor",
|
|
1040
|
+
);
|
|
1041
|
+
editor._bodyJson = {
|
|
1042
|
+
type: "doc",
|
|
1043
|
+
content: [
|
|
1044
|
+
{
|
|
1045
|
+
type: "paragraph",
|
|
1046
|
+
content: [{ type: "text", text: "Discard then browse" }],
|
|
1047
|
+
},
|
|
1048
|
+
],
|
|
1049
|
+
};
|
|
1050
|
+
await editor.updateComplete;
|
|
1051
|
+
|
|
1052
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
1053
|
+
new Response(JSON.stringify({ posts: [] }), {
|
|
1054
|
+
status: 200,
|
|
1055
|
+
headers: { "Content-Type": "application/json" },
|
|
1056
|
+
}),
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
let submitFired = false;
|
|
1060
|
+
el.addEventListener("jant:compose-submit-deferred", () => {
|
|
1061
|
+
submitFired = true;
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// Click draft button → confirm panel
|
|
1065
|
+
const draftBtn = requireElement(
|
|
1066
|
+
el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
|
|
1067
|
+
"expected draft button",
|
|
1068
|
+
);
|
|
1069
|
+
draftBtn.click();
|
|
1070
|
+
await el.updateComplete;
|
|
1071
|
+
|
|
1072
|
+
// Click "Don't save"
|
|
1073
|
+
requireElement(
|
|
1074
|
+
el.querySelector<HTMLButtonElement>(".compose-confirm-discard"),
|
|
1075
|
+
"expected discard button",
|
|
1076
|
+
).click();
|
|
1077
|
+
await el.updateComplete;
|
|
1078
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1079
|
+
await el.updateComplete;
|
|
1080
|
+
|
|
1081
|
+
// No submit dispatched
|
|
1082
|
+
expect(submitFired).toBe(false);
|
|
1083
|
+
// Drafts panel opened
|
|
1084
|
+
expect(el._draftsPanelOpen).toBe(true);
|
|
1085
|
+
expect(el._confirmPanelOpen).toBe(false);
|
|
1086
|
+
|
|
1087
|
+
fetchSpy.mockRestore();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it("attachments detected as content for confirmation", async () => {
|
|
1091
|
+
const el = await createElement();
|
|
1092
|
+
const editor = requireElement(
|
|
1093
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
1094
|
+
"expected compose editor",
|
|
1095
|
+
);
|
|
1096
|
+
|
|
1097
|
+
const blob = new Blob(["fake-image"], { type: "image/png" });
|
|
1098
|
+
const file = new File([blob], "test.png", { type: "image/png" });
|
|
1099
|
+
const previewUrl = URL.createObjectURL(blob);
|
|
1100
|
+
|
|
1101
|
+
editor._attachments = [
|
|
1102
|
+
{
|
|
1103
|
+
clientId: "test-id-1",
|
|
1104
|
+
file,
|
|
1105
|
+
previewUrl,
|
|
1106
|
+
status: "done",
|
|
1107
|
+
progress: null,
|
|
1108
|
+
mediaId: "media-1",
|
|
1109
|
+
alt: "",
|
|
1110
|
+
error: null,
|
|
1111
|
+
summary: null,
|
|
1112
|
+
chars: null,
|
|
1113
|
+
},
|
|
1114
|
+
];
|
|
1115
|
+
await editor.updateComplete;
|
|
1116
|
+
|
|
1117
|
+
el.requestClose();
|
|
1118
|
+
await el.updateComplete;
|
|
1119
|
+
|
|
1120
|
+
expect(el._confirmPanelOpen).toBe(true);
|
|
1121
|
+
expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
|
|
1122
|
+
|
|
1123
|
+
URL.revokeObjectURL(previewUrl);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
it("rating detected as content for confirmation", async () => {
|
|
1127
|
+
const el = await createElement();
|
|
1128
|
+
const editor = requireElement(
|
|
1129
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
1130
|
+
"expected compose editor",
|
|
1131
|
+
);
|
|
1132
|
+
editor._rating = 3;
|
|
1133
|
+
await editor.updateComplete;
|
|
1134
|
+
|
|
1135
|
+
el.requestClose();
|
|
1136
|
+
await el.updateComplete;
|
|
1137
|
+
|
|
1138
|
+
expect(el._confirmPanelOpen).toBe(true);
|
|
1139
|
+
});
|
|
544
1140
|
});
|