@jant/core 0.3.26 → 0.3.28
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/dist/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +21 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +112 -173
- package/src/auth.ts +4 -1
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -265
- package/dist/auth.js +0 -36
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
4
|
+
import type { ComposeLabels } from "../compose-types.js";
|
|
5
|
+
import "../jant-compose-editor.js";
|
|
6
|
+
import type { JantComposeEditor } from "../jant-compose-editor.js";
|
|
7
|
+
|
|
8
|
+
function requireElement<T extends globalThis.Element>(
|
|
9
|
+
element: T | null,
|
|
10
|
+
message: string,
|
|
11
|
+
): T {
|
|
12
|
+
if (!element) {
|
|
13
|
+
throw new Error(message);
|
|
14
|
+
}
|
|
15
|
+
return element;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requireItem<T extends globalThis.Element>(
|
|
19
|
+
collection: globalThis.NodeListOf<T>,
|
|
20
|
+
index: number,
|
|
21
|
+
message: string,
|
|
22
|
+
): T {
|
|
23
|
+
const item = collection.item(index);
|
|
24
|
+
if (!item) {
|
|
25
|
+
throw new Error(message);
|
|
26
|
+
}
|
|
27
|
+
return item;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const labels: ComposeLabels = {
|
|
31
|
+
cancel: "Cancel",
|
|
32
|
+
note: "Note",
|
|
33
|
+
link: "Link",
|
|
34
|
+
quote: "Quote",
|
|
35
|
+
saveDraft: "Save as Draft",
|
|
36
|
+
saveAsDraft: "Save as draft",
|
|
37
|
+
discard: "Discard",
|
|
38
|
+
titlePlaceholder: "Title",
|
|
39
|
+
bodyPlaceholder: "What's on your mind...",
|
|
40
|
+
urlPlaceholder: "Paste a URL...",
|
|
41
|
+
linkTitlePlaceholder: "Give it a title...",
|
|
42
|
+
thoughtsPlaceholder: "Your thoughts (optional)",
|
|
43
|
+
quotePlaceholder: "Type the quote...",
|
|
44
|
+
authorPlaceholder: "Author (optional)",
|
|
45
|
+
sourcePlaceholder: "Source link (optional)",
|
|
46
|
+
attachedText: "Attached Text",
|
|
47
|
+
attachedTextPlaceholder: "Paste text...",
|
|
48
|
+
attachedTextHint: "Supplementary content",
|
|
49
|
+
done: "Done",
|
|
50
|
+
media: "Media",
|
|
51
|
+
score: "Score",
|
|
52
|
+
title: "Title",
|
|
53
|
+
collection: "Collection",
|
|
54
|
+
searchCollections: "Search...",
|
|
55
|
+
noCollections: "No collections found.",
|
|
56
|
+
post: "Post",
|
|
57
|
+
addAlt: "+ ALT",
|
|
58
|
+
addAltTitle: "Add alt text",
|
|
59
|
+
altPlaceholder: "Describe this...",
|
|
60
|
+
altHint: "Alt text improves accessibility",
|
|
61
|
+
addMore: "Add",
|
|
62
|
+
uploading: "Uploading...",
|
|
63
|
+
published: "Published!",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
async function createElement(
|
|
67
|
+
format: string = "note",
|
|
68
|
+
): Promise<JantComposeEditor> {
|
|
69
|
+
const el = document.createElement("jant-compose-editor") as JantComposeEditor;
|
|
70
|
+
el.format = format as "note" | "link" | "quote";
|
|
71
|
+
el.labels = labels;
|
|
72
|
+
document.body.appendChild(el);
|
|
73
|
+
await el.updateComplete;
|
|
74
|
+
return el;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("JantComposeEditor", () => {
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
document.body.innerHTML = "";
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("renders note fields by default", async () => {
|
|
83
|
+
const el = await createElement("note");
|
|
84
|
+
const textarea = requireElement(
|
|
85
|
+
el.querySelector<HTMLTextAreaElement>(".compose-body-input"),
|
|
86
|
+
"expected compose body textarea",
|
|
87
|
+
);
|
|
88
|
+
expect(textarea.placeholder).toBe("What's on your mind...");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("renders link fields when format is link", async () => {
|
|
92
|
+
const el = await createElement("link");
|
|
93
|
+
const urlInput = requireElement(
|
|
94
|
+
el.querySelector<HTMLInputElement>('input[type="url"]'),
|
|
95
|
+
"expected url input",
|
|
96
|
+
);
|
|
97
|
+
expect(urlInput.placeholder).toBe("Paste a URL...");
|
|
98
|
+
|
|
99
|
+
const titleInput = el.querySelector<HTMLInputElement>(
|
|
100
|
+
".compose-link-title",
|
|
101
|
+
);
|
|
102
|
+
expect(titleInput).not.toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("renders quote fields when format is quote", async () => {
|
|
106
|
+
const el = await createElement("quote");
|
|
107
|
+
const quoteTextarea = el.querySelector<HTMLTextAreaElement>(
|
|
108
|
+
".compose-quote-text",
|
|
109
|
+
);
|
|
110
|
+
expect(quoteTextarea).not.toBeNull();
|
|
111
|
+
|
|
112
|
+
const authorInput = el.querySelector<HTMLInputElement>(
|
|
113
|
+
".compose-quote-author",
|
|
114
|
+
);
|
|
115
|
+
expect(authorInput).not.toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("toggles star rating visibility", async () => {
|
|
119
|
+
const el = await createElement("note");
|
|
120
|
+
|
|
121
|
+
// Rating not visible initially
|
|
122
|
+
expect(el.querySelector(".compose-star-rating")).toBeNull();
|
|
123
|
+
|
|
124
|
+
// Click score button to show rating
|
|
125
|
+
const toolButtons =
|
|
126
|
+
el.querySelectorAll<HTMLButtonElement>(".compose-tool-btn");
|
|
127
|
+
const scoreBtnEl = requireItem(
|
|
128
|
+
toolButtons,
|
|
129
|
+
2,
|
|
130
|
+
"expected score tool button",
|
|
131
|
+
);
|
|
132
|
+
scoreBtnEl.click();
|
|
133
|
+
await el.updateComplete;
|
|
134
|
+
|
|
135
|
+
expect(el.querySelector(".compose-star-rating")).not.toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("sets rating on star click and deselects on same star", async () => {
|
|
139
|
+
const el = await createElement("note");
|
|
140
|
+
el._showRating = true;
|
|
141
|
+
await el.updateComplete;
|
|
142
|
+
|
|
143
|
+
const stars = el.querySelectorAll<HTMLButtonElement>(".compose-star");
|
|
144
|
+
expect(stars.length).toBe(5);
|
|
145
|
+
|
|
146
|
+
// Click third star
|
|
147
|
+
stars[2].click();
|
|
148
|
+
await el.updateComplete;
|
|
149
|
+
expect(el._rating).toBe(3);
|
|
150
|
+
|
|
151
|
+
// Rating label shows
|
|
152
|
+
const label = el.querySelector(".compose-star-label");
|
|
153
|
+
expect(label?.textContent).toContain("3/5");
|
|
154
|
+
|
|
155
|
+
// Click third star again to deselect
|
|
156
|
+
stars[2].click();
|
|
157
|
+
await el.updateComplete;
|
|
158
|
+
expect(el._rating).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("toggles attached text panel", async () => {
|
|
162
|
+
const el = await createElement("note");
|
|
163
|
+
|
|
164
|
+
// Click attached text tool button
|
|
165
|
+
const toolBtns =
|
|
166
|
+
el.querySelectorAll<HTMLButtonElement>(".compose-tool-btn");
|
|
167
|
+
const attachedBtn = requireItem(
|
|
168
|
+
toolBtns,
|
|
169
|
+
1,
|
|
170
|
+
"expected attached text button",
|
|
171
|
+
);
|
|
172
|
+
attachedBtn.click();
|
|
173
|
+
await el.updateComplete;
|
|
174
|
+
|
|
175
|
+
expect(el.querySelector(".compose-attached-panel")).not.toBeNull();
|
|
176
|
+
|
|
177
|
+
// Click done button to close
|
|
178
|
+
const doneBtn = el.querySelector<HTMLButtonElement>(
|
|
179
|
+
".compose-attached-panel .compose-post-btn",
|
|
180
|
+
);
|
|
181
|
+
doneBtn?.click();
|
|
182
|
+
await el.updateComplete;
|
|
183
|
+
|
|
184
|
+
expect(el.querySelector(".compose-attached-panel")).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("shows title toggle only in note mode", async () => {
|
|
188
|
+
const el = await createElement("note");
|
|
189
|
+
const toolSep = el.querySelector(".compose-tool-sep");
|
|
190
|
+
expect(toolSep).not.toBeNull();
|
|
191
|
+
|
|
192
|
+
el.format = "link";
|
|
193
|
+
await el.updateComplete;
|
|
194
|
+
expect(el.querySelector(".compose-tool-sep")).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("getData returns current field values", async () => {
|
|
198
|
+
const el = await createElement("note");
|
|
199
|
+
el._title = "Test Title";
|
|
200
|
+
el._body = "Test Body";
|
|
201
|
+
el._rating = 4;
|
|
202
|
+
el._attachedText = "Some attached text";
|
|
203
|
+
|
|
204
|
+
const data = el.getData();
|
|
205
|
+
expect(data.title).toBe("Test Title");
|
|
206
|
+
expect(data.body).toBe("Test Body");
|
|
207
|
+
expect(data.rating).toBe(4);
|
|
208
|
+
expect(data.attachedText).toBe("Some attached text");
|
|
209
|
+
expect(data.url).toBe("");
|
|
210
|
+
expect(data.quoteText).toBe("");
|
|
211
|
+
expect(data.quoteAuthor).toBe("");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("reset clears all fields", async () => {
|
|
215
|
+
const el = await createElement("note");
|
|
216
|
+
el._title = "Test";
|
|
217
|
+
el._body = "Body";
|
|
218
|
+
el._rating = 3;
|
|
219
|
+
el._showRating = true;
|
|
220
|
+
el._attachedText = "text";
|
|
221
|
+
el._showAttachedText = true;
|
|
222
|
+
|
|
223
|
+
el.reset();
|
|
224
|
+
|
|
225
|
+
expect(el._title).toBe("");
|
|
226
|
+
expect(el._body).toBe("");
|
|
227
|
+
expect(el._rating).toBe(0);
|
|
228
|
+
expect(el._showRating).toBe(false);
|
|
229
|
+
expect(el._attachedText).toBe("");
|
|
230
|
+
expect(el._showAttachedText).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("shows attached text badge when text is present", async () => {
|
|
234
|
+
const el = await createElement("note");
|
|
235
|
+
el._attachedText = "Some content here";
|
|
236
|
+
await el.updateComplete;
|
|
237
|
+
|
|
238
|
+
const badge = el.querySelector(".compose-attached-badge");
|
|
239
|
+
expect(badge).not.toBeNull();
|
|
240
|
+
expect(badge?.textContent).toContain("chars");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("media button highlights when attachments are present", async () => {
|
|
244
|
+
const el = await createElement("note");
|
|
245
|
+
|
|
246
|
+
// Media button should not be active initially
|
|
247
|
+
const mediaBtn = el.querySelector<HTMLButtonElement>(".compose-tool-btn");
|
|
248
|
+
expect(mediaBtn?.classList.contains("compose-tool-btn-active")).toBe(false);
|
|
249
|
+
|
|
250
|
+
// Add an attachment
|
|
251
|
+
const blob = new Blob(["fake"], { type: "image/png" });
|
|
252
|
+
const file = new File([blob], "test.png", { type: "image/png" });
|
|
253
|
+
el._attachments = [
|
|
254
|
+
{
|
|
255
|
+
clientId: "test-1",
|
|
256
|
+
file,
|
|
257
|
+
previewUrl: URL.createObjectURL(blob),
|
|
258
|
+
status: "done",
|
|
259
|
+
mediaId: "m1",
|
|
260
|
+
alt: "",
|
|
261
|
+
error: null,
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
await el.updateComplete;
|
|
265
|
+
|
|
266
|
+
const mediaBtnAfter =
|
|
267
|
+
el.querySelector<HTMLButtonElement>(".compose-tool-btn");
|
|
268
|
+
expect(mediaBtnAfter?.classList.contains("compose-tool-btn-active")).toBe(
|
|
269
|
+
true,
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
4
|
+
import type {
|
|
5
|
+
PostFormLabels,
|
|
6
|
+
PostFormInitial,
|
|
7
|
+
PostCollectionOption,
|
|
8
|
+
PostMediaItem,
|
|
9
|
+
PostSubmitDetail,
|
|
10
|
+
} from "../post-form-types.js";
|
|
11
|
+
import "../jant-post-form.js";
|
|
12
|
+
import type { JantPostForm } from "../jant-post-form.js";
|
|
13
|
+
|
|
14
|
+
const labels: PostFormLabels = {
|
|
15
|
+
formatLabel: "Format",
|
|
16
|
+
noteOption: "Note",
|
|
17
|
+
linkOption: "Link",
|
|
18
|
+
quoteOption: "Quote",
|
|
19
|
+
titleLabel: "Title",
|
|
20
|
+
titlePlaceholder: "Title...",
|
|
21
|
+
bodyLabel: "Body",
|
|
22
|
+
bodyPlaceholder: "Body...",
|
|
23
|
+
urlLabel: "URL",
|
|
24
|
+
urlPlaceholder: "https://example.com",
|
|
25
|
+
quoteTextLabel: "Quote Text",
|
|
26
|
+
quoteTextPlaceholder: "Quote...",
|
|
27
|
+
mediaLabel: "Media",
|
|
28
|
+
mediaAddButton: "Add Media",
|
|
29
|
+
mediaRemoveButton: "Remove",
|
|
30
|
+
mediaEmptyLabel: "No media",
|
|
31
|
+
statusLabel: "Status",
|
|
32
|
+
statusPublished: "Published",
|
|
33
|
+
statusDraft: "Draft",
|
|
34
|
+
featuredLabel: "Featured",
|
|
35
|
+
pinnedLabel: "Pinned",
|
|
36
|
+
collectionsLabel: "Collections",
|
|
37
|
+
submitLabel: "Publish",
|
|
38
|
+
cancelLabel: "Cancel",
|
|
39
|
+
mediaDialogTitle: "Select Media",
|
|
40
|
+
mediaDialogDone: "Done",
|
|
41
|
+
mediaDialogLoading: "Loading...",
|
|
42
|
+
submitSuccessMessage: "Saved!",
|
|
43
|
+
submitErrorMessage: "Failed.",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const initial: PostFormInitial = {
|
|
47
|
+
format: "note",
|
|
48
|
+
title: "",
|
|
49
|
+
body: "",
|
|
50
|
+
url: "",
|
|
51
|
+
quoteText: "",
|
|
52
|
+
status: "published",
|
|
53
|
+
featured: false,
|
|
54
|
+
pinned: false,
|
|
55
|
+
rating: 0,
|
|
56
|
+
collectionIds: [],
|
|
57
|
+
mediaIds: [],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const collections: PostCollectionOption[] = [
|
|
61
|
+
{ id: 1, title: "General", icon: null },
|
|
62
|
+
{ id: 2, title: "Favorites", icon: "★" },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const media: PostMediaItem[] = [
|
|
66
|
+
{ id: "m1", thumbUrl: "https://cdn.example.com/m1.jpg", alt: "Media 1" },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
async function createElement(
|
|
70
|
+
overrides: Partial<JantPostForm> = {},
|
|
71
|
+
): Promise<JantPostForm> {
|
|
72
|
+
const el = document.createElement("jant-post-form") as JantPostForm;
|
|
73
|
+
el.labels = { ...labels };
|
|
74
|
+
el.initial = { ...initial };
|
|
75
|
+
el.collections = [...collections];
|
|
76
|
+
el.media = [...media];
|
|
77
|
+
el.action = "/dash/posts";
|
|
78
|
+
Object.assign(el, overrides);
|
|
79
|
+
document.body.appendChild(el);
|
|
80
|
+
await el.updateComplete;
|
|
81
|
+
return el;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("JantPostForm", () => {
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
document.body.innerHTML = "";
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("renders base fields and labels", async () => {
|
|
90
|
+
const el = await createElement();
|
|
91
|
+
const select = el.querySelector("select.select");
|
|
92
|
+
expect(select).not.toBeNull();
|
|
93
|
+
const label = el.querySelector(".field .label");
|
|
94
|
+
expect(label?.textContent?.trim()).toBe("Format");
|
|
95
|
+
const submit = el.querySelector<HTMLButtonElement>("button[type=submit]");
|
|
96
|
+
expect(submit?.textContent?.trim()).toContain("Publish");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("shows quote textarea when format set to quote", async () => {
|
|
100
|
+
const el = await createElement({
|
|
101
|
+
initial: { ...initial, format: "quote" },
|
|
102
|
+
});
|
|
103
|
+
await el.updateComplete;
|
|
104
|
+
const textarea = el.querySelector<HTMLTextAreaElement>(
|
|
105
|
+
"textarea[placeholder='Quote...']",
|
|
106
|
+
);
|
|
107
|
+
expect(textarea).not.toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("dispatches jant:post-submit with form data", async () => {
|
|
111
|
+
const el = await createElement();
|
|
112
|
+
const form = el.querySelector("form");
|
|
113
|
+
expect(form).not.toBeNull();
|
|
114
|
+
if (!form) throw new Error("Form element not found");
|
|
115
|
+
|
|
116
|
+
const titleInput = el.querySelector<HTMLInputElement>("input.input");
|
|
117
|
+
expect(titleInput).not.toBeNull();
|
|
118
|
+
if (!titleInput) throw new Error("Title input not found");
|
|
119
|
+
titleInput.value = "Sample Post";
|
|
120
|
+
titleInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
121
|
+
|
|
122
|
+
const bodyTextarea =
|
|
123
|
+
el.querySelector<HTMLTextAreaElement>("textarea.textarea");
|
|
124
|
+
expect(bodyTextarea).not.toBeNull();
|
|
125
|
+
if (!bodyTextarea) throw new Error("Body textarea not found");
|
|
126
|
+
bodyTextarea.value = "Hello world";
|
|
127
|
+
bodyTextarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
128
|
+
|
|
129
|
+
const checkboxList =
|
|
130
|
+
el.querySelectorAll<HTMLInputElement>("input.checkbox");
|
|
131
|
+
expect(checkboxList.length).toBeGreaterThan(0);
|
|
132
|
+
const checkbox = checkboxList[0];
|
|
133
|
+
checkbox.checked = true;
|
|
134
|
+
checkbox.dispatchEvent(new Event("change", { bubbles: true }));
|
|
135
|
+
|
|
136
|
+
const collectionCheckbox = checkboxList.item(2);
|
|
137
|
+
expect(collectionCheckbox).not.toBeNull();
|
|
138
|
+
if (!collectionCheckbox) throw new Error("Collection checkbox missing");
|
|
139
|
+
collectionCheckbox.checked = true;
|
|
140
|
+
collectionCheckbox.dispatchEvent(new Event("change", { bubbles: true }));
|
|
141
|
+
|
|
142
|
+
el.mediaIds = ["m1"];
|
|
143
|
+
|
|
144
|
+
let detail: PostSubmitDetail | null = null;
|
|
145
|
+
el.addEventListener("jant:post-submit", (event) => {
|
|
146
|
+
detail = (event as CustomEvent<PostSubmitDetail>).detail;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
form.dispatchEvent(
|
|
150
|
+
new Event("submit", { bubbles: true, cancelable: true }),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(detail).not.toBeNull();
|
|
154
|
+
const d = detail as unknown as PostSubmitDetail;
|
|
155
|
+
expect(d.endpoint).toBe("/dash/posts");
|
|
156
|
+
expect(d.data.title).toBe("Sample Post");
|
|
157
|
+
expect(d.data.body).toBe("Hello world");
|
|
158
|
+
expect(d.data.featured).toBe(true);
|
|
159
|
+
expect(d.data.collectionIds).toEqual([collections[0].id]);
|
|
160
|
+
expect(d.data.mediaIds).toEqual(["m1"]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("updates mediaIds when setter called", async () => {
|
|
164
|
+
const el = await createElement();
|
|
165
|
+
el.mediaIds = ["m1"];
|
|
166
|
+
await el.updateComplete;
|
|
167
|
+
|
|
168
|
+
const items = el.querySelectorAll("[data-media-id]");
|
|
169
|
+
expect(items.length).toBe(1);
|
|
170
|
+
expect(items[0].getAttribute("data-media-id")).toBe("m1");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
4
|
+
import type {
|
|
5
|
+
AvatarRemoveDetail,
|
|
6
|
+
SettingsLabels,
|
|
7
|
+
SettingsSaveDetail,
|
|
8
|
+
} from "../settings-types.js";
|
|
9
|
+
import "../jant-settings-avatar.js";
|
|
10
|
+
import type { JantSettingsAvatar } from "../jant-settings-avatar.js";
|
|
11
|
+
|
|
12
|
+
function requireElement<T extends globalThis.Element>(
|
|
13
|
+
element: T | null,
|
|
14
|
+
message: string,
|
|
15
|
+
): T {
|
|
16
|
+
if (!element) {
|
|
17
|
+
throw new Error(message);
|
|
18
|
+
}
|
|
19
|
+
return element;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const labels: SettingsLabels = {
|
|
23
|
+
blogAvatar: "Blog Avatar",
|
|
24
|
+
uploadAvatar: "Upload Avatar",
|
|
25
|
+
remove: "Remove",
|
|
26
|
+
avatarHelp: "For best results, upload a square image.",
|
|
27
|
+
displayInHeader: "Display avatar in my site header",
|
|
28
|
+
processing: "Processing...",
|
|
29
|
+
uploading: "Uploading...",
|
|
30
|
+
uploadError: "Upload failed.",
|
|
31
|
+
general: "General",
|
|
32
|
+
siteName: "Site Name",
|
|
33
|
+
aboutBlog: "About this blog",
|
|
34
|
+
aboutBlogHelp: "Displayed above your blog posts.",
|
|
35
|
+
language: "Language",
|
|
36
|
+
defaultHomepageView: "Default Homepage View",
|
|
37
|
+
latest: "Latest",
|
|
38
|
+
featured: "Featured",
|
|
39
|
+
timeZone: "Time Zone",
|
|
40
|
+
siteFooter: "Site Footer",
|
|
41
|
+
markdownSupported: "Markdown supported",
|
|
42
|
+
footerHelp: "Displayed at the bottom of posts.",
|
|
43
|
+
seo: "SEO",
|
|
44
|
+
allowIndexing: "It's OK for search engines to index my site",
|
|
45
|
+
save: "Save",
|
|
46
|
+
cancel: "Cancel",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
async function createElement(
|
|
50
|
+
avatarUrl = "",
|
|
51
|
+
showInHeader = false,
|
|
52
|
+
): Promise<JantSettingsAvatar> {
|
|
53
|
+
const el = document.createElement(
|
|
54
|
+
"jant-settings-avatar",
|
|
55
|
+
) as JantSettingsAvatar;
|
|
56
|
+
el.avatarUrl = avatarUrl;
|
|
57
|
+
el.showInHeader = showInHeader;
|
|
58
|
+
el.labels = labels;
|
|
59
|
+
document.body.appendChild(el);
|
|
60
|
+
await el.updateComplete;
|
|
61
|
+
return el;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Toggle a checkbox reliably in happy-dom by explicitly dispatching change */
|
|
65
|
+
function toggleCheckbox(checkbox: HTMLInputElement) {
|
|
66
|
+
checkbox.checked = !checkbox.checked;
|
|
67
|
+
checkbox.dispatchEvent(new Event("change", { bubbles: true }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Find the save button (not the upload label which also has .btn) */
|
|
71
|
+
function findSaveBtn(el: HTMLElement): HTMLButtonElement | null {
|
|
72
|
+
return el.querySelector<HTMLButtonElement>("button.btn:not(.btn-outline)");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Find the cancel button */
|
|
76
|
+
function findCancelBtn(el: HTMLElement): HTMLButtonElement | null {
|
|
77
|
+
return el.querySelector<HTMLButtonElement>("button.btn-outline");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("JantSettingsAvatar", () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
document.body.innerHTML = "";
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("renders card with Blog Avatar heading", async () => {
|
|
86
|
+
const el = await createElement();
|
|
87
|
+
const heading = el.querySelector("h2");
|
|
88
|
+
expect(heading?.textContent).toBe("Blog Avatar");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("renders placeholder when no avatar URL", async () => {
|
|
92
|
+
const el = await createElement();
|
|
93
|
+
const img = el.querySelector("img");
|
|
94
|
+
expect(img).toBeNull();
|
|
95
|
+
const placeholder = el.querySelector(".bg-muted");
|
|
96
|
+
expect(placeholder).not.toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("renders avatar image when URL provided", async () => {
|
|
100
|
+
const el = await createElement("https://example.com/avatar.png");
|
|
101
|
+
const img = el.querySelector("img");
|
|
102
|
+
expect(img).not.toBeNull();
|
|
103
|
+
expect(img?.src).toBe("https://example.com/avatar.png");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("shows Remove button when avatar exists", async () => {
|
|
107
|
+
const el = await createElement("https://example.com/avatar.png");
|
|
108
|
+
const buttons = el.querySelectorAll("button");
|
|
109
|
+
const removeBtn = Array.from(buttons).find((b) =>
|
|
110
|
+
b.textContent?.includes("Remove"),
|
|
111
|
+
);
|
|
112
|
+
expect(removeBtn).not.toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("hides Remove button when no avatar", async () => {
|
|
116
|
+
const el = await createElement();
|
|
117
|
+
const buttons = el.querySelectorAll("button");
|
|
118
|
+
const removeBtn = Array.from(buttons).find((b) =>
|
|
119
|
+
b.textContent?.includes("Remove"),
|
|
120
|
+
);
|
|
121
|
+
expect(removeBtn).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("renders display-in-header checkbox with correct state", async () => {
|
|
125
|
+
const el = await createElement("", true);
|
|
126
|
+
const checkbox = el.querySelector<HTMLInputElement>(
|
|
127
|
+
'input[type="checkbox"]',
|
|
128
|
+
);
|
|
129
|
+
expect(checkbox?.checked).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("toggling checkbox marks form as dirty", async () => {
|
|
133
|
+
const el = await createElement("", false);
|
|
134
|
+
const checkbox = requireElement(
|
|
135
|
+
el.querySelector<HTMLInputElement>('input[type="checkbox"]'),
|
|
136
|
+
"expected header display checkbox",
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
toggleCheckbox(checkbox);
|
|
140
|
+
await el.updateComplete;
|
|
141
|
+
|
|
142
|
+
const saveBtn = findSaveBtn(el);
|
|
143
|
+
expect(saveBtn?.disabled).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("cancel reverts checkbox to original state", async () => {
|
|
147
|
+
const el = await createElement("", false);
|
|
148
|
+
const checkbox = requireElement(
|
|
149
|
+
el.querySelector<HTMLInputElement>('input[type="checkbox"]'),
|
|
150
|
+
"expected header display checkbox",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
toggleCheckbox(checkbox);
|
|
154
|
+
await el.updateComplete;
|
|
155
|
+
|
|
156
|
+
const cancelBtn = findCancelBtn(el);
|
|
157
|
+
cancelBtn?.click();
|
|
158
|
+
await el.updateComplete;
|
|
159
|
+
|
|
160
|
+
expect(checkbox.checked).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("dispatches jant:settings-save on save click", async () => {
|
|
164
|
+
const el = await createElement("", false);
|
|
165
|
+
const checkbox = requireElement(
|
|
166
|
+
el.querySelector<HTMLInputElement>('input[type="checkbox"]'),
|
|
167
|
+
"expected header display checkbox",
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
toggleCheckbox(checkbox);
|
|
171
|
+
await el.updateComplete;
|
|
172
|
+
|
|
173
|
+
let detail: SettingsSaveDetail | null = null;
|
|
174
|
+
el.addEventListener("jant:settings-save", (event) => {
|
|
175
|
+
const customEvent = event as CustomEvent<SettingsSaveDetail>;
|
|
176
|
+
detail = customEvent.detail;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const saveBtn = findSaveBtn(el);
|
|
180
|
+
saveBtn?.click();
|
|
181
|
+
await el.updateComplete;
|
|
182
|
+
|
|
183
|
+
expect(detail).not.toBeNull();
|
|
184
|
+
const d = detail as unknown as SettingsSaveDetail;
|
|
185
|
+
expect(d.endpoint).toBe("/dash/settings/avatar/display");
|
|
186
|
+
expect(d.section).toBe("avatar-display");
|
|
187
|
+
expect(d.data.showHeaderAvatar).toBe("true");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("dispatches jant:avatar-remove on remove click", async () => {
|
|
191
|
+
const el = await createElement("https://example.com/avatar.png");
|
|
192
|
+
|
|
193
|
+
let detail: AvatarRemoveDetail | null = null;
|
|
194
|
+
el.addEventListener("jant:avatar-remove", (event) => {
|
|
195
|
+
const customEvent = event as CustomEvent<AvatarRemoveDetail>;
|
|
196
|
+
detail = customEvent.detail;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const buttons = el.querySelectorAll("button");
|
|
200
|
+
const removeBtn = Array.from(buttons).find((b) =>
|
|
201
|
+
b.textContent?.includes("Remove"),
|
|
202
|
+
);
|
|
203
|
+
removeBtn?.click();
|
|
204
|
+
|
|
205
|
+
expect(detail).not.toBeNull();
|
|
206
|
+
const d = detail as unknown as AvatarRemoveDetail;
|
|
207
|
+
expect(d.endpoint).toBe("/dash/settings/avatar/remove");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("saved() resets dirty state", async () => {
|
|
211
|
+
const el = await createElement("", false);
|
|
212
|
+
const checkbox = requireElement(
|
|
213
|
+
el.querySelector<HTMLInputElement>('input[type="checkbox"]'),
|
|
214
|
+
"expected header display checkbox",
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
toggleCheckbox(checkbox);
|
|
218
|
+
await el.updateComplete;
|
|
219
|
+
|
|
220
|
+
el.saved();
|
|
221
|
+
await el.updateComplete;
|
|
222
|
+
|
|
223
|
+
const saveBtn = findSaveBtn(el);
|
|
224
|
+
expect(saveBtn?.disabled).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("renders file input with data-avatar-upload attribute", async () => {
|
|
228
|
+
const el = await createElement();
|
|
229
|
+
const fileInput = el.querySelector<HTMLInputElement>(
|
|
230
|
+
"input[data-avatar-upload]",
|
|
231
|
+
);
|
|
232
|
+
expect(fileInput).not.toBeNull();
|
|
233
|
+
expect(fileInput?.type).toBe("file");
|
|
234
|
+
});
|
|
235
|
+
});
|