@jant/core 0.6.14 → 0.6.16
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/import-site.js +88 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +7 -7
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +316 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +82 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
- package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- 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-manager-types.ts +9 -3
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +55 -3
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +14 -7
- package/src/client/components/jant-compose-dialog.ts +1542 -694
- package/src/client/components/jant-compose-editor.ts +237 -99
- package/src/client/components/jant-compose-fullscreen.ts +27 -9
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +177 -95
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +147 -123
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +133 -39
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +35 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +35 -0
- package/src/db/pg/schema.ts +46 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +48 -20
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +222 -51
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +216 -45
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +216 -45
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +24 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +15 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +193 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +47 -8
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +137 -131
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +359 -83
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +58 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +14 -10
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +246 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +48 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +8 -3
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +176 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +90 -22
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +86 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +190 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +249 -39
- package/src/services/__tests__/post.test.ts +844 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +224 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +94 -85
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +1142 -342
- package/src/services/search.ts +17 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -14
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +76 -10
- package/src/styles/ui.css +2227 -1347
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +14 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +20 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +22 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +109 -19
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +49 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +38 -13
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +52 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -7,6 +7,8 @@ import type { Editor } from "@tiptap/core";
|
|
|
7
7
|
import { MAX_THREAD_POSTS } from "../../../types.js";
|
|
8
8
|
|
|
9
9
|
import type {
|
|
10
|
+
ComposeFullscreenCloseDetail,
|
|
11
|
+
ComposeFullscreenOpenDetail,
|
|
10
12
|
ComposeLabels,
|
|
11
13
|
ComposeCollection,
|
|
12
14
|
ComposeSubmitDetail,
|
|
@@ -67,6 +69,45 @@ async function flushUpdates(el?: JantComposeDialog) {
|
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
async function openPublishPanel(el: JantComposeDialog) {
|
|
73
|
+
requireElement(
|
|
74
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
75
|
+
"expected publish settings toggle",
|
|
76
|
+
).click();
|
|
77
|
+
await flushUpdates(el);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Options-panel rows are found by their title — the drafts entries moved off
|
|
81
|
+
* the title bar and into that panel. */
|
|
82
|
+
function sheetRow(
|
|
83
|
+
el: JantComposeDialog,
|
|
84
|
+
title: string,
|
|
85
|
+
): HTMLButtonElement | null {
|
|
86
|
+
return (
|
|
87
|
+
Array.from(
|
|
88
|
+
el.querySelectorAll<HTMLButtonElement>(
|
|
89
|
+
".compose-publish-panel .compose-sheet-row",
|
|
90
|
+
),
|
|
91
|
+
).find(
|
|
92
|
+
(row) =>
|
|
93
|
+
row.querySelector(".compose-sheet-title")?.textContent?.trim() ===
|
|
94
|
+
title,
|
|
95
|
+
) ?? null
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sheetRowTitles(el: JantComposeDialog): (string | undefined)[] {
|
|
100
|
+
return Array.from(
|
|
101
|
+
el.querySelectorAll<HTMLElement>(
|
|
102
|
+
".compose-publish-panel .compose-sheet-title",
|
|
103
|
+
),
|
|
104
|
+
).map((n) => n.textContent?.trim());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function draftsRow(el: JantComposeDialog): HTMLButtonElement | null {
|
|
108
|
+
return sheetRow(el, "Drafts");
|
|
109
|
+
}
|
|
110
|
+
|
|
70
111
|
function deferred<T>() {
|
|
71
112
|
let resolve!: (value: T) => void;
|
|
72
113
|
const promise = new Promise<T>((res) => {
|
|
@@ -181,6 +222,8 @@ const labels: ComposeLabels = {
|
|
|
181
222
|
discardChangesConfirm: "Discard changes?",
|
|
182
223
|
drafts: "Drafts",
|
|
183
224
|
draftsEmpty: "No drafts yet. Save a draft to find it here.",
|
|
225
|
+
previewDraft: "Preview",
|
|
226
|
+
draftActions: "Draft actions",
|
|
184
227
|
deleteDraft: "Delete Draft",
|
|
185
228
|
draftDeleted: "Draft deleted.",
|
|
186
229
|
publishFailedDraft: "Couldn't publish. Saved as draft.",
|
|
@@ -207,13 +250,13 @@ const labels: ComposeLabels = {
|
|
|
207
250
|
publishDateInvalid: "Enter a valid date.",
|
|
208
251
|
publishDateFutureError:
|
|
209
252
|
"Choose today or an earlier date, or leave it blank to publish now.",
|
|
210
|
-
publishDateSummaryNow: "
|
|
253
|
+
publishDateSummaryNow: "Now",
|
|
211
254
|
publishDateSummaryAction: "Edit publish date",
|
|
212
255
|
publishSlugLabel: "Custom link",
|
|
213
256
|
publishSlugPlaceholder: "your-post-link",
|
|
214
257
|
publishSlugHint: "Leave blank to generate one automatically.",
|
|
215
258
|
publishSlugAuto: "Generate automatically",
|
|
216
|
-
publishSlugSummaryAuto: "Auto
|
|
259
|
+
publishSlugSummaryAuto: "Auto",
|
|
217
260
|
publishSlugSummaryAction: "Edit custom link",
|
|
218
261
|
publishSlugReset: "Reset link",
|
|
219
262
|
publishSlugSuggested: "Suggested link",
|
|
@@ -229,10 +272,24 @@ const labels: ComposeLabels = {
|
|
|
229
272
|
threadLimitReached: "Threads can include up to 20 posts.",
|
|
230
273
|
showMore: "Show more",
|
|
231
274
|
showLess: "Show less",
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
275
|
+
closeCompose: "Close compose",
|
|
276
|
+
editing: "Editing",
|
|
277
|
+
composeDialogLabel: "Compose",
|
|
235
278
|
slashHint: "Type / for commands",
|
|
279
|
+
tableControls: {
|
|
280
|
+
toolbarLabel: "Table controls",
|
|
281
|
+
addRowAbove: "Add row above",
|
|
282
|
+
addRowBelow: "Add row below",
|
|
283
|
+
addColumnBefore: "Add column before",
|
|
284
|
+
addColumnAfter: "Add column after",
|
|
285
|
+
options: "Table options",
|
|
286
|
+
deleteRow: "Delete row",
|
|
287
|
+
deleteColumn: "Delete column",
|
|
288
|
+
toggleHeaderRow: "Toggle header row",
|
|
289
|
+
deleteTable: "Delete table",
|
|
290
|
+
sizePickerLabel: "Choose table size",
|
|
291
|
+
insertTableSize: "Insert %rows% by %cols% table",
|
|
292
|
+
},
|
|
236
293
|
collectionFormLabels: {
|
|
237
294
|
titleLabel: "Title",
|
|
238
295
|
titlePlaceholder: "My Collection",
|
|
@@ -261,6 +318,35 @@ const collections: ComposeCollection[] = [
|
|
|
261
318
|
{ id: "col-2", title: "Movies", slug: "movies" },
|
|
262
319
|
];
|
|
263
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Date and permalink hang off the post itself, not the publish panel. Both
|
|
323
|
+
* fields are open as soon as the panel is, so there is nothing further to
|
|
324
|
+
* expand. Idempotent.
|
|
325
|
+
*/
|
|
326
|
+
async function openPostMeta(el: JantComposeDialog, index = 0) {
|
|
327
|
+
const pill = el.querySelectorAll<HTMLButtonElement>(
|
|
328
|
+
"[data-compose-post-meta-pill]",
|
|
329
|
+
)[index];
|
|
330
|
+
if (pill && pill.getAttribute("aria-expanded") !== "true") pill.click();
|
|
331
|
+
// The slug suggestion is fetched when the panel opens; let it settle.
|
|
332
|
+
await flushUpdates(el);
|
|
333
|
+
await flushUpdates(el);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The date or permalink as the post's own pill states it. */
|
|
337
|
+
function pillValue(
|
|
338
|
+
el: JantComposeDialog,
|
|
339
|
+
kind: "date" | "slug",
|
|
340
|
+
index = 0,
|
|
341
|
+
): string | undefined {
|
|
342
|
+
const pill = el.querySelectorAll("[data-compose-post-meta-pill]")[index];
|
|
343
|
+
const selector =
|
|
344
|
+
kind === "date"
|
|
345
|
+
? ".compose-post-meta-value:not(.compose-post-meta-value-slug)"
|
|
346
|
+
: ".compose-post-meta-value-slug";
|
|
347
|
+
return pill?.querySelector(selector)?.textContent?.trim();
|
|
348
|
+
}
|
|
349
|
+
|
|
264
350
|
async function createElement(
|
|
265
351
|
cols: ComposeCollection[] = collections,
|
|
266
352
|
): Promise<JantComposeDialog> {
|
|
@@ -280,6 +366,7 @@ describe("JantComposeDialog", () => {
|
|
|
280
366
|
vi.restoreAllMocks();
|
|
281
367
|
document.body.innerHTML = "";
|
|
282
368
|
globalThis.localStorage.clear();
|
|
369
|
+
delete document.documentElement.dataset.sitePathPrefix;
|
|
283
370
|
Object.defineProperty(globalThis, "matchMedia", {
|
|
284
371
|
configurable: true,
|
|
285
372
|
writable: true,
|
|
@@ -312,8 +399,11 @@ describe("JantComposeDialog", () => {
|
|
|
312
399
|
it("renders with collections and labels", async () => {
|
|
313
400
|
const el = await createElement();
|
|
314
401
|
|
|
315
|
-
//
|
|
316
|
-
|
|
402
|
+
// No title bar: the composer opens straight onto the post header row. A new
|
|
403
|
+
// post gets no marker either — an empty composer already says that much.
|
|
404
|
+
expect(el.querySelector(".compose-dialog-header")).toBeNull();
|
|
405
|
+
expect(el.querySelector(".compose-thread-post-header")).not.toBeNull();
|
|
406
|
+
expect(el.querySelector(".compose-post-badge")).toBeNull();
|
|
317
407
|
|
|
318
408
|
// Format buttons present
|
|
319
409
|
const segmentedItems = el.querySelectorAll(".compose-segmented-item");
|
|
@@ -331,7 +421,7 @@ describe("JantComposeDialog", () => {
|
|
|
331
421
|
expect(postBtn.disabled).toBe(true);
|
|
332
422
|
expect(
|
|
333
423
|
requireElement(
|
|
334
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
424
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
335
425
|
"expected publish settings toggle",
|
|
336
426
|
).disabled,
|
|
337
427
|
).toBe(false);
|
|
@@ -363,7 +453,6 @@ describe("JantComposeDialog", () => {
|
|
|
363
453
|
],
|
|
364
454
|
},
|
|
365
455
|
title: "",
|
|
366
|
-
showTitle: false,
|
|
367
456
|
selection: { from: 3, to: 3 },
|
|
368
457
|
replyExpanded: false,
|
|
369
458
|
},
|
|
@@ -471,7 +560,7 @@ describe("JantComposeDialog", () => {
|
|
|
471
560
|
const el = await createElement();
|
|
472
561
|
|
|
473
562
|
requireElement(
|
|
474
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
563
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
475
564
|
"expected publish settings toggle",
|
|
476
565
|
).click();
|
|
477
566
|
await el.updateComplete;
|
|
@@ -479,16 +568,39 @@ describe("JantComposeDialog", () => {
|
|
|
479
568
|
expect(el.querySelector(".compose-publish-panel")).not.toBeNull();
|
|
480
569
|
});
|
|
481
570
|
|
|
482
|
-
it("
|
|
571
|
+
it("offers saving separately from browsing once there is something to save", async () => {
|
|
483
572
|
const el = await createElement();
|
|
573
|
+
await openPublishPanel(el);
|
|
484
574
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
575
|
+
// Nothing written yet, so there is only a place to go.
|
|
576
|
+
expect(sheetRowTitles(el)).toContain("Drafts");
|
|
577
|
+
expect(sheetRowTitles(el)).not.toContain("Save as draft");
|
|
578
|
+
|
|
579
|
+
const editor = requireElement(
|
|
580
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
581
|
+
"expected compose editor",
|
|
488
582
|
);
|
|
489
|
-
|
|
490
|
-
|
|
583
|
+
editor._bodyJson = {
|
|
584
|
+
type: "doc",
|
|
585
|
+
content: [
|
|
586
|
+
{ type: "paragraph", content: [{ type: "text", text: "Hello world" }] },
|
|
587
|
+
],
|
|
588
|
+
};
|
|
589
|
+
await editor.updateComplete;
|
|
590
|
+
await flushUpdates(el);
|
|
491
591
|
|
|
592
|
+
// Now both: an action on this post, and the list. Neither label lies about
|
|
593
|
+
// what its row does.
|
|
594
|
+
const titles = sheetRowTitles(el);
|
|
595
|
+
expect(titles).toContain("Save as draft");
|
|
596
|
+
expect(titles).toContain("Drafts");
|
|
597
|
+
expect(titles.indexOf("Save as draft")).toBeLessThan(
|
|
598
|
+
titles.indexOf("Drafts"),
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("saves straight to a draft from its own row, with no prompt in the way", async () => {
|
|
603
|
+
const el = await createElement();
|
|
492
604
|
const editor = requireElement(
|
|
493
605
|
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
494
606
|
"expected compose editor",
|
|
@@ -500,34 +612,299 @@ describe("JantComposeDialog", () => {
|
|
|
500
612
|
],
|
|
501
613
|
};
|
|
502
614
|
await editor.updateComplete;
|
|
503
|
-
await el
|
|
615
|
+
await flushUpdates(el);
|
|
504
616
|
|
|
505
|
-
|
|
506
|
-
|
|
617
|
+
let detail: ComposeSubmitDetail | null = null;
|
|
618
|
+
el.addEventListener("jant:compose-submit-deferred", (event) => {
|
|
619
|
+
detail = (event as CustomEvent<ComposeSubmitDetail>).detail;
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
await openPublishPanel(el);
|
|
623
|
+
requireElement(
|
|
624
|
+
sheetRow(el, "Save as draft"),
|
|
625
|
+
"expected save-as-draft row",
|
|
626
|
+
).click();
|
|
627
|
+
await flushUpdates(el);
|
|
628
|
+
|
|
629
|
+
expect(el._confirmPanelOpen).toBe(false);
|
|
630
|
+
expect(el._draftsPanelOpen).toBe(false);
|
|
631
|
+
expect(detail).not.toBeNull();
|
|
632
|
+
expect(detail!.status).toBe("draft");
|
|
507
633
|
});
|
|
508
634
|
|
|
509
|
-
it("
|
|
635
|
+
it("opens options from its own button, not a chevron on Publish", async () => {
|
|
510
636
|
const el = await createElement();
|
|
511
637
|
|
|
638
|
+
// The split button is gone: Publish is one button, options is another.
|
|
639
|
+
expect(el.querySelector(".compose-publish-toggle")).toBeNull();
|
|
640
|
+
|
|
641
|
+
const trigger = requireElement(
|
|
642
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
643
|
+
"expected options trigger",
|
|
644
|
+
);
|
|
645
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
646
|
+
expect(el.querySelector(".compose-sheet")).toBeNull();
|
|
647
|
+
|
|
648
|
+
trigger.click();
|
|
649
|
+
await flushUpdates(el);
|
|
650
|
+
|
|
651
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("true");
|
|
652
|
+
expect(el.querySelector(".compose-sheet")).not.toBeNull();
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("keeps date and permalink on the post, not in the publish panel", async () => {
|
|
656
|
+
const el = await createElement();
|
|
657
|
+
|
|
658
|
+
// They describe one post, so they hang off the post — not the publish
|
|
659
|
+
// panel, which speaks for the whole submission.
|
|
512
660
|
requireElement(
|
|
513
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
661
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
662
|
+
"expected options trigger",
|
|
663
|
+
).click();
|
|
664
|
+
await flushUpdates(el);
|
|
665
|
+
expect(el.querySelector(".compose-publish-date-input")).toBeNull();
|
|
666
|
+
expect(el.querySelector(".compose-publish-slug-input")).toBeNull();
|
|
667
|
+
|
|
668
|
+
// The pill states both values while the panel is shut.
|
|
669
|
+
expect(pillValue(el, "date")).toBe("Now");
|
|
670
|
+
expect(pillValue(el, "slug")).toBe("Auto");
|
|
671
|
+
|
|
672
|
+
// Two settings is not a list to drill through: opening the pill opens both
|
|
673
|
+
// fields at once, with focus on the panel so Tab lands in them.
|
|
674
|
+
await openPostMeta(el);
|
|
675
|
+
expect(el.querySelector(".compose-publish-date-input")).not.toBeNull();
|
|
676
|
+
expect(el.querySelector(".compose-publish-slug-input")).not.toBeNull();
|
|
677
|
+
expect(el.querySelector("[data-compose-post-meta-panel]")).toBe(
|
|
678
|
+
document.activeElement,
|
|
679
|
+
);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("pins the date panel to its pill instead of inside the scroller", async () => {
|
|
683
|
+
const el = await createElement();
|
|
684
|
+
|
|
685
|
+
// The pill lives in `.compose-scroll`, which clips and scrolls. An
|
|
686
|
+
// absolutely positioned panel is taller than that box on an empty post, so
|
|
687
|
+
// it lost its footer, and focusing it scrolled the format switcher off the
|
|
688
|
+
// top to bring it into view. Fixed positioning leaves the scroller behind,
|
|
689
|
+
// which makes placing the panel our job.
|
|
690
|
+
const focusCalls: Array<{
|
|
691
|
+
target: unknown;
|
|
692
|
+
options?: globalThis.FocusOptions;
|
|
693
|
+
}> = [];
|
|
694
|
+
const nativeFocus = globalThis.HTMLElement.prototype.focus;
|
|
695
|
+
globalThis.HTMLElement.prototype.focus = function (
|
|
696
|
+
this: globalThis.HTMLElement,
|
|
697
|
+
options?: globalThis.FocusOptions,
|
|
698
|
+
) {
|
|
699
|
+
focusCalls.push({ target: this, options });
|
|
700
|
+
return nativeFocus.call(this, options);
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
try {
|
|
704
|
+
await openPostMeta(el);
|
|
705
|
+
} finally {
|
|
706
|
+
globalThis.HTMLElement.prototype.focus = nativeFocus;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const panel = requireElement(
|
|
710
|
+
el.querySelector<globalThis.HTMLElement>(
|
|
711
|
+
"[data-compose-post-meta-panel]",
|
|
712
|
+
),
|
|
713
|
+
"expected post meta panel",
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
expect(focusCalls.find((call) => call.target === panel)?.options).toEqual({
|
|
717
|
+
preventScroll: true,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Placed under the pill, and capped at the room the viewport actually has
|
|
721
|
+
// so a short window scrolls the panel rather than cropping it.
|
|
722
|
+
expect(panel.style.top).toBe("6px");
|
|
723
|
+
expect(panel.style.left).not.toBe("");
|
|
724
|
+
expect(
|
|
725
|
+
panel.style.getPropertyValue("--compose-post-meta-panel-max-height"),
|
|
726
|
+
).toBe(`${globalThis.innerHeight - 12 - 6}px`);
|
|
727
|
+
|
|
728
|
+
// Inline coordinates only mean anything if the panel is out of the
|
|
729
|
+
// scroller — `position: absolute` here is the bug, not a style choice.
|
|
730
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
731
|
+
expect(css).toMatch(
|
|
732
|
+
/\.compose-post-meta-panel\s*\{\s*position:\s*fixed;[\s\S]*max-height:\s*var\(--compose-post-meta-panel-max-height,\s*none\);\s*overflow-y:\s*auto;/,
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("keeps the date panel on its pill when the composer scrolls", async () => {
|
|
737
|
+
const el = await createElement();
|
|
738
|
+
await openPostMeta(el);
|
|
739
|
+
|
|
740
|
+
const panel = requireElement(
|
|
741
|
+
el.querySelector<globalThis.HTMLElement>(
|
|
742
|
+
"[data-compose-post-meta-panel]",
|
|
743
|
+
),
|
|
744
|
+
"expected post meta panel",
|
|
745
|
+
);
|
|
746
|
+
const pill = requireElement(
|
|
747
|
+
el.querySelector<globalThis.HTMLElement>("[data-compose-post-meta-pill]"),
|
|
748
|
+
"expected post meta pill",
|
|
749
|
+
);
|
|
750
|
+
pill.getBoundingClientRect = () =>
|
|
751
|
+
({
|
|
752
|
+
top: 100,
|
|
753
|
+
bottom: 130,
|
|
754
|
+
left: 300,
|
|
755
|
+
right: 400,
|
|
756
|
+
width: 100,
|
|
757
|
+
height: 30,
|
|
758
|
+
}) as globalThis.DOMRect;
|
|
759
|
+
|
|
760
|
+
// A `scroll` event on the composer's own box never reaches `window` — it
|
|
761
|
+
// doesn't bubble — so a pinned panel would hang in place while the pill it
|
|
762
|
+
// belongs to slid away underneath it.
|
|
763
|
+
const scroller = requireElement(
|
|
764
|
+
el.querySelector(".compose-scroll"),
|
|
765
|
+
"expected compose scroller",
|
|
766
|
+
);
|
|
767
|
+
scroller.dispatchEvent(new globalThis.Event("scroll"));
|
|
768
|
+
|
|
769
|
+
expect(panel.style.top).toBe("136px");
|
|
770
|
+
expect(panel.style.left).toBe("400px");
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("opens the calendar from its own button, not the UA glyph", async () => {
|
|
774
|
+
const el = await createElement();
|
|
775
|
+
await openPostMeta(el);
|
|
776
|
+
|
|
777
|
+
const input = requireElement(
|
|
778
|
+
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
779
|
+
"expected publish date input",
|
|
780
|
+
);
|
|
781
|
+
// Chrome only opens the picker from its ~13px indicator, which is hidden
|
|
782
|
+
// here — the button has to drive the same picker itself.
|
|
783
|
+
const showPicker = vi.fn();
|
|
784
|
+
(input as HTMLInputElement & { showPicker: () => void }).showPicker =
|
|
785
|
+
showPicker;
|
|
786
|
+
|
|
787
|
+
requireElement(
|
|
788
|
+
el.querySelector<HTMLButtonElement>("[data-compose-date-picker]"),
|
|
789
|
+
"expected date picker button",
|
|
790
|
+
).click();
|
|
791
|
+
await flushUpdates(el);
|
|
792
|
+
|
|
793
|
+
expect(showPicker).toHaveBeenCalledTimes(1);
|
|
794
|
+
expect(input).toBe(document.activeElement);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it("keeps the date field usable when showPicker is unsupported", async () => {
|
|
798
|
+
const el = await createElement();
|
|
799
|
+
await openPostMeta(el);
|
|
800
|
+
|
|
801
|
+
const input = requireElement(
|
|
802
|
+
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
803
|
+
"expected publish date input",
|
|
804
|
+
);
|
|
805
|
+
// Engines without `showPicker` must still get a focused, typable field
|
|
806
|
+
// rather than an uncaught TypeError from the button.
|
|
807
|
+
expect(
|
|
808
|
+
(input as HTMLInputElement & { showPicker?: () => void }).showPicker,
|
|
809
|
+
).toBeUndefined();
|
|
810
|
+
|
|
811
|
+
expect(() =>
|
|
812
|
+
requireElement(
|
|
813
|
+
el.querySelector<HTMLButtonElement>("[data-compose-date-picker]"),
|
|
814
|
+
"expected date picker button",
|
|
815
|
+
).click(),
|
|
816
|
+
).not.toThrow();
|
|
817
|
+
await flushUpdates(el);
|
|
818
|
+
|
|
819
|
+
expect(input).toBe(document.activeElement);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it("closes the date panel from its Done button", async () => {
|
|
823
|
+
const el = await createElement();
|
|
824
|
+
await openPostMeta(el);
|
|
825
|
+
|
|
826
|
+
// Escape and an outside click both close it, but neither announces itself,
|
|
827
|
+
// and touch has no Escape key at all.
|
|
828
|
+
requireElement(
|
|
829
|
+
el.querySelector<HTMLButtonElement>("[data-compose-post-meta-done]"),
|
|
830
|
+
"expected Done button",
|
|
831
|
+
).click();
|
|
832
|
+
await flushUpdates(el);
|
|
833
|
+
|
|
834
|
+
expect(el.querySelector("[data-compose-post-meta-panel]")).toBeNull();
|
|
835
|
+
expect(el.querySelector(".compose-publish-date-input")).toBeNull();
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it("hands focus back to the post when the date panel closes", async () => {
|
|
839
|
+
const el = await createElement();
|
|
840
|
+
await openPostMeta(el);
|
|
841
|
+
expect(el.querySelector("[data-compose-post-meta-panel]")).toBe(
|
|
842
|
+
document.activeElement,
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
// The panel holds focus while it is open, so it would take focus out of the
|
|
846
|
+
// document with it on the way out. The pill is only visible on hover, focus
|
|
847
|
+
// or a non-default value — and on touch there is no hover — so losing focus
|
|
848
|
+
// to `<body>` hides the only way back in.
|
|
849
|
+
const editor = requireElement(
|
|
850
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
851
|
+
"expected editor",
|
|
852
|
+
);
|
|
853
|
+
const focusInput = vi.spyOn(editor, "focusInput");
|
|
854
|
+
|
|
855
|
+
requireElement(
|
|
856
|
+
el.querySelector<HTMLButtonElement>("[data-compose-post-meta-pill]"),
|
|
857
|
+
"expected post meta pill",
|
|
858
|
+
).click();
|
|
859
|
+
await flushUpdates(el);
|
|
860
|
+
|
|
861
|
+
expect(el.querySelector("[data-compose-post-meta-panel]")).toBeNull();
|
|
862
|
+
expect(focusInput).toHaveBeenCalled();
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it("explains every visibility option and checks the selected one", async () => {
|
|
866
|
+
const el = await createElement();
|
|
867
|
+
|
|
868
|
+
requireElement(
|
|
869
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
514
870
|
"expected publish settings toggle",
|
|
515
871
|
).click();
|
|
516
872
|
await el.updateComplete;
|
|
517
873
|
|
|
518
|
-
//
|
|
874
|
+
// Each row carries its own hint, so the options can be compared before
|
|
875
|
+
// one is picked — a single hint under a chip group could not do that.
|
|
519
876
|
expect(el.textContent).toContain("Appears in Latest.");
|
|
520
|
-
expect(el.textContent).
|
|
877
|
+
expect(el.textContent).toContain("Only visible when signed in.");
|
|
521
878
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
".compose-publish-chip[role='radio']",
|
|
879
|
+
const rows = el.querySelectorAll<HTMLButtonElement>(
|
|
880
|
+
".compose-sheet-row[role='radio']",
|
|
525
881
|
);
|
|
526
|
-
|
|
527
|
-
|
|
882
|
+
expect(rows).toHaveLength(3);
|
|
883
|
+
expect(rows[0].getAttribute("aria-checked")).toBe("true");
|
|
884
|
+
expect(rows[0].querySelector(".compose-sheet-check")).not.toBeNull();
|
|
885
|
+
expect(rows[2].querySelector(".compose-sheet-check")).toBeNull();
|
|
528
886
|
|
|
529
|
-
|
|
530
|
-
|
|
887
|
+
rows[2].click();
|
|
888
|
+
await flushUpdates(el);
|
|
889
|
+
|
|
890
|
+
// Picking is the whole job of this panel, so it dismisses itself — no
|
|
891
|
+
// Done button to confirm a radio selection.
|
|
892
|
+
expect(el._visibility).toBe("private");
|
|
893
|
+
expect(el.querySelector(".compose-sheet")).toBeNull();
|
|
894
|
+
expect(el.querySelector(".compose-publish-done")).toBeNull();
|
|
895
|
+
|
|
896
|
+
// Reopening shows the new choice checked.
|
|
897
|
+
requireElement(
|
|
898
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
899
|
+
"expected publish settings toggle",
|
|
900
|
+
).click();
|
|
901
|
+
await flushUpdates(el);
|
|
902
|
+
const updated = el.querySelectorAll<HTMLButtonElement>(
|
|
903
|
+
".compose-sheet-row[role='radio']",
|
|
904
|
+
);
|
|
905
|
+
expect(updated[2].getAttribute("aria-checked")).toBe("true");
|
|
906
|
+
expect(updated[2].querySelector(".compose-sheet-check")).not.toBeNull();
|
|
907
|
+
expect(updated[0].querySelector(".compose-sheet-check")).toBeNull();
|
|
531
908
|
});
|
|
532
909
|
|
|
533
910
|
it("format switching updates active state", async () => {
|
|
@@ -611,7 +988,7 @@ describe("JantComposeDialog", () => {
|
|
|
611
988
|
);
|
|
612
989
|
}
|
|
613
990
|
|
|
614
|
-
it("shows the format switcher
|
|
991
|
+
it("shows the format switcher above the post while editing", async () => {
|
|
615
992
|
vi.spyOn(globalThis, "requestAnimationFrame").mockImplementation((cb) => {
|
|
616
993
|
cb(0);
|
|
617
994
|
return 1;
|
|
@@ -622,12 +999,21 @@ describe("JantComposeDialog", () => {
|
|
|
622
999
|
await el.openEdit("pst_123");
|
|
623
1000
|
await flushUpdates(el);
|
|
624
1001
|
|
|
625
|
-
|
|
626
|
-
//
|
|
627
|
-
expect(el.querySelector(".compose-
|
|
1002
|
+
// The switcher has exactly one home: above its own post. Editing says so
|
|
1003
|
+
// through the submit label and a marker in the header row, not a title bar.
|
|
1004
|
+
expect(el.querySelector(".compose-thread-post-header")).toBeTruthy();
|
|
1005
|
+
expect(el.querySelector(".compose-dialog-header")).toBeNull();
|
|
1006
|
+
expect(
|
|
1007
|
+
el
|
|
1008
|
+
.querySelector(".compose-thread-post-header .compose-post-badge")
|
|
1009
|
+
?.textContent?.trim(),
|
|
1010
|
+
).toBe(labels.editing);
|
|
1011
|
+
expect(el.querySelector(".compose-publish-main")?.textContent?.trim()).toBe(
|
|
1012
|
+
labels.update,
|
|
1013
|
+
);
|
|
628
1014
|
});
|
|
629
1015
|
|
|
630
|
-
it("shows
|
|
1016
|
+
it("shows the format selector above the post when replying", async () => {
|
|
631
1017
|
const el = await createElement();
|
|
632
1018
|
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
633
1019
|
contentHtml: "<p>Parent</p>",
|
|
@@ -635,18 +1021,50 @@ describe("JantComposeDialog", () => {
|
|
|
635
1021
|
});
|
|
636
1022
|
await flushUpdates(el);
|
|
637
1023
|
|
|
638
|
-
expect(el.querySelector(".compose-dialog-
|
|
639
|
-
|
|
640
|
-
);
|
|
641
|
-
// The header center no longer hosts the format selector...
|
|
642
|
-
expect(
|
|
643
|
-
el.querySelector(".compose-dialog-header-center .compose-segmented"),
|
|
644
|
-
).toBeNull();
|
|
645
|
-
// ...it sits inline above the reply editor instead.
|
|
1024
|
+
expect(el.querySelector(".compose-dialog-header")).toBeNull();
|
|
1025
|
+
// The selector sits inline above the reply editor.
|
|
646
1026
|
expect(el.querySelector(".compose-thread-post-header")).not.toBeNull();
|
|
647
1027
|
expect(
|
|
648
1028
|
el.querySelector(".compose-thread-layout.compose-reply-compose-layout"),
|
|
649
1029
|
).not.toBeNull();
|
|
1030
|
+
expect(el.querySelector(".compose-collection-trigger")).toBeNull();
|
|
1031
|
+
expect(
|
|
1032
|
+
el.querySelector(".compose-action-row-without-collection"),
|
|
1033
|
+
).not.toBeNull();
|
|
1034
|
+
expect(el.querySelector(".compose-publish-main")).not.toBeNull();
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
it("puts a reply's add-to-thread row on the rail with the other posts", async () => {
|
|
1038
|
+
const el = await createElement();
|
|
1039
|
+
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
1040
|
+
contentHtml: "<p>Parent</p>",
|
|
1041
|
+
dateText: "Mar 14",
|
|
1042
|
+
});
|
|
1043
|
+
await flushUpdates(el);
|
|
1044
|
+
|
|
1045
|
+
// Inside the layout, not a sibling of it: out there it misses the rail's
|
|
1046
|
+
// indent and lands a rail's width left of everything above it.
|
|
1047
|
+
const layout = el.querySelector(
|
|
1048
|
+
".compose-thread-layout.compose-reply-compose-layout",
|
|
1049
|
+
);
|
|
1050
|
+
const addRow = layout?.querySelector(":scope > .compose-thread-add-row");
|
|
1051
|
+
expect(addRow).not.toBeNull();
|
|
1052
|
+
expect(el.querySelector(".compose-add-thread-trigger")).toBeNull();
|
|
1053
|
+
// The dashed dot marks where the next post lands, as in thread compose.
|
|
1054
|
+
expect(addRow?.querySelector(".compose-thread-add-dot")).not.toBeNull();
|
|
1055
|
+
// ...and it stays the last row, so the rail still stops at the reply.
|
|
1056
|
+
expect(layout?.lastElementChild).toBe(addRow);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it("keeps a single post's add-to-thread row off the rail", async () => {
|
|
1060
|
+
const el = await createElement();
|
|
1061
|
+
await flushUpdates(el);
|
|
1062
|
+
|
|
1063
|
+
// No rail to join, so no dot: the row's position says "next slot" alone.
|
|
1064
|
+
const trigger = el.querySelector(".compose-add-thread-trigger");
|
|
1065
|
+
expect(trigger).not.toBeNull();
|
|
1066
|
+
expect(el.querySelector(".compose-thread-add-dot")).toBeNull();
|
|
1067
|
+
expect(trigger?.querySelector(".compose-thread-add-btn")).not.toBeNull();
|
|
650
1068
|
});
|
|
651
1069
|
|
|
652
1070
|
it("shows an Edit title with the format selector above the post when editing a reply", async () => {
|
|
@@ -668,6 +1086,7 @@ describe("JantComposeDialog", () => {
|
|
|
668
1086
|
title: "Hello",
|
|
669
1087
|
body: null,
|
|
670
1088
|
replyToId: parentId,
|
|
1089
|
+
collectionIds: ["col-thread"],
|
|
671
1090
|
};
|
|
672
1091
|
return Promise.resolve(
|
|
673
1092
|
new Response(JSON.stringify(json), {
|
|
@@ -680,13 +1099,307 @@ describe("JantComposeDialog", () => {
|
|
|
680
1099
|
await el.openEdit("pst_123");
|
|
681
1100
|
await flushUpdates(el);
|
|
682
1101
|
|
|
683
|
-
expect(el.querySelector(".compose-
|
|
684
|
-
|
|
1102
|
+
expect(el.querySelector(".compose-publish-main")?.textContent?.trim()).toBe(
|
|
1103
|
+
labels.update,
|
|
685
1104
|
);
|
|
686
|
-
expect(
|
|
687
|
-
el.querySelector(".compose-dialog-header-center .compose-segmented"),
|
|
688
|
-
).toBeNull();
|
|
1105
|
+
expect(el.querySelector(".compose-dialog-header")).toBeNull();
|
|
689
1106
|
expect(el.querySelector(".compose-thread-post-header")).not.toBeNull();
|
|
1107
|
+
expect(el._collectionIds).toEqual([]);
|
|
1108
|
+
expect(el.querySelector(".compose-collection-trigger")).toBeNull();
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it("keeps the format selector above the post in single-post mode", async () => {
|
|
1112
|
+
const el = await createElement();
|
|
1113
|
+
await flushUpdates(el);
|
|
1114
|
+
|
|
1115
|
+
// One selector, one place — above its own post.
|
|
1116
|
+
const items = el.querySelectorAll<HTMLButtonElement>(
|
|
1117
|
+
".compose-thread-post-header .compose-segmented-item",
|
|
1118
|
+
);
|
|
1119
|
+
expect(items.length).toBe(3);
|
|
1120
|
+
|
|
1121
|
+
items[1].click(); // link
|
|
1122
|
+
await flushUpdates(el);
|
|
1123
|
+
expect(el._format).toBe("link");
|
|
1124
|
+
|
|
1125
|
+
// A single post has no "which one", so no position marker.
|
|
1126
|
+
expect(el.querySelector(".compose-post-position")).toBeNull();
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it("gives every thread post its own date and permalink control", async () => {
|
|
1130
|
+
const el = await createElement();
|
|
1131
|
+
el._threadItems = [
|
|
1132
|
+
{ id: "thread-1", format: "note" },
|
|
1133
|
+
{ id: "thread-2", format: "note" },
|
|
1134
|
+
];
|
|
1135
|
+
await flushUpdates(el);
|
|
1136
|
+
|
|
1137
|
+
// Every post is separately addressable (`path_registry` has a row each),
|
|
1138
|
+
// so every post gets both controls — not just the root.
|
|
1139
|
+
const pills = el.querySelectorAll("[data-compose-post-meta-pill]");
|
|
1140
|
+
expect(pills).toHaveLength(2);
|
|
1141
|
+
for (const pill of pills) {
|
|
1142
|
+
expect(
|
|
1143
|
+
pill.querySelector(".compose-post-meta-value-slug"),
|
|
1144
|
+
).not.toBeNull();
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it("sends a reply's own permalink and blocks an invalid one", async () => {
|
|
1149
|
+
const el = await createElement();
|
|
1150
|
+
el._threadItems = [
|
|
1151
|
+
{ id: "thread-1", format: "note" },
|
|
1152
|
+
{ id: "thread-2", format: "note", slug: "part-two" },
|
|
1153
|
+
];
|
|
1154
|
+
await flushUpdates(el);
|
|
1155
|
+
|
|
1156
|
+
const build = (
|
|
1157
|
+
el as unknown as {
|
|
1158
|
+
_buildEditorPostDetail: (
|
|
1159
|
+
editor: JantComposeEditor,
|
|
1160
|
+
format: string,
|
|
1161
|
+
index: number,
|
|
1162
|
+
status: string,
|
|
1163
|
+
) => ComposeSubmitDetail;
|
|
1164
|
+
}
|
|
1165
|
+
)._buildEditorPostDetail;
|
|
1166
|
+
const editors = el.querySelectorAll<JantComposeEditor>(
|
|
1167
|
+
"jant-compose-editor",
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
expect(build.call(el, editors[1], "note", 1, "published").slug).toBe(
|
|
1171
|
+
"part-two",
|
|
1172
|
+
);
|
|
1173
|
+
expect(build.call(el, editors[0], "note", 0, "published").slug).toBe(
|
|
1174
|
+
undefined,
|
|
1175
|
+
);
|
|
1176
|
+
|
|
1177
|
+
// A reply's bad slug blocks publish just like the root's would.
|
|
1178
|
+
el._threadItems = [
|
|
1179
|
+
{ id: "thread-1", format: "note" },
|
|
1180
|
+
{ id: "thread-2", format: "note", slug: "not a slug!" },
|
|
1181
|
+
];
|
|
1182
|
+
await flushUpdates(el);
|
|
1183
|
+
expect(
|
|
1184
|
+
(el as unknown as { _canPublish: () => boolean })._canPublish(),
|
|
1185
|
+
).toBe(false);
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
it("sends a reply's own date and leaves the rest for the server to inherit", async () => {
|
|
1189
|
+
const el = await createElement();
|
|
1190
|
+
el._threadItems = [
|
|
1191
|
+
{ id: "thread-1", format: "note" },
|
|
1192
|
+
{ id: "thread-2", format: "note", publishedAtInput: "2024-03-09" },
|
|
1193
|
+
{ id: "thread-3", format: "note" },
|
|
1194
|
+
];
|
|
1195
|
+
await flushUpdates(el);
|
|
1196
|
+
|
|
1197
|
+
const detail = (
|
|
1198
|
+
el as unknown as {
|
|
1199
|
+
_buildEditorPostDetail: (
|
|
1200
|
+
editor: JantComposeEditor,
|
|
1201
|
+
format: string,
|
|
1202
|
+
index: number,
|
|
1203
|
+
status: string,
|
|
1204
|
+
) => ComposeSubmitDetail;
|
|
1205
|
+
}
|
|
1206
|
+
)._buildEditorPostDetail;
|
|
1207
|
+
const editors = el.querySelectorAll<JantComposeEditor>(
|
|
1208
|
+
"jant-compose-editor",
|
|
1209
|
+
);
|
|
1210
|
+
|
|
1211
|
+
const second = detail.call(el, editors[1], "note", 1, "published");
|
|
1212
|
+
const third = detail.call(el, editors[2], "note", 2, "published");
|
|
1213
|
+
|
|
1214
|
+
expect(second.publishedAt).toBeGreaterThan(0);
|
|
1215
|
+
// Undefined, not "now" — the server fills it from the root.
|
|
1216
|
+
expect(third.publishedAt).toBeUndefined();
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
it("blocks publishing when any thread post has a future date", async () => {
|
|
1220
|
+
const el = await createElement();
|
|
1221
|
+
const future = new Date(Date.now() + 86400000 * 3)
|
|
1222
|
+
.toISOString()
|
|
1223
|
+
.slice(0, 10);
|
|
1224
|
+
el._threadItems = [
|
|
1225
|
+
{ id: "thread-1", format: "note" },
|
|
1226
|
+
{ id: "thread-2", format: "note", publishedAtInput: future },
|
|
1227
|
+
];
|
|
1228
|
+
await flushUpdates(el);
|
|
1229
|
+
|
|
1230
|
+
expect(
|
|
1231
|
+
(el as unknown as { _canPublish: () => boolean })._canPublish(),
|
|
1232
|
+
).toBe(false);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Publish reads each row's own answer, so a row that has been removed stops
|
|
1237
|
+
* being asked. These used to fail: the button was computed during render from
|
|
1238
|
+
* the editors still in the DOM — including the one that render was about to
|
|
1239
|
+
* remove.
|
|
1240
|
+
*/
|
|
1241
|
+
describe("publish reflects the rows that are left", () => {
|
|
1242
|
+
const publishButton = (el: JantComposeDialog) =>
|
|
1243
|
+
requireElement(
|
|
1244
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
1245
|
+
"expected publish button",
|
|
1246
|
+
);
|
|
1247
|
+
|
|
1248
|
+
async function typeInto(
|
|
1249
|
+
el: JantComposeDialog,
|
|
1250
|
+
index: number,
|
|
1251
|
+
text: string,
|
|
1252
|
+
) {
|
|
1253
|
+
const editor = el.querySelectorAll<JantComposeEditor>(
|
|
1254
|
+
"jant-compose-editor",
|
|
1255
|
+
)[index];
|
|
1256
|
+
requireEditor(
|
|
1257
|
+
requireElement(editor, `expected editor ${index}`),
|
|
1258
|
+
).commands.insertContent(text);
|
|
1259
|
+
await flushUpdates(el);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
async function addPost(el: JantComposeDialog) {
|
|
1263
|
+
requireElement(
|
|
1264
|
+
el.querySelector<HTMLButtonElement>(".compose-thread-add-btn"),
|
|
1265
|
+
"expected add-to-thread button",
|
|
1266
|
+
).click();
|
|
1267
|
+
await flushUpdates(el);
|
|
1268
|
+
await flushUpdates(el);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
async function removePost(el: JantComposeDialog, index: number) {
|
|
1272
|
+
requireElement(
|
|
1273
|
+
el.querySelectorAll<HTMLButtonElement>(".compose-thread-post-remove")[
|
|
1274
|
+
index
|
|
1275
|
+
],
|
|
1276
|
+
`expected remove button ${index}`,
|
|
1277
|
+
).click();
|
|
1278
|
+
await flushUpdates(el);
|
|
1279
|
+
await flushUpdates(el);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
it("re-enables publish when the empty last post is removed", async () => {
|
|
1283
|
+
const el = await createElement();
|
|
1284
|
+
await typeInto(el, 0, "one");
|
|
1285
|
+
await addPost(el);
|
|
1286
|
+
await typeInto(el, 1, "two");
|
|
1287
|
+
await addPost(el);
|
|
1288
|
+
|
|
1289
|
+
expect(publishButton(el).disabled).toBe(true);
|
|
1290
|
+
|
|
1291
|
+
await removePost(el, 2);
|
|
1292
|
+
|
|
1293
|
+
expect(publishButton(el).disabled).toBe(false);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
it("re-enables publish when the emptied post is the first of two", async () => {
|
|
1297
|
+
const el = await createElement();
|
|
1298
|
+
await typeInto(el, 0, "one");
|
|
1299
|
+
await addPost(el);
|
|
1300
|
+
await typeInto(el, 1, "two");
|
|
1301
|
+
|
|
1302
|
+
requireEditor(
|
|
1303
|
+
el.querySelectorAll<JantComposeEditor>("jant-compose-editor")[0],
|
|
1304
|
+
).commands.clearContent();
|
|
1305
|
+
await flushUpdates(el);
|
|
1306
|
+
expect(publishButton(el).disabled).toBe(true);
|
|
1307
|
+
|
|
1308
|
+
await removePost(el, 0);
|
|
1309
|
+
|
|
1310
|
+
expect(publishButton(el).disabled).toBe(false);
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
it("keeps each post's own text when the middle post is removed", async () => {
|
|
1314
|
+
const el = await createElement();
|
|
1315
|
+
await typeInto(el, 0, "AAA");
|
|
1316
|
+
await addPost(el);
|
|
1317
|
+
await typeInto(el, 1, "BBB");
|
|
1318
|
+
await addPost(el);
|
|
1319
|
+
await typeInto(el, 2, "CCC");
|
|
1320
|
+
|
|
1321
|
+
await removePost(el, 1);
|
|
1322
|
+
|
|
1323
|
+
const text = Array.from(
|
|
1324
|
+
el.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
|
|
1325
|
+
).map((editor) => requireEditor(editor).getText());
|
|
1326
|
+
expect(text).toEqual(["AAA", "CCC"]);
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
it("numbers each post in a thread", async () => {
|
|
1331
|
+
const el = await createElement();
|
|
1332
|
+
el._threadItems = [
|
|
1333
|
+
{ id: "thread-1", format: "note" },
|
|
1334
|
+
{ id: "thread-2", format: "note" },
|
|
1335
|
+
];
|
|
1336
|
+
await flushUpdates(el);
|
|
1337
|
+
|
|
1338
|
+
const positions = () =>
|
|
1339
|
+
Array.from(el.querySelectorAll(".compose-post-position")).map((n) =>
|
|
1340
|
+
n.textContent?.trim(),
|
|
1341
|
+
);
|
|
1342
|
+
expect(positions()).toEqual(["1/2", "2/2"]);
|
|
1343
|
+
|
|
1344
|
+
(el as unknown as { _addThreadItem: () => void })._addThreadItem();
|
|
1345
|
+
await flushUpdates(el);
|
|
1346
|
+
|
|
1347
|
+
// The total re-renders on every post, not just the new one.
|
|
1348
|
+
expect(positions()).toEqual(["1/3", "2/3", "3/3"]);
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
const positionsIn = (el: JantComposeDialog) =>
|
|
1352
|
+
Array.from(el.querySelectorAll(".compose-post-position")).map((n) =>
|
|
1353
|
+
n.textContent?.trim(),
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
it("numbers a reply from the end of the thread it continues", async () => {
|
|
1357
|
+
// The parent is the second post of a thread, so the reply is the third —
|
|
1358
|
+
// not the first, and not the second.
|
|
1359
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
1360
|
+
new Response(JSON.stringify({ id: "pst_parent", threadPosition: 2 }), {
|
|
1361
|
+
headers: { "Content-Type": "application/json" },
|
|
1362
|
+
}),
|
|
1363
|
+
);
|
|
1364
|
+
|
|
1365
|
+
const el = await createElement();
|
|
1366
|
+
await el.openReply("pst_parent", {
|
|
1367
|
+
contentHtml: "<p>Parent</p>",
|
|
1368
|
+
dateText: "Mar 14",
|
|
1369
|
+
});
|
|
1370
|
+
await flushUpdates(el);
|
|
1371
|
+
await flushUpdates(el);
|
|
1372
|
+
|
|
1373
|
+
expect(positionsIn(el)).toEqual(["3/3"]);
|
|
1374
|
+
|
|
1375
|
+
(el as unknown as { _addThreadItem: () => void })._addThreadItem();
|
|
1376
|
+
await flushUpdates(el);
|
|
1377
|
+
|
|
1378
|
+
expect(positionsIn(el)).toEqual(["3/4", "4/4"]);
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
it("shows no number for a reply until the parent's position is known", async () => {
|
|
1382
|
+
// Counting from 1 and then correcting to 3 is worse than arriving late, so
|
|
1383
|
+
// the marker stays away while the lookup is in flight — and stays away for
|
|
1384
|
+
// good if it fails.
|
|
1385
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("offline"));
|
|
1386
|
+
|
|
1387
|
+
const el = await createElement();
|
|
1388
|
+
await el.openReply("pst_parent", {
|
|
1389
|
+
contentHtml: "<p>Parent</p>",
|
|
1390
|
+
dateText: "Mar 14",
|
|
1391
|
+
});
|
|
1392
|
+
await flushUpdates(el);
|
|
1393
|
+
await flushUpdates(el);
|
|
1394
|
+
|
|
1395
|
+
expect(positionsIn(el)).toEqual([]);
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
it("leaves a lone new post unnumbered — there is no chain to place it in", async () => {
|
|
1399
|
+
const el = await createElement();
|
|
1400
|
+
await flushUpdates(el);
|
|
1401
|
+
|
|
1402
|
+
expect(el.querySelector(".compose-post-position")).toBeNull();
|
|
690
1403
|
});
|
|
691
1404
|
|
|
692
1405
|
it("switches format from the inline selector when replying", async () => {
|
|
@@ -1013,13 +1726,13 @@ describe("JantComposeDialog", () => {
|
|
|
1013
1726
|
await editor.updateComplete;
|
|
1014
1727
|
|
|
1015
1728
|
requireElement(
|
|
1016
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
1729
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1017
1730
|
"expected publish settings toggle",
|
|
1018
1731
|
).click();
|
|
1019
1732
|
await el.updateComplete;
|
|
1020
1733
|
|
|
1021
1734
|
const options = el.querySelectorAll<HTMLButtonElement>(
|
|
1022
|
-
".compose-
|
|
1735
|
+
".compose-sheet-row[role='radio']",
|
|
1023
1736
|
);
|
|
1024
1737
|
expect(options).toHaveLength(3);
|
|
1025
1738
|
options[1]?.click();
|
|
@@ -1073,11 +1786,12 @@ describe("JantComposeDialog", () => {
|
|
|
1073
1786
|
await editor.updateComplete;
|
|
1074
1787
|
|
|
1075
1788
|
requireElement(
|
|
1076
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
1789
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1077
1790
|
"expected publish settings toggle",
|
|
1078
1791
|
).click();
|
|
1079
1792
|
await el.updateComplete;
|
|
1080
1793
|
|
|
1794
|
+
await openPostMeta(el);
|
|
1081
1795
|
const publishedAtInput = requireElement(
|
|
1082
1796
|
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
1083
1797
|
"expected publish date input",
|
|
@@ -1118,16 +1832,17 @@ describe("JantComposeDialog", () => {
|
|
|
1118
1832
|
}
|
|
1119
1833
|
});
|
|
1120
1834
|
|
|
1121
|
-
it("
|
|
1835
|
+
it("keeps the chosen publish date readable on the post's pill", async () => {
|
|
1122
1836
|
const el = await createElement();
|
|
1123
1837
|
|
|
1124
1838
|
const publishToggle = requireElement(
|
|
1125
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
1839
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1126
1840
|
"expected publish settings toggle",
|
|
1127
1841
|
);
|
|
1128
1842
|
publishToggle.click();
|
|
1129
1843
|
await el.updateComplete;
|
|
1130
1844
|
|
|
1845
|
+
await openPostMeta(el);
|
|
1131
1846
|
const publishedAtInput = requireElement(
|
|
1132
1847
|
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
1133
1848
|
"expected publish date input",
|
|
@@ -1136,31 +1851,25 @@ describe("JantComposeDialog", () => {
|
|
|
1136
1851
|
publishedAtInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1137
1852
|
await el.updateComplete;
|
|
1138
1853
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
"[data-compose-publish-date-summary]",
|
|
1145
|
-
),
|
|
1146
|
-
"expected publish date summary",
|
|
1147
|
-
);
|
|
1148
|
-
expect(summary.dataset.publishDate).toBe("2024-01-15");
|
|
1149
|
-
expect(summary.textContent).toContain("2024");
|
|
1150
|
-
|
|
1151
|
-
summary.click();
|
|
1152
|
-
await el.updateComplete;
|
|
1854
|
+
// Close the panel: the chosen date stays readable on the pill.
|
|
1855
|
+
requireElement(
|
|
1856
|
+
el.querySelector<HTMLButtonElement>("[data-compose-post-meta-pill]"),
|
|
1857
|
+
"expected post meta pill",
|
|
1858
|
+
).click();
|
|
1153
1859
|
await flushUpdates(el);
|
|
1860
|
+
expect(el.querySelector(".compose-publish-date-input")).toBeNull();
|
|
1861
|
+
expect(pillValue(el, "date")).toContain("2024");
|
|
1154
1862
|
|
|
1863
|
+
await openPostMeta(el);
|
|
1155
1864
|
expect(
|
|
1156
1865
|
requireElement(
|
|
1157
1866
|
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
1158
1867
|
"expected publish date input after reopening publish settings",
|
|
1159
|
-
),
|
|
1160
|
-
).toBe(
|
|
1868
|
+
).value,
|
|
1869
|
+
).toBe("2024-01-15");
|
|
1161
1870
|
});
|
|
1162
1871
|
|
|
1163
|
-
it("
|
|
1872
|
+
it("shows the existing publish date on the post pill while editing a post", async () => {
|
|
1164
1873
|
const el = await createElement();
|
|
1165
1874
|
(
|
|
1166
1875
|
el as unknown as {
|
|
@@ -1186,10 +1895,12 @@ describe("JantComposeDialog", () => {
|
|
|
1186
1895
|
el.requestUpdate();
|
|
1187
1896
|
await el.updateComplete;
|
|
1188
1897
|
|
|
1189
|
-
|
|
1898
|
+
await openPostMeta(el);
|
|
1899
|
+
|
|
1900
|
+
expect(pillValue(el, "date")).toContain("2024");
|
|
1190
1901
|
});
|
|
1191
1902
|
|
|
1192
|
-
it("shows
|
|
1903
|
+
it("shows Now on the post date control after clearing the date while editing", async () => {
|
|
1193
1904
|
const el = await createElement();
|
|
1194
1905
|
(
|
|
1195
1906
|
el as unknown as {
|
|
@@ -1215,15 +1926,12 @@ describe("JantComposeDialog", () => {
|
|
|
1215
1926
|
el.requestUpdate();
|
|
1216
1927
|
await el.updateComplete;
|
|
1217
1928
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
"expected publish now summary",
|
|
1222
|
-
).textContent,
|
|
1223
|
-
).toContain("Publish now");
|
|
1929
|
+
await openPostMeta(el);
|
|
1930
|
+
|
|
1931
|
+
expect(pillValue(el, "date")).toBe("Now");
|
|
1224
1932
|
});
|
|
1225
1933
|
|
|
1226
|
-
it("
|
|
1934
|
+
it("keeps the custom link readable on the post's pill", async () => {
|
|
1227
1935
|
mockSlugApi((url) => {
|
|
1228
1936
|
if (url.searchParams.get("mode") === "suggest") {
|
|
1229
1937
|
return { body: { slug: "configured-post" } };
|
|
@@ -1236,7 +1944,7 @@ describe("JantComposeDialog", () => {
|
|
|
1236
1944
|
|
|
1237
1945
|
const el = await createElement();
|
|
1238
1946
|
const publishToggle = requireElement(
|
|
1239
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
1947
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1240
1948
|
"expected publish settings toggle",
|
|
1241
1949
|
);
|
|
1242
1950
|
|
|
@@ -1244,6 +1952,7 @@ describe("JantComposeDialog", () => {
|
|
|
1244
1952
|
await el.updateComplete;
|
|
1245
1953
|
await flushUpdates(el);
|
|
1246
1954
|
|
|
1955
|
+
await openPostMeta(el);
|
|
1247
1956
|
const slugInput = requireElement(
|
|
1248
1957
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1249
1958
|
"expected custom link input",
|
|
@@ -1253,31 +1962,25 @@ describe("JantComposeDialog", () => {
|
|
|
1253
1962
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1254
1963
|
await el.updateComplete;
|
|
1255
1964
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
"[data-compose-publish-slug-summary]",
|
|
1262
|
-
),
|
|
1263
|
-
"expected custom link summary",
|
|
1264
|
-
);
|
|
1265
|
-
expect(summary.dataset.publishSlug).toBe("reading-notes");
|
|
1266
|
-
expect(summary.textContent).toContain("/reading-notes");
|
|
1267
|
-
|
|
1268
|
-
summary.click();
|
|
1269
|
-
await el.updateComplete;
|
|
1965
|
+
// Close the panel: the custom link stays readable on the pill.
|
|
1966
|
+
requireElement(
|
|
1967
|
+
el.querySelector<HTMLButtonElement>("[data-compose-post-meta-pill]"),
|
|
1968
|
+
"expected post meta pill",
|
|
1969
|
+
).click();
|
|
1270
1970
|
await flushUpdates(el);
|
|
1971
|
+
expect(el.querySelector(".compose-publish-slug-input")).toBeNull();
|
|
1972
|
+
expect(pillValue(el, "slug")).toBe("/reading-notes");
|
|
1271
1973
|
|
|
1974
|
+
await openPostMeta(el);
|
|
1272
1975
|
expect(
|
|
1273
1976
|
requireElement(
|
|
1274
1977
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1275
1978
|
"expected custom link input after reopening publish settings",
|
|
1276
|
-
),
|
|
1277
|
-
).toBe(
|
|
1979
|
+
).value,
|
|
1980
|
+
).toBe("reading-notes");
|
|
1278
1981
|
});
|
|
1279
1982
|
|
|
1280
|
-
it("
|
|
1983
|
+
it("shows the existing custom link on the post pill while editing a post", async () => {
|
|
1281
1984
|
const el = await createElement();
|
|
1282
1985
|
(
|
|
1283
1986
|
el as unknown as {
|
|
@@ -1303,10 +2006,12 @@ describe("JantComposeDialog", () => {
|
|
|
1303
2006
|
el.requestUpdate();
|
|
1304
2007
|
await el.updateComplete;
|
|
1305
2008
|
|
|
1306
|
-
|
|
2009
|
+
await openPostMeta(el);
|
|
2010
|
+
|
|
2011
|
+
expect(pillValue(el, "slug")).toBe("/reading-notes");
|
|
1307
2012
|
});
|
|
1308
2013
|
|
|
1309
|
-
it("shows
|
|
2014
|
+
it("shows Auto on the post permalink pill after clearing the link while editing", async () => {
|
|
1310
2015
|
const el = await createElement();
|
|
1311
2016
|
(
|
|
1312
2017
|
el as unknown as {
|
|
@@ -1332,12 +2037,9 @@ describe("JantComposeDialog", () => {
|
|
|
1332
2037
|
el.requestUpdate();
|
|
1333
2038
|
await el.updateComplete;
|
|
1334
2039
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
"expected auto link summary",
|
|
1339
|
-
).textContent,
|
|
1340
|
-
).toContain("Auto link");
|
|
2040
|
+
await openPostMeta(el);
|
|
2041
|
+
|
|
2042
|
+
expect(pillValue(el, "slug")).toBe("Auto");
|
|
1341
2043
|
});
|
|
1342
2044
|
|
|
1343
2045
|
it("blocks publishing with a future publish date", async () => {
|
|
@@ -1362,11 +2064,12 @@ describe("JantComposeDialog", () => {
|
|
|
1362
2064
|
await editor.updateComplete;
|
|
1363
2065
|
|
|
1364
2066
|
requireElement(
|
|
1365
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2067
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1366
2068
|
"expected publish settings toggle",
|
|
1367
2069
|
).click();
|
|
1368
2070
|
await el.updateComplete;
|
|
1369
2071
|
|
|
2072
|
+
await openPostMeta(el);
|
|
1370
2073
|
const publishedAtInput = requireElement(
|
|
1371
2074
|
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
1372
2075
|
"expected publish date input",
|
|
@@ -1393,13 +2096,13 @@ describe("JantComposeDialog", () => {
|
|
|
1393
2096
|
const el = await createElement();
|
|
1394
2097
|
|
|
1395
2098
|
requireElement(
|
|
1396
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2099
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1397
2100
|
"expected publish settings toggle",
|
|
1398
2101
|
).click();
|
|
1399
2102
|
await el.updateComplete;
|
|
1400
2103
|
|
|
1401
2104
|
const options = el.querySelectorAll<HTMLButtonElement>(
|
|
1402
|
-
".compose-
|
|
2105
|
+
".compose-sheet-row[role='radio']",
|
|
1403
2106
|
);
|
|
1404
2107
|
expect(options).toHaveLength(3);
|
|
1405
2108
|
options[2]?.click();
|
|
@@ -1413,28 +2116,157 @@ describe("JantComposeDialog", () => {
|
|
|
1413
2116
|
).toBe("Post privately");
|
|
1414
2117
|
});
|
|
1415
2118
|
|
|
1416
|
-
it("updates the publish button label for hidden visibility", async () => {
|
|
2119
|
+
it("updates the publish button label for hidden visibility", async () => {
|
|
2120
|
+
const el = await createElement();
|
|
2121
|
+
|
|
2122
|
+
requireElement(
|
|
2123
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
2124
|
+
"expected publish settings toggle",
|
|
2125
|
+
).click();
|
|
2126
|
+
await el.updateComplete;
|
|
2127
|
+
|
|
2128
|
+
const options = el.querySelectorAll<HTMLButtonElement>(
|
|
2129
|
+
".compose-sheet-row[role='radio']",
|
|
2130
|
+
);
|
|
2131
|
+
expect(options).toHaveLength(3);
|
|
2132
|
+
options[1]?.click();
|
|
2133
|
+
await el.updateComplete;
|
|
2134
|
+
|
|
2135
|
+
expect(
|
|
2136
|
+
requireElement(
|
|
2137
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
2138
|
+
"expected publish button",
|
|
2139
|
+
).textContent?.trim(),
|
|
2140
|
+
).toBe("Post hidden");
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
it("flips visibility from the Hide from Latest shortcut", async () => {
|
|
2144
|
+
const el = await createElement();
|
|
2145
|
+
|
|
2146
|
+
const toggle = requireElement(
|
|
2147
|
+
el.querySelector<HTMLInputElement>(
|
|
2148
|
+
".compose-quick-actions-row .compose-publish-quick-toggle-input",
|
|
2149
|
+
),
|
|
2150
|
+
"expected hide-from-latest shortcut",
|
|
2151
|
+
);
|
|
2152
|
+
expect(toggle.checked).toBe(false);
|
|
2153
|
+
|
|
2154
|
+
toggle.checked = true;
|
|
2155
|
+
toggle.dispatchEvent(new globalThis.Event("change"));
|
|
2156
|
+
await el.updateComplete;
|
|
2157
|
+
expect(el._visibility).toBe("latest_hidden");
|
|
2158
|
+
|
|
2159
|
+
toggle.checked = false;
|
|
2160
|
+
toggle.dispatchEvent(new globalThis.Event("change"));
|
|
2161
|
+
await el.updateComplete;
|
|
2162
|
+
expect(el._visibility).toBe("public");
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
it("drops the Hide from Latest shortcut once the post is private", async () => {
|
|
1417
2166
|
const el = await createElement();
|
|
1418
2167
|
|
|
1419
2168
|
requireElement(
|
|
1420
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2169
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1421
2170
|
"expected publish settings toggle",
|
|
1422
2171
|
).click();
|
|
1423
2172
|
await el.updateComplete;
|
|
1424
2173
|
|
|
1425
|
-
|
|
1426
|
-
|
|
2174
|
+
// A checkbox cannot speak for a third state, so it steps aside.
|
|
2175
|
+
el.querySelectorAll<HTMLButtonElement>(
|
|
2176
|
+
".compose-sheet-row[role='radio']",
|
|
2177
|
+
)[2]?.click();
|
|
2178
|
+
await el.updateComplete;
|
|
2179
|
+
|
|
2180
|
+
expect(el._visibility).toBe("private");
|
|
2181
|
+
expect(el.querySelector(".compose-quick-actions-row")).toBeNull();
|
|
2182
|
+
});
|
|
2183
|
+
|
|
2184
|
+
it("offers the quiet reply shortcut on a reply, in place of visibility", async () => {
|
|
2185
|
+
const el = await createElement();
|
|
2186
|
+
|
|
2187
|
+
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
2188
|
+
contentHtml: "<p>Parent</p>",
|
|
2189
|
+
dateText: "Mar 14",
|
|
2190
|
+
});
|
|
2191
|
+
await flushUpdates(el);
|
|
2192
|
+
|
|
2193
|
+
const toggles = el.querySelectorAll<HTMLInputElement>(
|
|
2194
|
+
".compose-quick-actions-row .compose-publish-quick-toggle-input",
|
|
1427
2195
|
);
|
|
1428
|
-
|
|
1429
|
-
|
|
2196
|
+
// A reply inherits the root's visibility, so only quiet reply is offered.
|
|
2197
|
+
expect(toggles).toHaveLength(1);
|
|
2198
|
+
|
|
2199
|
+
const toggle = requireElement(
|
|
2200
|
+
toggles.item(0),
|
|
2201
|
+
"expected quiet reply shortcut",
|
|
2202
|
+
);
|
|
2203
|
+
toggle.checked = true;
|
|
2204
|
+
toggle.dispatchEvent(new globalThis.Event("change"));
|
|
1430
2205
|
await el.updateComplete;
|
|
1431
2206
|
|
|
2207
|
+
expect(el._quietReply).toBe(true);
|
|
1432
2208
|
expect(
|
|
1433
2209
|
requireElement(
|
|
1434
2210
|
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
1435
2211
|
"expected publish button",
|
|
1436
2212
|
).textContent?.trim(),
|
|
1437
|
-
).toBe("
|
|
2213
|
+
).toBe("Reply quietly");
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
it("drops the quiet reply switch when editing an existing reply", async () => {
|
|
2217
|
+
const parentId = "019ce8ce-d6d8-7fda-a5df-c2da2bef5ade";
|
|
2218
|
+
vi.spyOn(globalThis, "fetch").mockImplementation((input) => {
|
|
2219
|
+
const url = String(input);
|
|
2220
|
+
const json = url.includes(parentId)
|
|
2221
|
+
? { id: parentId, bodyHtml: "<p>Parent</p>", format: "note" }
|
|
2222
|
+
: {
|
|
2223
|
+
id: "pst_123",
|
|
2224
|
+
format: "note",
|
|
2225
|
+
title: null,
|
|
2226
|
+
body: null,
|
|
2227
|
+
replyToId: parentId,
|
|
2228
|
+
};
|
|
2229
|
+
return Promise.resolve(
|
|
2230
|
+
new Response(JSON.stringify(json), {
|
|
2231
|
+
headers: { "Content-Type": "application/json" },
|
|
2232
|
+
}),
|
|
2233
|
+
);
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
const el = await createElement();
|
|
2237
|
+
await el.openEdit("pst_123");
|
|
2238
|
+
await flushUpdates(el);
|
|
2239
|
+
|
|
2240
|
+
// Saving an edit goes through the update path, which has no create-time
|
|
2241
|
+
// thread bump to skip — so the switch is gone rather than dead.
|
|
2242
|
+
expect(el.querySelector(".compose-quick-actions-row")).toBeNull();
|
|
2243
|
+
|
|
2244
|
+
el._showPublishPanel = true;
|
|
2245
|
+
await flushUpdates(el);
|
|
2246
|
+
expect(
|
|
2247
|
+
Array.from(el.querySelectorAll(".compose-sheet-title")).some(
|
|
2248
|
+
(row) => row.textContent?.trim() === labels.quietReplyLabel,
|
|
2249
|
+
),
|
|
2250
|
+
).toBe(false);
|
|
2251
|
+
});
|
|
2252
|
+
|
|
2253
|
+
it("keeps the quiet reply switch on a thread draft, which saves by recreating", async () => {
|
|
2254
|
+
const el = await createElement();
|
|
2255
|
+
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
2256
|
+
contentHtml: "<p>Parent</p>",
|
|
2257
|
+
dateText: "Mar 14",
|
|
2258
|
+
});
|
|
2259
|
+
(el as unknown as { _addThreadItem: () => void })._addThreadItem();
|
|
2260
|
+
// Re-editing a saved thread draft deletes and recreates every post, so the
|
|
2261
|
+
// create-time flag still lands.
|
|
2262
|
+
el._draftSourceId = "pst_draft";
|
|
2263
|
+
await flushUpdates(el);
|
|
2264
|
+
|
|
2265
|
+
expect(
|
|
2266
|
+
el.querySelectorAll(
|
|
2267
|
+
".compose-quick-actions-row .compose-publish-quick-toggle-input",
|
|
2268
|
+
),
|
|
2269
|
+
).toHaveLength(1);
|
|
1438
2270
|
});
|
|
1439
2271
|
|
|
1440
2272
|
it("opens a new post with the requested collection and keeps the last visibility until refresh", async () => {
|
|
@@ -1447,13 +2279,13 @@ describe("JantComposeDialog", () => {
|
|
|
1447
2279
|
expect(el._visibility).toBe("public");
|
|
1448
2280
|
|
|
1449
2281
|
requireElement(
|
|
1450
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2282
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1451
2283
|
"expected publish settings toggle",
|
|
1452
2284
|
).click();
|
|
1453
2285
|
await el.updateComplete;
|
|
1454
2286
|
|
|
1455
2287
|
const options = el.querySelectorAll<HTMLButtonElement>(
|
|
1456
|
-
".compose-
|
|
2288
|
+
".compose-sheet-row[role='radio']",
|
|
1457
2289
|
);
|
|
1458
2290
|
options[1]?.click();
|
|
1459
2291
|
await el.updateComplete;
|
|
@@ -1495,7 +2327,6 @@ describe("JantComposeDialog", () => {
|
|
|
1495
2327
|
slug: "",
|
|
1496
2328
|
visibility: "public",
|
|
1497
2329
|
rating: 0,
|
|
1498
|
-
showTitle: false,
|
|
1499
2330
|
showRating: false,
|
|
1500
2331
|
collectionIds: ["col-1"],
|
|
1501
2332
|
attachedTexts: [],
|
|
@@ -1510,6 +2341,250 @@ describe("JantComposeDialog", () => {
|
|
|
1510
2341
|
expect(el._collectionIds).toEqual(["col-2", "col-1"]);
|
|
1511
2342
|
});
|
|
1512
2343
|
|
|
2344
|
+
function seedDraftWithMedia() {
|
|
2345
|
+
globalThis.localStorage.setItem(
|
|
2346
|
+
"jant:compose-draft",
|
|
2347
|
+
JSON.stringify({
|
|
2348
|
+
format: "note",
|
|
2349
|
+
title: "",
|
|
2350
|
+
bodyJson: {
|
|
2351
|
+
type: "doc",
|
|
2352
|
+
content: [
|
|
2353
|
+
{
|
|
2354
|
+
type: "paragraph",
|
|
2355
|
+
content: [{ type: "text", text: "Draft with images" }],
|
|
2356
|
+
},
|
|
2357
|
+
],
|
|
2358
|
+
},
|
|
2359
|
+
url: "",
|
|
2360
|
+
quoteText: "",
|
|
2361
|
+
quoteAuthor: "",
|
|
2362
|
+
slug: "",
|
|
2363
|
+
visibility: "public",
|
|
2364
|
+
rating: 0,
|
|
2365
|
+
showTitle: false,
|
|
2366
|
+
showRating: false,
|
|
2367
|
+
collectionIds: [],
|
|
2368
|
+
replyToId: null,
|
|
2369
|
+
attachedTexts: [],
|
|
2370
|
+
attachmentOrder: ["client-a", "client-b"],
|
|
2371
|
+
mediaAttachments: [
|
|
2372
|
+
{
|
|
2373
|
+
clientId: "client-a",
|
|
2374
|
+
mediaId: "med_aaa",
|
|
2375
|
+
url: "/media/aaa.jpg",
|
|
2376
|
+
mimeType: "image/jpeg",
|
|
2377
|
+
name: "aaa.jpg",
|
|
2378
|
+
alt: "first",
|
|
2379
|
+
},
|
|
2380
|
+
],
|
|
2381
|
+
savedAt: Date.now(),
|
|
2382
|
+
}),
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
it("restores uploaded media from the draft snapshot plus bridge-supplied uploads", async () => {
|
|
2387
|
+
const el = await createElement();
|
|
2388
|
+
seedDraftWithMedia();
|
|
2389
|
+
|
|
2390
|
+
// client-b's upload finished after the dialog closed — only the bridge
|
|
2391
|
+
// knows about it, so it arrives via restoreMedia instead of the snapshot.
|
|
2392
|
+
await el.openNew({
|
|
2393
|
+
restoreDraft: true,
|
|
2394
|
+
restoreToast: false,
|
|
2395
|
+
restoreMedia: [
|
|
2396
|
+
{
|
|
2397
|
+
clientId: "client-b",
|
|
2398
|
+
mediaId: "med_bbb",
|
|
2399
|
+
url: "/media/bbb.png",
|
|
2400
|
+
mimeType: "image/png",
|
|
2401
|
+
name: "bbb.png",
|
|
2402
|
+
},
|
|
2403
|
+
],
|
|
2404
|
+
});
|
|
2405
|
+
await flushUpdates(el);
|
|
2406
|
+
|
|
2407
|
+
const editor = requireElement(
|
|
2408
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
2409
|
+
"expected compose editor",
|
|
2410
|
+
);
|
|
2411
|
+
const attachments = editor._attachments;
|
|
2412
|
+
expect(attachments.map((a) => a.clientId)).toEqual([
|
|
2413
|
+
"client-a",
|
|
2414
|
+
"client-b",
|
|
2415
|
+
]);
|
|
2416
|
+
expect(attachments.map((a) => a.mediaId)).toEqual(["med_aaa", "med_bbb"]);
|
|
2417
|
+
expect(attachments.every((a) => a.status === "done")).toBe(true);
|
|
2418
|
+
expect(attachments[0].remoteUrl).toBe("/media/aaa.jpg");
|
|
2419
|
+
expect(attachments[0].alt).toBe("first");
|
|
2420
|
+
expect(editor._attachmentOrder).toEqual(["client-a", "client-b"]);
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
it("suppresses the draft-restored toast when restoreToast is false", async () => {
|
|
2424
|
+
const container = document.createElement("div");
|
|
2425
|
+
container.id = "toast-container";
|
|
2426
|
+
document.body.appendChild(container);
|
|
2427
|
+
const el = await createElement();
|
|
2428
|
+
seedDraftWithMedia();
|
|
2429
|
+
|
|
2430
|
+
await el.openNew({ restoreDraft: true, restoreToast: false });
|
|
2431
|
+
await flushUpdates(el);
|
|
2432
|
+
expect(container.querySelector(".toast")).toBeNull();
|
|
2433
|
+
|
|
2434
|
+
el.reset();
|
|
2435
|
+
await el.openNew({ restoreDraft: true });
|
|
2436
|
+
await flushUpdates(el);
|
|
2437
|
+
expect(container.querySelector(".toast span")?.textContent).toBe(
|
|
2438
|
+
"Draft restored.",
|
|
2439
|
+
);
|
|
2440
|
+
});
|
|
2441
|
+
|
|
2442
|
+
const NOTE_TITLE_KEY = "jant:compose-note-title";
|
|
2443
|
+
|
|
2444
|
+
function titleToggle(el: JantComposeDialog, index = 0): HTMLButtonElement {
|
|
2445
|
+
return requireElement(
|
|
2446
|
+
el.querySelectorAll<HTMLButtonElement>(
|
|
2447
|
+
'.compose-tool-btn[title="Title"]',
|
|
2448
|
+
)[index] ?? null,
|
|
2449
|
+
"expected title toggle",
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
it("remembers a note's title toggle for the next new post", async () => {
|
|
2454
|
+
const el = await createElement();
|
|
2455
|
+
await el.openNew({ restoreDraft: false });
|
|
2456
|
+
await flushUpdates(el);
|
|
2457
|
+
|
|
2458
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
2459
|
+
|
|
2460
|
+
titleToggle(el).click();
|
|
2461
|
+
await flushUpdates(el);
|
|
2462
|
+
|
|
2463
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBe("0");
|
|
2464
|
+
expect(el.querySelector(".compose-note-title")).toBeNull();
|
|
2465
|
+
|
|
2466
|
+
await el.openNew({ restoreDraft: false });
|
|
2467
|
+
await flushUpdates(el);
|
|
2468
|
+
|
|
2469
|
+
expect(el.querySelector(".compose-note-title")).toBeNull();
|
|
2470
|
+
|
|
2471
|
+
titleToggle(el).click();
|
|
2472
|
+
await flushUpdates(el);
|
|
2473
|
+
|
|
2474
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBe("1");
|
|
2475
|
+
|
|
2476
|
+
await el.openNew({ restoreDraft: false });
|
|
2477
|
+
await flushUpdates(el);
|
|
2478
|
+
|
|
2479
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
2480
|
+
});
|
|
2481
|
+
|
|
2482
|
+
it("opens a fresh composer without the title field when it was last turned off", async () => {
|
|
2483
|
+
globalThis.localStorage.setItem(NOTE_TITLE_KEY, "0");
|
|
2484
|
+
|
|
2485
|
+
const el = await createElement();
|
|
2486
|
+
|
|
2487
|
+
expect(el.querySelector(".compose-note-title")).toBeNull();
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
it("does not remember the title toggle from an edit", async () => {
|
|
2491
|
+
mockEditPost({ format: "note", title: "Hello", body: null });
|
|
2492
|
+
|
|
2493
|
+
const el = await createElement();
|
|
2494
|
+
await el.openEdit("pst_123");
|
|
2495
|
+
await flushUpdates(el);
|
|
2496
|
+
|
|
2497
|
+
titleToggle(el).click();
|
|
2498
|
+
await flushUpdates(el);
|
|
2499
|
+
|
|
2500
|
+
// Dropping this post's title says nothing about the next post's.
|
|
2501
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBeNull();
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
it("does not remember the title toggle from a reply", async () => {
|
|
2505
|
+
const el = await createElement();
|
|
2506
|
+
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
2507
|
+
contentHtml: "<p>Parent</p>",
|
|
2508
|
+
dateText: "Mar 14",
|
|
2509
|
+
});
|
|
2510
|
+
await flushUpdates(el);
|
|
2511
|
+
|
|
2512
|
+
titleToggle(el).click();
|
|
2513
|
+
await flushUpdates(el);
|
|
2514
|
+
|
|
2515
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
2516
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBeNull();
|
|
2517
|
+
});
|
|
2518
|
+
|
|
2519
|
+
it("does not remember the title toggle from a thread post", async () => {
|
|
2520
|
+
const el = await createElement();
|
|
2521
|
+
(el as unknown as { _addThreadItem: () => void })._addThreadItem();
|
|
2522
|
+
await flushUpdates(el);
|
|
2523
|
+
|
|
2524
|
+
titleToggle(el).click();
|
|
2525
|
+
await flushUpdates(el);
|
|
2526
|
+
|
|
2527
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBeNull();
|
|
2528
|
+
});
|
|
2529
|
+
|
|
2530
|
+
it("does not remember a title the editor reveals on its own", async () => {
|
|
2531
|
+
const el = await createElement();
|
|
2532
|
+
await el.openNew({ restoreDraft: false });
|
|
2533
|
+
await flushUpdates(el);
|
|
2534
|
+
|
|
2535
|
+
const editor = requireElement(
|
|
2536
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
2537
|
+
"expected compose editor",
|
|
2538
|
+
);
|
|
2539
|
+
// The fullscreen and H1-promotion paths land here. Neither is the author
|
|
2540
|
+
// stating a habit, so neither writes one down.
|
|
2541
|
+
editor.setEditorState(null, "Promoted", true);
|
|
2542
|
+
await flushUpdates(el);
|
|
2543
|
+
|
|
2544
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
2545
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBeNull();
|
|
2546
|
+
});
|
|
2547
|
+
|
|
2548
|
+
it("lets a restored draft's title state win over the remembered default", async () => {
|
|
2549
|
+
globalThis.localStorage.setItem(NOTE_TITLE_KEY, "0");
|
|
2550
|
+
globalThis.localStorage.setItem(
|
|
2551
|
+
"jant:compose-draft",
|
|
2552
|
+
JSON.stringify({
|
|
2553
|
+
format: "note",
|
|
2554
|
+
title: "Draft title",
|
|
2555
|
+
showTitle: true,
|
|
2556
|
+
bodyJson: {
|
|
2557
|
+
type: "doc",
|
|
2558
|
+
content: [
|
|
2559
|
+
{
|
|
2560
|
+
type: "paragraph",
|
|
2561
|
+
content: [{ type: "text", text: "Draft body" }],
|
|
2562
|
+
},
|
|
2563
|
+
],
|
|
2564
|
+
},
|
|
2565
|
+
url: "",
|
|
2566
|
+
quoteText: "",
|
|
2567
|
+
quoteAuthor: "",
|
|
2568
|
+
slug: "",
|
|
2569
|
+
visibility: "public",
|
|
2570
|
+
rating: 0,
|
|
2571
|
+
showRating: false,
|
|
2572
|
+
collectionIds: [],
|
|
2573
|
+
attachedTexts: [],
|
|
2574
|
+
attachmentOrder: [],
|
|
2575
|
+
savedAt: Date.now(),
|
|
2576
|
+
}),
|
|
2577
|
+
);
|
|
2578
|
+
|
|
2579
|
+
const el = await createElement();
|
|
2580
|
+
await el.openNew();
|
|
2581
|
+
await flushUpdates(el);
|
|
2582
|
+
|
|
2583
|
+
expect(
|
|
2584
|
+
el.querySelector<HTMLInputElement>(".compose-note-title")?.value,
|
|
2585
|
+
).toBe("Draft title");
|
|
2586
|
+
});
|
|
2587
|
+
|
|
1513
2588
|
it("includes a custom slug from the publish settings panel in the submit payload", async () => {
|
|
1514
2589
|
mockSlugApi((url) => {
|
|
1515
2590
|
if (url.searchParams.get("mode") === "suggest") {
|
|
@@ -1538,13 +2613,14 @@ describe("JantComposeDialog", () => {
|
|
|
1538
2613
|
await editor.updateComplete;
|
|
1539
2614
|
|
|
1540
2615
|
const publishToggle = requireElement(
|
|
1541
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2616
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1542
2617
|
"expected publish settings toggle",
|
|
1543
2618
|
);
|
|
1544
2619
|
publishToggle.click();
|
|
1545
2620
|
await el.updateComplete;
|
|
1546
2621
|
await flushUpdates(el);
|
|
1547
2622
|
|
|
2623
|
+
await openPostMeta(el);
|
|
1548
2624
|
const slugInput = requireElement(
|
|
1549
2625
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1550
2626
|
"expected custom link input",
|
|
@@ -1584,7 +2660,7 @@ describe("JantComposeDialog", () => {
|
|
|
1584
2660
|
const el = await createElement();
|
|
1585
2661
|
|
|
1586
2662
|
const publishToggle = requireElement(
|
|
1587
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2663
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1588
2664
|
"expected publish settings toggle",
|
|
1589
2665
|
);
|
|
1590
2666
|
|
|
@@ -1592,6 +2668,7 @@ describe("JantComposeDialog", () => {
|
|
|
1592
2668
|
await el.updateComplete;
|
|
1593
2669
|
await flushUpdates(el);
|
|
1594
2670
|
|
|
2671
|
+
await openPostMeta(el);
|
|
1595
2672
|
const slugInput = requireElement(
|
|
1596
2673
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1597
2674
|
"expected custom link input",
|
|
@@ -1606,6 +2683,7 @@ describe("JantComposeDialog", () => {
|
|
|
1606
2683
|
publishToggle.click();
|
|
1607
2684
|
await el.updateComplete;
|
|
1608
2685
|
|
|
2686
|
+
await openPostMeta(el);
|
|
1609
2687
|
expect(el.querySelector(".compose-publish-slug-input")).not.toBeNull();
|
|
1610
2688
|
});
|
|
1611
2689
|
|
|
@@ -1664,7 +2742,7 @@ describe("JantComposeDialog", () => {
|
|
|
1664
2742
|
} as never);
|
|
1665
2743
|
|
|
1666
2744
|
requireElement(
|
|
1667
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2745
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1668
2746
|
"expected publish settings toggle",
|
|
1669
2747
|
).click();
|
|
1670
2748
|
await el.updateComplete;
|
|
@@ -1735,7 +2813,7 @@ describe("JantComposeDialog", () => {
|
|
|
1735
2813
|
} as never);
|
|
1736
2814
|
|
|
1737
2815
|
requireElement(
|
|
1738
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2816
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1739
2817
|
"expected publish settings toggle",
|
|
1740
2818
|
).click();
|
|
1741
2819
|
await el.updateComplete;
|
|
@@ -1757,7 +2835,7 @@ describe("JantComposeDialog", () => {
|
|
|
1757
2835
|
const el = await createElement();
|
|
1758
2836
|
|
|
1759
2837
|
requireElement(
|
|
1760
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2838
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1761
2839
|
"expected publish settings toggle",
|
|
1762
2840
|
).click();
|
|
1763
2841
|
await el.updateComplete;
|
|
@@ -1785,13 +2863,14 @@ describe("JantComposeDialog", () => {
|
|
|
1785
2863
|
await editor.updateComplete;
|
|
1786
2864
|
|
|
1787
2865
|
const publishToggle = requireElement(
|
|
1788
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2866
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1789
2867
|
"expected publish settings toggle",
|
|
1790
2868
|
);
|
|
1791
2869
|
publishToggle.click();
|
|
1792
2870
|
await el.updateComplete;
|
|
1793
2871
|
await flushUpdates(el);
|
|
1794
2872
|
|
|
2873
|
+
await openPostMeta(el);
|
|
1795
2874
|
const slugInput = requireElement(
|
|
1796
2875
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1797
2876
|
"expected custom link input",
|
|
@@ -1836,7 +2915,6 @@ describe("JantComposeDialog", () => {
|
|
|
1836
2915
|
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
1837
2916
|
"expected compose editor",
|
|
1838
2917
|
);
|
|
1839
|
-
editor._showTitle = true;
|
|
1840
2918
|
editor._title = "Hello World";
|
|
1841
2919
|
editor._bodyJson = {
|
|
1842
2920
|
type: "doc",
|
|
@@ -1847,13 +2925,14 @@ describe("JantComposeDialog", () => {
|
|
|
1847
2925
|
await editor.updateComplete;
|
|
1848
2926
|
|
|
1849
2927
|
const publishToggle = requireElement(
|
|
1850
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2928
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1851
2929
|
"expected publish settings toggle",
|
|
1852
2930
|
);
|
|
1853
2931
|
publishToggle.click();
|
|
1854
2932
|
await el.updateComplete;
|
|
1855
2933
|
await flushUpdates(el);
|
|
1856
2934
|
|
|
2935
|
+
await openPostMeta(el);
|
|
1857
2936
|
expect(
|
|
1858
2937
|
el.querySelector(".compose-slug-suggestion-value")?.textContent?.trim(),
|
|
1859
2938
|
).toBe("/hello-world");
|
|
@@ -1874,11 +2953,11 @@ describe("JantComposeDialog", () => {
|
|
|
1874
2953
|
).toBeUndefined();
|
|
1875
2954
|
});
|
|
1876
2955
|
|
|
1877
|
-
it("
|
|
2956
|
+
it("keeps the publish panel to whole-submission settings", async () => {
|
|
1878
2957
|
const el = await createElement();
|
|
1879
2958
|
|
|
1880
2959
|
const publishToggle = requireElement(
|
|
1881
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2960
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1882
2961
|
"expected publish settings toggle",
|
|
1883
2962
|
);
|
|
1884
2963
|
publishToggle.click();
|
|
@@ -1890,12 +2969,16 @@ describe("JantComposeDialog", () => {
|
|
|
1890
2969
|
);
|
|
1891
2970
|
expect(panel.textContent).not.toContain("Publish settings");
|
|
1892
2971
|
expect(panel.textContent).toContain("Visibility");
|
|
1893
|
-
expect(panel.textContent).toContain("Published on");
|
|
1894
|
-
expect(panel.textContent).toContain("Custom link");
|
|
1895
2972
|
expect(panel.textContent).not.toContain("Save as draft");
|
|
1896
2973
|
expect(panel.textContent).not.toContain("Discard");
|
|
1897
|
-
|
|
1898
|
-
expect(panel.
|
|
2974
|
+
// Post-scoped settings are not in here — they live on the post.
|
|
2975
|
+
expect(panel.textContent).not.toContain("Published on");
|
|
2976
|
+
expect(panel.textContent).not.toContain("Custom link");
|
|
2977
|
+
|
|
2978
|
+
await openPostMeta(el);
|
|
2979
|
+
expect(el.querySelector(".compose-publish-date-input")).not.toBeNull();
|
|
2980
|
+
await openPostMeta(el);
|
|
2981
|
+
expect(el.querySelector(".compose-publish-slug-input")).not.toBeNull();
|
|
1899
2982
|
});
|
|
1900
2983
|
|
|
1901
2984
|
it("clears the custom slug with the reset action", async () => {
|
|
@@ -1911,13 +2994,14 @@ describe("JantComposeDialog", () => {
|
|
|
1911
2994
|
|
|
1912
2995
|
const el = await createElement();
|
|
1913
2996
|
const publishToggle = requireElement(
|
|
1914
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2997
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1915
2998
|
"expected publish settings toggle",
|
|
1916
2999
|
);
|
|
1917
3000
|
publishToggle.click();
|
|
1918
3001
|
await el.updateComplete;
|
|
1919
3002
|
await flushUpdates(el);
|
|
1920
3003
|
|
|
3004
|
+
await openPostMeta(el);
|
|
1921
3005
|
const slugInput = requireElement(
|
|
1922
3006
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1923
3007
|
"expected custom link input",
|
|
@@ -1937,6 +3021,7 @@ describe("JantComposeDialog", () => {
|
|
|
1937
3021
|
).click();
|
|
1938
3022
|
await flushUpdates(el);
|
|
1939
3023
|
|
|
3024
|
+
await openPostMeta(el);
|
|
1940
3025
|
expect(
|
|
1941
3026
|
requireElement(
|
|
1942
3027
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
@@ -1959,7 +3044,7 @@ describe("JantComposeDialog", () => {
|
|
|
1959
3044
|
await el.updateComplete;
|
|
1960
3045
|
|
|
1961
3046
|
const publishToggle = requireElement(
|
|
1962
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
3047
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1963
3048
|
"expected publish settings toggle",
|
|
1964
3049
|
);
|
|
1965
3050
|
publishToggle.click();
|
|
@@ -1991,18 +3076,18 @@ describe("JantComposeDialog", () => {
|
|
|
1991
3076
|
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
1992
3077
|
"expected compose editor",
|
|
1993
3078
|
);
|
|
1994
|
-
editor._showTitle = true;
|
|
1995
3079
|
editor._title = "Hello World";
|
|
1996
3080
|
await editor.updateComplete;
|
|
1997
3081
|
|
|
1998
3082
|
const publishToggle = requireElement(
|
|
1999
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
3083
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
2000
3084
|
"expected publish settings toggle",
|
|
2001
3085
|
);
|
|
2002
3086
|
publishToggle.click();
|
|
2003
3087
|
await el.updateComplete;
|
|
2004
3088
|
await flushUpdates(el);
|
|
2005
3089
|
|
|
3090
|
+
await openPostMeta(el);
|
|
2006
3091
|
expect(
|
|
2007
3092
|
el.querySelector(".compose-slug-suggestion-value")?.textContent?.trim(),
|
|
2008
3093
|
).toBe("/hello-world");
|
|
@@ -2052,13 +3137,14 @@ describe("JantComposeDialog", () => {
|
|
|
2052
3137
|
await editor.updateComplete;
|
|
2053
3138
|
|
|
2054
3139
|
const publishToggle = requireElement(
|
|
2055
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
3140
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
2056
3141
|
"expected publish settings toggle",
|
|
2057
3142
|
);
|
|
2058
3143
|
publishToggle.click();
|
|
2059
3144
|
await el.updateComplete;
|
|
2060
3145
|
await flushUpdates(el);
|
|
2061
3146
|
|
|
3147
|
+
await openPostMeta(el);
|
|
2062
3148
|
const slugInput = requireElement(
|
|
2063
3149
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
2064
3150
|
"expected custom link input",
|
|
@@ -2109,11 +3195,12 @@ describe("JantComposeDialog", () => {
|
|
|
2109
3195
|
await editor.updateComplete;
|
|
2110
3196
|
|
|
2111
3197
|
requireElement(
|
|
2112
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
3198
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
2113
3199
|
"expected publish settings toggle",
|
|
2114
3200
|
).click();
|
|
2115
3201
|
await el.updateComplete;
|
|
2116
3202
|
|
|
3203
|
+
await openPostMeta(el);
|
|
2117
3204
|
const slugInput = requireElement(
|
|
2118
3205
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
2119
3206
|
"expected custom link input",
|
|
@@ -2161,11 +3248,12 @@ describe("JantComposeDialog", () => {
|
|
|
2161
3248
|
await editor.updateComplete;
|
|
2162
3249
|
|
|
2163
3250
|
requireElement(
|
|
2164
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
3251
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
2165
3252
|
"expected publish settings toggle",
|
|
2166
3253
|
).click();
|
|
2167
3254
|
await el.updateComplete;
|
|
2168
3255
|
|
|
3256
|
+
await openPostMeta(el);
|
|
2169
3257
|
const slugInput = requireElement(
|
|
2170
3258
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
2171
3259
|
"expected custom link input",
|
|
@@ -2279,9 +3367,8 @@ describe("JantComposeDialog", () => {
|
|
|
2279
3367
|
slug: "",
|
|
2280
3368
|
visibility: "public",
|
|
2281
3369
|
rating: 0,
|
|
2282
|
-
showTitle: false,
|
|
2283
3370
|
showRating: false,
|
|
2284
|
-
collectionIds: [],
|
|
3371
|
+
collectionIds: ["col-stale"],
|
|
2285
3372
|
replyToId,
|
|
2286
3373
|
attachedTexts: [],
|
|
2287
3374
|
attachmentOrder: [],
|
|
@@ -2300,6 +3387,8 @@ describe("JantComposeDialog", () => {
|
|
|
2300
3387
|
"expected compose editor",
|
|
2301
3388
|
);
|
|
2302
3389
|
expect(el._replyToId).toBe(replyToId);
|
|
3390
|
+
expect(el._collectionIds).toEqual([]);
|
|
3391
|
+
expect(el.querySelector(".compose-collection-trigger")).toBeNull();
|
|
2303
3392
|
expect(editor.getData().body).toContain("Recovered reply draft");
|
|
2304
3393
|
});
|
|
2305
3394
|
|
|
@@ -2333,19 +3422,69 @@ describe("JantComposeDialog", () => {
|
|
|
2333
3422
|
"expected reply button",
|
|
2334
3423
|
).click();
|
|
2335
3424
|
|
|
2336
|
-
const savedDraftRaw = globalThis.localStorage.getItem("jant:compose-draft");
|
|
2337
|
-
if (!savedDraftRaw) {
|
|
2338
|
-
throw new Error("expected local compose draft");
|
|
2339
|
-
}
|
|
2340
|
-
const savedDraft = JSON.parse(savedDraftRaw) as {
|
|
2341
|
-
replyToId: string;
|
|
2342
|
-
bodyJson: { content: Array<{ content: Array<{ text: string }> }> };
|
|
2343
|
-
};
|
|
2344
|
-
|
|
2345
|
-
expect(savedDraft.replyToId).toBe(replyToId);
|
|
2346
|
-
expect(savedDraft.bodyJson.content[0]?.content[0]?.text).toBe(
|
|
2347
|
-
"Persist me before submit",
|
|
2348
|
-
);
|
|
3425
|
+
const savedDraftRaw = globalThis.localStorage.getItem("jant:compose-draft");
|
|
3426
|
+
if (!savedDraftRaw) {
|
|
3427
|
+
throw new Error("expected local compose draft");
|
|
3428
|
+
}
|
|
3429
|
+
const savedDraft = JSON.parse(savedDraftRaw) as {
|
|
3430
|
+
replyToId: string;
|
|
3431
|
+
bodyJson: { content: Array<{ content: Array<{ text: string }> }> };
|
|
3432
|
+
};
|
|
3433
|
+
|
|
3434
|
+
expect(savedDraft.replyToId).toBe(replyToId);
|
|
3435
|
+
expect(savedDraft.bodyJson.content[0]?.content[0]?.text).toBe(
|
|
3436
|
+
"Persist me before submit",
|
|
3437
|
+
);
|
|
3438
|
+
});
|
|
3439
|
+
|
|
3440
|
+
it("includes quiet reply intent when submitting multiple reply posts", async () => {
|
|
3441
|
+
const el = await createElement();
|
|
3442
|
+
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
3443
|
+
contentHtml: "<p>Parent</p>",
|
|
3444
|
+
dateText: "Mar 14",
|
|
3445
|
+
});
|
|
3446
|
+
|
|
3447
|
+
(
|
|
3448
|
+
el as unknown as {
|
|
3449
|
+
_addThreadItem: () => void;
|
|
3450
|
+
}
|
|
3451
|
+
)._addThreadItem();
|
|
3452
|
+
await flushUpdates(el);
|
|
3453
|
+
|
|
3454
|
+
el._quietReply = true;
|
|
3455
|
+
const editors = Array.from(
|
|
3456
|
+
el.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
|
|
3457
|
+
);
|
|
3458
|
+
expect(editors).toHaveLength(2);
|
|
3459
|
+
|
|
3460
|
+
editors.forEach((editor, index) => {
|
|
3461
|
+
editor._bodyJson = {
|
|
3462
|
+
type: "doc",
|
|
3463
|
+
content: [
|
|
3464
|
+
{
|
|
3465
|
+
type: "paragraph",
|
|
3466
|
+
content: [{ type: "text", text: `Quiet reply ${index + 1}` }],
|
|
3467
|
+
},
|
|
3468
|
+
],
|
|
3469
|
+
};
|
|
3470
|
+
});
|
|
3471
|
+
await Promise.all(editors.map((editor) => editor.updateComplete));
|
|
3472
|
+
await el.updateComplete;
|
|
3473
|
+
|
|
3474
|
+
let receivedDetail: ComposeSubmitDetail | null = null;
|
|
3475
|
+
el.addEventListener("jant:compose-submit-deferred", (event) => {
|
|
3476
|
+
receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
|
|
3477
|
+
});
|
|
3478
|
+
|
|
3479
|
+
requireElement(
|
|
3480
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
3481
|
+
"expected reply button",
|
|
3482
|
+
).click();
|
|
3483
|
+
|
|
3484
|
+
expect(receivedDetail).not.toBeNull();
|
|
3485
|
+
const detail = receivedDetail as unknown as ComposeSubmitDetail;
|
|
3486
|
+
expect(detail.quietReply).toBe(true);
|
|
3487
|
+
expect(detail.threadPosts?.[0]?.quietReply).toBe(true);
|
|
2349
3488
|
});
|
|
2350
3489
|
|
|
2351
3490
|
it("does not add more thread items than the shared limit", async () => {
|
|
@@ -2364,6 +3503,34 @@ describe("JantComposeDialog", () => {
|
|
|
2364
3503
|
expect(el._threadItems).toHaveLength(MAX_THREAD_POSTS);
|
|
2365
3504
|
});
|
|
2366
3505
|
|
|
3506
|
+
it("scrolls the thread composer to the bottom after adding an item", async () => {
|
|
3507
|
+
const el = await createElement();
|
|
3508
|
+
el._threadItems = [
|
|
3509
|
+
{ id: "thread-1", format: "note" },
|
|
3510
|
+
{ id: "thread-2", format: "note" },
|
|
3511
|
+
];
|
|
3512
|
+
await el.updateComplete;
|
|
3513
|
+
|
|
3514
|
+
const scroller = requireElement(
|
|
3515
|
+
el.querySelector<HTMLElement>(".compose-scroll"),
|
|
3516
|
+
"expected compose scroll region",
|
|
3517
|
+
);
|
|
3518
|
+
Object.defineProperty(scroller, "scrollHeight", {
|
|
3519
|
+
configurable: true,
|
|
3520
|
+
value: 640,
|
|
3521
|
+
});
|
|
3522
|
+
scroller.scrollTop = 120;
|
|
3523
|
+
|
|
3524
|
+
(
|
|
3525
|
+
el as unknown as {
|
|
3526
|
+
_addThreadItem: () => void;
|
|
3527
|
+
}
|
|
3528
|
+
)._addThreadItem();
|
|
3529
|
+
await flushUpdates(el);
|
|
3530
|
+
|
|
3531
|
+
expect(scroller.scrollTop).toBe(640);
|
|
3532
|
+
});
|
|
3533
|
+
|
|
2367
3534
|
it("omits visibility from locked edit submissions", async () => {
|
|
2368
3535
|
const el = await createElement();
|
|
2369
3536
|
const editor = requireElement(
|
|
@@ -2405,6 +3572,20 @@ describe("JantComposeDialog", () => {
|
|
|
2405
3572
|
);
|
|
2406
3573
|
});
|
|
2407
3574
|
|
|
3575
|
+
it("gives the collection trigger one glyph and no chevron", async () => {
|
|
3576
|
+
const el = await createElement();
|
|
3577
|
+
const trigger = requireElement(
|
|
3578
|
+
el.querySelector<HTMLButtonElement>(".compose-collection-trigger"),
|
|
3579
|
+
"expected collection trigger",
|
|
3580
|
+
);
|
|
3581
|
+
|
|
3582
|
+
expect(trigger.querySelectorAll("svg").length).toBe(1);
|
|
3583
|
+
expect(
|
|
3584
|
+
trigger.querySelector(".compose-collection-trigger-svg"),
|
|
3585
|
+
).not.toBeNull();
|
|
3586
|
+
expect(trigger.querySelector(".compose-collection-chevron")).toBeNull();
|
|
3587
|
+
});
|
|
3588
|
+
|
|
2408
3589
|
it("collection selector toggles IDs", async () => {
|
|
2409
3590
|
const el = await createElement();
|
|
2410
3591
|
|
|
@@ -2935,13 +4116,10 @@ describe("JantComposeDialog", () => {
|
|
|
2935
4116
|
};
|
|
2936
4117
|
await editor.updateComplete;
|
|
2937
4118
|
|
|
2938
|
-
// Click the
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
);
|
|
2943
|
-
draftBtn.click();
|
|
2944
|
-
await el.updateComplete;
|
|
4119
|
+
// Click the drafts row in the options panel — should show confirm panel
|
|
4120
|
+
await openPublishPanel(el);
|
|
4121
|
+
requireElement(draftsRow(el), "expected drafts row").click();
|
|
4122
|
+
await flushUpdates(el);
|
|
2945
4123
|
|
|
2946
4124
|
expect(el._confirmPanelOpen).toBe(true);
|
|
2947
4125
|
expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
|
|
@@ -2958,13 +4136,10 @@ describe("JantComposeDialog", () => {
|
|
|
2958
4136
|
}),
|
|
2959
4137
|
);
|
|
2960
4138
|
|
|
2961
|
-
// Click the
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
);
|
|
2966
|
-
draftBtn.click();
|
|
2967
|
-
await el.updateComplete;
|
|
4139
|
+
// Click the drafts row in the options panel — should open drafts panel
|
|
4140
|
+
await openPublishPanel(el);
|
|
4141
|
+
requireElement(draftsRow(el), "expected drafts row").click();
|
|
4142
|
+
await flushUpdates(el);
|
|
2968
4143
|
|
|
2969
4144
|
expect(el._draftsPanelOpen).toBe(true);
|
|
2970
4145
|
|
|
@@ -2978,6 +4153,132 @@ describe("JantComposeDialog", () => {
|
|
|
2978
4153
|
fetchSpy.mockRestore();
|
|
2979
4154
|
});
|
|
2980
4155
|
|
|
4156
|
+
it("opens a prefixed draft preview from the overflow menu without loading the editor", async () => {
|
|
4157
|
+
document.documentElement.dataset.sitePathPrefix = "/blog";
|
|
4158
|
+
const el = await createElement();
|
|
4159
|
+
el._draftsPanelOpen = true;
|
|
4160
|
+
el._draftsLoading = false;
|
|
4161
|
+
el._drafts = [
|
|
4162
|
+
{
|
|
4163
|
+
id: "pst_draft",
|
|
4164
|
+
slug: "draft-slug",
|
|
4165
|
+
format: "note",
|
|
4166
|
+
title: "Preview me",
|
|
4167
|
+
bodyText: "Draft body",
|
|
4168
|
+
bodyHtml: "<p>Draft body</p>",
|
|
4169
|
+
url: null,
|
|
4170
|
+
quoteText: null,
|
|
4171
|
+
replyToId: null,
|
|
4172
|
+
updatedAt: 0,
|
|
4173
|
+
mediaAttachments: [],
|
|
4174
|
+
},
|
|
4175
|
+
];
|
|
4176
|
+
await el.updateComplete;
|
|
4177
|
+
|
|
4178
|
+
const trigger = requireElement(
|
|
4179
|
+
el.querySelector<HTMLButtonElement>(".compose-draft-more"),
|
|
4180
|
+
"expected draft actions trigger",
|
|
4181
|
+
);
|
|
4182
|
+
expect(trigger.getAttribute("aria-label")).toBe("Draft actions");
|
|
4183
|
+
expect(trigger.getAttribute("aria-haspopup")).toBe("menu");
|
|
4184
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
4185
|
+
|
|
4186
|
+
trigger.click();
|
|
4187
|
+
await el.updateComplete;
|
|
4188
|
+
|
|
4189
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("true");
|
|
4190
|
+
const menu = requireElement(
|
|
4191
|
+
el.querySelector<HTMLElement>('[role="menu"]'),
|
|
4192
|
+
"expected draft actions menu",
|
|
4193
|
+
);
|
|
4194
|
+
const items = menu.querySelectorAll<HTMLElement>('[role="menuitem"]');
|
|
4195
|
+
expect(items).toHaveLength(2);
|
|
4196
|
+
expect(items[0]?.textContent?.trim()).toBe("Preview");
|
|
4197
|
+
expect(items[1]?.textContent?.trim()).toBe("Delete Draft");
|
|
4198
|
+
|
|
4199
|
+
const previewLink = requireElement(
|
|
4200
|
+
menu.querySelector<HTMLAnchorElement>('a[role="menuitem"]'),
|
|
4201
|
+
"expected preview link",
|
|
4202
|
+
);
|
|
4203
|
+
expect(previewLink.getAttribute("href")).toBe("/blog/preview/draft-slug");
|
|
4204
|
+
expect(previewLink.target).toBe("_blank");
|
|
4205
|
+
expect(previewLink.rel).toBe("noopener noreferrer");
|
|
4206
|
+
|
|
4207
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
4208
|
+
previewLink.addEventListener("click", (event) => event.preventDefault());
|
|
4209
|
+
previewLink.click();
|
|
4210
|
+
await el.updateComplete;
|
|
4211
|
+
|
|
4212
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
4213
|
+
expect(el._draftsPanelOpen).toBe(true);
|
|
4214
|
+
expect(el._draftMenuOpenId).toBeNull();
|
|
4215
|
+
});
|
|
4216
|
+
|
|
4217
|
+
it("uses title or quote text for draft previews and identifies each format", async () => {
|
|
4218
|
+
const el = await createElement();
|
|
4219
|
+
el._draftsPanelOpen = true;
|
|
4220
|
+
el._draftsLoading = false;
|
|
4221
|
+
el._drafts = [
|
|
4222
|
+
{
|
|
4223
|
+
id: "pst_note",
|
|
4224
|
+
slug: "note-draft",
|
|
4225
|
+
format: "note",
|
|
4226
|
+
title: "A titled note",
|
|
4227
|
+
bodyText: "This body should not be the preview",
|
|
4228
|
+
bodyHtml: "<p>This body should not be the preview</p>",
|
|
4229
|
+
url: null,
|
|
4230
|
+
quoteText: null,
|
|
4231
|
+
replyToId: null,
|
|
4232
|
+
updatedAt: 0,
|
|
4233
|
+
mediaAttachments: [],
|
|
4234
|
+
},
|
|
4235
|
+
{
|
|
4236
|
+
id: "pst_quote",
|
|
4237
|
+
slug: "quote-draft",
|
|
4238
|
+
format: "quote",
|
|
4239
|
+
title: "Quote title",
|
|
4240
|
+
bodyText: "Supporting note",
|
|
4241
|
+
bodyHtml: "<p>Supporting note</p>",
|
|
4242
|
+
url: null,
|
|
4243
|
+
quoteText: "The quoted words come first.",
|
|
4244
|
+
replyToId: null,
|
|
4245
|
+
updatedAt: 0,
|
|
4246
|
+
mediaAttachments: [],
|
|
4247
|
+
},
|
|
4248
|
+
{
|
|
4249
|
+
id: "pst_link",
|
|
4250
|
+
slug: "link-draft",
|
|
4251
|
+
format: "link",
|
|
4252
|
+
title: null,
|
|
4253
|
+
bodyText: "A link note",
|
|
4254
|
+
bodyHtml: "<p>A link note</p>",
|
|
4255
|
+
url: "https://example.com",
|
|
4256
|
+
quoteText: null,
|
|
4257
|
+
replyToId: null,
|
|
4258
|
+
updatedAt: 0,
|
|
4259
|
+
mediaAttachments: [],
|
|
4260
|
+
},
|
|
4261
|
+
];
|
|
4262
|
+
await el.updateComplete;
|
|
4263
|
+
|
|
4264
|
+
const rows = el.querySelectorAll<HTMLElement>(".compose-draft-item");
|
|
4265
|
+
expect(rows).toHaveLength(3);
|
|
4266
|
+
expect(rows[0]?.querySelector(".compose-draft-preview")?.textContent).toBe(
|
|
4267
|
+
"A titled note",
|
|
4268
|
+
);
|
|
4269
|
+
expect(rows[1]?.querySelector(".compose-draft-preview")?.textContent).toBe(
|
|
4270
|
+
"The quoted words come first.",
|
|
4271
|
+
);
|
|
4272
|
+
expect(rows[2]?.querySelector(".compose-draft-preview")?.textContent).toBe(
|
|
4273
|
+
"A link note",
|
|
4274
|
+
);
|
|
4275
|
+
expect(
|
|
4276
|
+
Array.from(el.querySelectorAll(".compose-draft-format")).map(
|
|
4277
|
+
(format) => format.textContent,
|
|
4278
|
+
),
|
|
4279
|
+
).toEqual(["Note", "Quote", "Link"]);
|
|
4280
|
+
});
|
|
4281
|
+
|
|
2981
4282
|
it("does not dispatch submit when loading", async () => {
|
|
2982
4283
|
const el = await createElement();
|
|
2983
4284
|
el._loading = true;
|
|
@@ -3036,6 +4337,7 @@ describe("JantComposeDialog", () => {
|
|
|
3036
4337
|
alt: "",
|
|
3037
4338
|
error: null,
|
|
3038
4339
|
posterUrl: null,
|
|
4340
|
+
remoteUrl: null,
|
|
3039
4341
|
summary: null,
|
|
3040
4342
|
chars: null,
|
|
3041
4343
|
},
|
|
@@ -3054,52 +4356,451 @@ describe("JantComposeDialog", () => {
|
|
|
3054
4356
|
expect(mediaBtn?.querySelector(".compose-tool-label")?.textContent).toBe(
|
|
3055
4357
|
"Add",
|
|
3056
4358
|
);
|
|
3057
|
-
|
|
3058
|
-
URL.revokeObjectURL(previewUrl);
|
|
4359
|
+
|
|
4360
|
+
URL.revokeObjectURL(previewUrl);
|
|
4361
|
+
});
|
|
4362
|
+
|
|
4363
|
+
it("puts the single-post editor straight inside the one scroll region", async () => {
|
|
4364
|
+
const el = await createElement();
|
|
4365
|
+
|
|
4366
|
+
const scroller = requireElement(
|
|
4367
|
+
el.querySelector<HTMLElement>(".compose-scroll"),
|
|
4368
|
+
"expected compose scroll region",
|
|
4369
|
+
);
|
|
4370
|
+
const editor = requireElement(
|
|
4371
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
4372
|
+
"expected compose editor",
|
|
4373
|
+
);
|
|
4374
|
+
|
|
4375
|
+
// The rules that lay the editor out are keyed on
|
|
4376
|
+
// `.compose-scroll > jant-compose-editor`. Any wrapper breaks them —
|
|
4377
|
+
// including a `display: contents` one, which drops the box but not the DOM
|
|
4378
|
+
// parentage the selector matches on.
|
|
4379
|
+
expect(editor.parentElement).toBe(scroller);
|
|
4380
|
+
// The action row is the composer's pinned floor; it must stay outside.
|
|
4381
|
+
expect(scroller.querySelector(".compose-action-row")).toBeNull();
|
|
4382
|
+
expect(el.querySelector(".compose-action-row")).not.toBeNull();
|
|
4383
|
+
});
|
|
4384
|
+
|
|
4385
|
+
it("scrolls in exactly one place across every compose mode", async () => {
|
|
4386
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4387
|
+
|
|
4388
|
+
// `.compose-scroll` owns the scrolling for single-post, reply and thread
|
|
4389
|
+
// alike. Anything nested inside it that scrolls on its own is the bug this
|
|
4390
|
+
// replaced: three stacked scrollers, and previews shrunk to fit them.
|
|
4391
|
+
expect(css).toMatch(
|
|
4392
|
+
/\.compose-scroll\s*\{[\s\S]*flex:\s*1;[\s\S]*min-height:\s*0;[\s\S]*overflow-y:\s*auto;/,
|
|
4393
|
+
);
|
|
4394
|
+
expect(css).not.toMatch(/\.compose-body\s*\{[^}]*overflow-y:\s*auto;/);
|
|
4395
|
+
expect(css).not.toMatch(
|
|
4396
|
+
/\.compose-thread-compose-layout\s*\{[^}]*overflow-y:\s*auto;/,
|
|
4397
|
+
);
|
|
4398
|
+
expect(css).not.toMatch(
|
|
4399
|
+
/\.compose-editor-row\s+\.compose-attachments-dock\s*\{[^}]*overflow-y:\s*auto;/,
|
|
4400
|
+
);
|
|
4401
|
+
|
|
4402
|
+
// Nothing between the scroll region and the text may clip or claim a fixed
|
|
4403
|
+
// share of the height — every block is sized by its own content.
|
|
4404
|
+
expect(css).not.toMatch(
|
|
4405
|
+
/\.compose-editor-row\s*\{[^}]*overflow:\s*hidden;/,
|
|
4406
|
+
);
|
|
4407
|
+
expect(css).not.toMatch(
|
|
4408
|
+
/\.compose-thread-layout\s*\{[^}]*overflow:\s*hidden;/,
|
|
4409
|
+
);
|
|
4410
|
+
});
|
|
4411
|
+
|
|
4412
|
+
it("lets attachment previews take the room the pinned toolbar used to need", () => {
|
|
4413
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4414
|
+
|
|
4415
|
+
expect(css).toMatch(
|
|
4416
|
+
/\.compose-attachment-img\s*\{[\s\S]*max-height:\s*min\(420px,\s*46dvh\);/,
|
|
4417
|
+
);
|
|
4418
|
+
expect(css).toMatch(
|
|
4419
|
+
/\.compose-attachment:only-child\s+\.compose-attachment-img\s*\{[\s\S]*max-height:\s*min\(460px,\s*52dvh\);/,
|
|
4420
|
+
);
|
|
4421
|
+
expect(css).toMatch(
|
|
4422
|
+
/\.compose-attachment:not\(:only-child\)\s+\.compose-attachment-img\s*\{[\s\S]*height:\s*min\(340px,\s*40dvh\);/,
|
|
4423
|
+
);
|
|
4424
|
+
// Reply compose no longer shrinks its previews to 96×72 to spare the
|
|
4425
|
+
// toolbar — one preview size everywhere.
|
|
4426
|
+
expect(css).not.toMatch(/--compose-reply-attachment-width/);
|
|
4427
|
+
});
|
|
4428
|
+
|
|
4429
|
+
it("routes a format change straight off the single-post editor", async () => {
|
|
4430
|
+
const el = await createElement();
|
|
4431
|
+
const editor = requireElement(
|
|
4432
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
4433
|
+
"expected compose editor",
|
|
4434
|
+
);
|
|
4435
|
+
|
|
4436
|
+
editor.dispatchEvent(
|
|
4437
|
+
new CustomEvent("jant:format-change", {
|
|
4438
|
+
detail: { format: "quote" },
|
|
4439
|
+
bubbles: true,
|
|
4440
|
+
}),
|
|
4441
|
+
);
|
|
4442
|
+
await flushUpdates(el);
|
|
4443
|
+
|
|
4444
|
+
expect(el._format).toBe("quote");
|
|
4445
|
+
});
|
|
4446
|
+
|
|
4447
|
+
it("shows the replied-to post at the same scale as the reply", () => {
|
|
4448
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4449
|
+
|
|
4450
|
+
// Same ceiling as the composer's own previews — the parent is not a
|
|
4451
|
+
// thumbnail strip.
|
|
4452
|
+
expect(css).toMatch(
|
|
4453
|
+
/\.compose-reply-context-body img,[\s\S]*?video\s*\{[\s\S]*?max-height:\s*min\(420px,\s*46dvh\);/,
|
|
4454
|
+
);
|
|
4455
|
+
// One rule governs every picture in the context. A second one keyed on
|
|
4456
|
+
// `.compose-reply-context-media-img` would sit at lower specificity than
|
|
4457
|
+
// the `…-body img` rule above it and never apply — the media strip is
|
|
4458
|
+
// inside the body.
|
|
4459
|
+
expect(css).not.toMatch(/\.compose-reply-context-media-img\s*\{/);
|
|
4460
|
+
|
|
4461
|
+
// Expanding hands the post its full height; a capped, separately scrolling
|
|
4462
|
+
// box is what left no room for full-size pictures.
|
|
4463
|
+
expect(css).toMatch(
|
|
4464
|
+
/\.compose-reply-context\.expanded\s*\{\s*max-height:\s*none;\s*overflow:\s*visible;/,
|
|
4465
|
+
);
|
|
4466
|
+
// Fullscreen's collapsed cap must not outrank that.
|
|
4467
|
+
expect(css).toMatch(
|
|
4468
|
+
/\.compose-fullscreen-thread-layout\s+\.compose-reply-context:not\(\.expanded\)\s*\{\s*max-height:\s*140px;/,
|
|
4469
|
+
);
|
|
4470
|
+
});
|
|
4471
|
+
|
|
4472
|
+
it("keeps reply compose tools inside the editor's text column", () => {
|
|
4473
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4474
|
+
|
|
4475
|
+
// `flex: 1` on the editor is horizontal — it claims the width the rail dot
|
|
4476
|
+
// leaves. Height comes from content.
|
|
4477
|
+
expect(css).toMatch(
|
|
4478
|
+
/\.compose-editor-row\s*>\s*jant-compose-editor\s*\{[\s\S]*flex:\s*1;[\s\S]*min-width:\s*0;/,
|
|
4479
|
+
);
|
|
4480
|
+
expect(css).toMatch(
|
|
4481
|
+
/\.compose-reply-compose-layout\s+\.compose-thread-post-header\s*\+\s*\.compose-body\s*\{[\s\S]*padding-top:\s*12px;/,
|
|
4482
|
+
);
|
|
4483
|
+
expect(css).toMatch(
|
|
4484
|
+
/@media \(max-width:\s*760px\),\s*\(hover:\s*none\) and \(pointer:\s*coarse\)\s*\{[\s\S]*\.compose-thread-layout\s*\{[\s\S]*padding-top:\s*0\.75rem;/,
|
|
4485
|
+
);
|
|
4486
|
+
expect(css).toMatch(
|
|
4487
|
+
/\.compose-dialog,[\s\S]*\.compose-page-shell\s*>\s*jant-compose-dialog\s*\{[\s\S]*--compose-quote-input-size:\s*var\(--type-content-subtitle\);[\s\S]*--compose-quote-input-leading:\s*1\.32;/,
|
|
4488
|
+
);
|
|
4489
|
+
// On the page the card's own padding is the text column, so every row
|
|
4490
|
+
// inside it reads a zero gutter and only restates its right.
|
|
4491
|
+
expect(css).toMatch(
|
|
4492
|
+
/\.compose-page-shell\s+\.compose-dialog-inner-page\s*\{[\s\S]*--compose-gutter:\s*0px;/,
|
|
4493
|
+
);
|
|
4494
|
+
expect(css).toMatch(
|
|
4495
|
+
/\.compose-page-shell\s+\.compose-dialog-inner-page\s+\.compose-thread-post-header\s*\{\s*padding-inline-end:\s*0;/,
|
|
4496
|
+
);
|
|
4497
|
+
expect(css).toMatch(
|
|
4498
|
+
/\.compose-quote-text\s*\{[\s\S]*font-size:\s*var\(--compose-quote-input-size\);[\s\S]*line-height:\s*var\(--compose-quote-input-leading\);/,
|
|
4499
|
+
);
|
|
4500
|
+
expect(css).toMatch(
|
|
4501
|
+
/\.compose-reply-compose-layout\s*\{[\s\S]*--compose-title-input-size:\s*calc\(var\(--type-content-body\) \* 1\.2\);[\s\S]*--compose-quote-input-size:\s*calc\(var\(--type-content-body\) \* 1\.06\);[\s\S]*--compose-quote-input-leading:\s*1\.42;[\s\S]*--compose-inline-input-size:\s*var\(--type-base\);/,
|
|
4502
|
+
);
|
|
4503
|
+
expect(css).toMatch(
|
|
4504
|
+
/\.compose-reply-compose-layout\s+\.compose-quote-wrap\s*\{[\s\S]*margin-top:\s*0\.4rem;[\s\S]*padding:\s*24px 18px 18px;[\s\S]*border-radius:\s*0\.95rem;/,
|
|
4505
|
+
);
|
|
4506
|
+
expect(css).toMatch(
|
|
4507
|
+
/\.compose-reply-compose-layout\s+\.compose-link-url-wrap\s*\{[\s\S]*margin-top:\s*0\.4rem;/,
|
|
4508
|
+
);
|
|
4509
|
+
// The reply layout tunes only the lead-in. Height is the shared four-line
|
|
4510
|
+
// floor, which reads this layout's own leading and type size, so a
|
|
4511
|
+
// `min-height` here would just restate what the base rule already knows.
|
|
4512
|
+
expect(css).toMatch(
|
|
4513
|
+
/\.compose-reply-compose-layout\s+\.compose-quote-text\s*\{\s*padding-top:\s*0\.35rem;\s*\}/,
|
|
4514
|
+
);
|
|
4515
|
+
expect(css).not.toMatch(
|
|
4516
|
+
/\.compose-reply-compose-layout\s+\.compose-quote-text\s*\{[^}]*min-height:/,
|
|
4517
|
+
);
|
|
4518
|
+
expect(css).toMatch(
|
|
4519
|
+
/\.compose-reply-compose-layout\s+\.compose-tiptap-body\s+\.tiptap\s*\{[\s\S]*font-size:\s*var\(--type-content-body\);/,
|
|
4520
|
+
);
|
|
4521
|
+
});
|
|
4522
|
+
|
|
4523
|
+
it("opens every compose text surface at a floor stated in lines", () => {
|
|
4524
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4525
|
+
|
|
4526
|
+
// Two lines for the editor, four for the quote — as multiples of the
|
|
4527
|
+
// leading, not pixels, so the floor follows the type instead of going
|
|
4528
|
+
// stale next to it.
|
|
4529
|
+
expect(css).toMatch(
|
|
4530
|
+
/\.compose-tiptap-body \.tiptap\s*\{[\s\S]*min-height:\s*calc\(var\(--type-body-leading\) \* 2em\);/,
|
|
4531
|
+
);
|
|
4532
|
+
expect(css).toMatch(
|
|
4533
|
+
/\.compose-quote-text\s*\{[\s\S]*min-height:\s*calc\(var\(--compose-quote-input-leading\) \* 4em\);/,
|
|
4534
|
+
);
|
|
4535
|
+
|
|
4536
|
+
// One floor for every editor: a note's body and the "thoughts" under a
|
|
4537
|
+
// link or a quote all open the same way.
|
|
4538
|
+
expect(css).not.toMatch(/\.compose-tiptap-thoughts/);
|
|
4539
|
+
});
|
|
4540
|
+
|
|
4541
|
+
it("runs the thread rail from the first dot to the last post's, and no further", () => {
|
|
4542
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4543
|
+
|
|
4544
|
+
// Drawn per row rather than as one strip down the layout, which is what
|
|
4545
|
+
// lets both ends be trimmed back to a dot.
|
|
4546
|
+
expect(css).not.toMatch(/\.compose-thread-layout::before\s*\{/);
|
|
4547
|
+
expect(css).toMatch(
|
|
4548
|
+
/\.compose-thread-layout > \*::before\s*\{[\s\S]*top:\s*0;[\s\S]*bottom:\s*0;/,
|
|
4549
|
+
);
|
|
4550
|
+
expect(css).toMatch(
|
|
4551
|
+
/\.compose-thread-layout > :first-child::before\s*\{\s*top:\s*var\(--compose-thread-dot-center\);/,
|
|
4552
|
+
);
|
|
4553
|
+
expect(css).toMatch(
|
|
4554
|
+
/\.compose-editor-row:not\(:has\(~ \.compose-editor-row\)\)::before\s*\{\s*bottom:\s*auto;\s*height:\s*var\(--compose-thread-dot-center\);/,
|
|
4555
|
+
);
|
|
4556
|
+
// The "add" row is a placeholder, not a post — the rail stops before it.
|
|
4557
|
+
expect(css).toMatch(
|
|
4558
|
+
/\.compose-thread-layout > \.compose-thread-add-row::before\s*\{\s*display:\s*none;/,
|
|
4559
|
+
);
|
|
4560
|
+
|
|
4561
|
+
// The dot reads its offset off the same centre the rail is trimmed to, so
|
|
4562
|
+
// resizing the marker cannot leave the line pointing past it.
|
|
4563
|
+
expect(css).toMatch(
|
|
4564
|
+
/\.compose-thread-dot\s*\{[\s\S]*margin-top:\s*calc\(\s*var\(--compose-thread-dot-center\) -\s*var\(--compose-thread-row-padding-top\) -\s*var\(--site-thread-marker-size\) \/ 2\s*\);/,
|
|
4565
|
+
);
|
|
4566
|
+
});
|
|
4567
|
+
|
|
4568
|
+
it("starts every compose row's ink on one text column", () => {
|
|
4569
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4570
|
+
|
|
4571
|
+
// One knob for the column itself, read by every row rather than restated
|
|
4572
|
+
// as a literal per row.
|
|
4573
|
+
expect(css).toMatch(
|
|
4574
|
+
/\.compose-dialog,\s*\.compose-page-shell\s*>\s*jant-compose-dialog\s*\{[\s\S]*--compose-gutter:\s*20px;/,
|
|
4575
|
+
);
|
|
4576
|
+
expect(css).toMatch(
|
|
4577
|
+
/@media \(min-width:\s*700px\)\s*\{[\s\S]*--compose-gutter:\s*24px;/,
|
|
4578
|
+
);
|
|
4579
|
+
// Zero wherever the indent already comes from outside the editor.
|
|
4580
|
+
expect(css).toMatch(
|
|
4581
|
+
/\.compose-editor-row\s*\{\s*\/\*[\s\S]*?\*\/\s*--compose-gutter:\s*0px;/,
|
|
4582
|
+
);
|
|
4583
|
+
|
|
4584
|
+
for (const rule of [
|
|
4585
|
+
/\.compose-body\s*\{[^}]*padding:\s*16px 20px 12px var\(--compose-gutter\);/,
|
|
4586
|
+
/\.compose-tools-row\s*\{[\s\S]*padding:\s*6px 10px 6px var\(--compose-gutter\);/,
|
|
4587
|
+
/\.compose-attachments-dock\s*\{\s*padding:\s*8px 20px 4px var\(--compose-gutter\);/,
|
|
4588
|
+
/\.compose-thread-post-header\s*\{[\s\S]*padding:\s*10px 16px 2px var\(--compose-gutter\);/,
|
|
4589
|
+
/\.compose-add-thread-trigger\s*\{\s*padding:\s*1px 10px 4px var\(--compose-gutter\);/,
|
|
4590
|
+
/\.compose-action-row\s*\{\s*padding:\s*10px 12px 14px var\(--compose-gutter\);/,
|
|
4591
|
+
]) {
|
|
4592
|
+
expect(css).toMatch(rule);
|
|
4593
|
+
}
|
|
4594
|
+
|
|
4595
|
+
// Each control hangs its box back out of the column by its own optical
|
|
4596
|
+
// inset, so the glyph or the label lands on the edge and not the box.
|
|
4597
|
+
expect(css).toMatch(
|
|
4598
|
+
/\.compose-tools-row\s*\{[\s\S]*--compose-tool-hang:\s*12px;/,
|
|
4599
|
+
);
|
|
4600
|
+
expect(css).toMatch(
|
|
4601
|
+
/\.compose-tools-row > :first-child\s*\{\s*margin-inline-start:\s*calc\(-1 \* var\(--compose-tool-hang\)\);/,
|
|
4602
|
+
);
|
|
4603
|
+
expect(css).toMatch(
|
|
4604
|
+
/\.compose-thread-add-btn\s*\{[\s\S]*padding:\s*5px var\(--compose-add-hang\);\s*margin-inline-start:\s*calc\(-1 \* var\(--compose-add-hang\)\);/,
|
|
4605
|
+
);
|
|
4606
|
+
expect(css).toMatch(
|
|
4607
|
+
/\.compose-collection-select\s*\{[\s\S]*margin-inline-start:\s*calc\(\s*-1 \* \(var\(--compose-collection-pad-icon\) \+ 1px\)\s*\);/,
|
|
4608
|
+
);
|
|
4609
|
+
|
|
4610
|
+
// The ad-hoc thread-only indents these replaced are gone — they were what
|
|
4611
|
+
// let each mode drift to its own edge in the first place.
|
|
4612
|
+
expect(css).not.toMatch(/margin-inline-start:\s*-10px;/);
|
|
4613
|
+
expect(css).not.toMatch(/\.compose-thread-layout \.compose-tools-row\s*\{/);
|
|
4614
|
+
// One "Add to thread" button, not two rule sets that have to stay in step.
|
|
4615
|
+
expect(css).not.toMatch(/\.compose-add-thread-btn/);
|
|
4616
|
+
});
|
|
4617
|
+
|
|
4618
|
+
it("declares one marker vocabulary for the compose and reading rails", () => {
|
|
4619
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4620
|
+
|
|
4621
|
+
// Both rails draw the same dot; only the surface behind it differs.
|
|
4622
|
+
// `[^}]*` rather than `[\s\S]*` throughout: a greedy any-char run walks past
|
|
4623
|
+
// the rule's closing brace and happily matches a later rule's declaration.
|
|
4624
|
+
expect(css).toMatch(
|
|
4625
|
+
/\.thread-group,\s*\.compose-thread-layout\s*\{\s*--site-thread-marker-size:\s*7px;[^}]*--site-thread-marker-gap:\s*9px;/,
|
|
4626
|
+
);
|
|
4627
|
+
expect(css).toMatch(
|
|
4628
|
+
/\.compose-thread-layout\s*\{\s*--site-thread-marker-surface:\s*var\(--compose-paper-bg\);\s*\}/,
|
|
4629
|
+
);
|
|
4630
|
+
// 7:1.5 marker-to-rail on both, from one token — compose used to hardcode
|
|
4631
|
+
// its own 1.5px while reading ran a 1px hairline under the same 7px dot.
|
|
4632
|
+
expect(css).toMatch(/--site-thread-rail-line-width:\s*1\.5px;/);
|
|
4633
|
+
expect(css).not.toMatch(
|
|
4634
|
+
/\.thread-group-preview,\s*\.thread-group-detail\s*\{[^}]*--site-thread-rail-line-width:/,
|
|
4635
|
+
);
|
|
4636
|
+
expect(css).toMatch(
|
|
4637
|
+
/\.compose-thread-layout > \*::before\s*\{[^}]*width:\s*var\(--site-thread-rail-line-width\);\s*margin-left:\s*calc\(var\(--site-thread-rail-line-width\) \/ -2\);/,
|
|
4638
|
+
);
|
|
4639
|
+
// The reading rail's fade-in gradient stays — it softens where collapsed
|
|
4640
|
+
// ancestor context begins, which compose solves by trimming per row.
|
|
4641
|
+
expect(css).toMatch(
|
|
4642
|
+
/\.thread-group-preview::before,\s*\.thread-group-detail::before\s*\{\s*background:\s*linear-gradient\(/,
|
|
4643
|
+
);
|
|
4644
|
+
// Both rails now stop at the last dot. Reading masks its group-wide line
|
|
4645
|
+
// from that dot down; `> ` keeps the mask off the last ancestor inside
|
|
4646
|
+
// `.thread-context-shell`, which the rail must run straight through.
|
|
4647
|
+
expect(css).toMatch(
|
|
4648
|
+
/\.thread-group > \.thread-item:last-child::after\s*\{[^}]*top:\s*50%;\s*bottom:\s*0;\s*width:\s*var\(--site-thread-rail-line-width\);\s*background-color:\s*var\(--site-thread-marker-surface\);/,
|
|
4649
|
+
);
|
|
4650
|
+
expect(css).not.toMatch(/\.thread-group \.thread-item:last-child::after/);
|
|
4651
|
+
// The ring is reading's alone: the token is declared on `.thread-group`, and
|
|
4652
|
+
// compose's dot rule never reads it. The compose rail is quiet by design —
|
|
4653
|
+
// it already has a card edge, a format switcher and a "1/3" per row.
|
|
4654
|
+
expect(css).toMatch(
|
|
4655
|
+
/\.thread-group\s*\{[^}]*--site-thread-marker-ring-inset:\s*2px;\s*--site-thread-marker-ring-width:\s*2px;/,
|
|
4656
|
+
);
|
|
4657
|
+
expect(css).not.toMatch(
|
|
4658
|
+
/\.compose-thread-layout\s*\{[^}]*--site-thread-marker-ring/,
|
|
4659
|
+
);
|
|
4660
|
+
expect(css).not.toMatch(
|
|
4661
|
+
/\.compose-thread-dot::before\s*\{[^}]*--site-thread-(dot-ring|marker-ring)/,
|
|
4662
|
+
);
|
|
4663
|
+
// The per-dot border and the size-based hero markers stay gone: a border
|
|
4664
|
+
// eats into the fill instead of sitting outside it, and a bigger dot
|
|
4665
|
+
// repeats what the full card beside it already says.
|
|
4666
|
+
for (const dead of [
|
|
4667
|
+
"--site-thread-marker-border-width",
|
|
4668
|
+
"--site-thread-marker-hero-size",
|
|
4669
|
+
"--site-thread-marker-hero-border-width",
|
|
4670
|
+
]) {
|
|
4671
|
+
expect(css).not.toContain(dead);
|
|
4672
|
+
}
|
|
4673
|
+
expect(css).not.toMatch(/\.compose-thread-dot(::before)?\s*\{[^}]*border:/);
|
|
3059
4674
|
});
|
|
3060
4675
|
|
|
3061
|
-
it("
|
|
4676
|
+
it("punches the dot's gap with the surface colour on both rails", () => {
|
|
3062
4677
|
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
3063
4678
|
|
|
4679
|
+
// Compose stops at the punch-out; reading layers its ring inside the same
|
|
4680
|
+
// radius, so the outermost layer — the one that breaks the rail line — is
|
|
4681
|
+
// still the gap at its full size on both rails.
|
|
4682
|
+
const punchOut =
|
|
4683
|
+
/0 0 0 var\(--site-thread-marker-gap\)\s*var\(--site-thread-marker-surface\);/;
|
|
3064
4684
|
expect(css).toMatch(
|
|
3065
|
-
|
|
4685
|
+
new RegExp(
|
|
4686
|
+
`\\.compose-thread-dot::before\\s*\\{[^}]*box-shadow:\\s*${punchOut.source}`,
|
|
4687
|
+
),
|
|
3066
4688
|
);
|
|
3067
|
-
expect(css).toMatch(/\.compose-tools-row\s*\{[\s\S]*flex-shrink:\s*0;/);
|
|
3068
4689
|
expect(css).toMatch(
|
|
3069
|
-
|
|
4690
|
+
new RegExp(`\\.thread-item::before\\s*\\{[^}]*${punchOut.source}`),
|
|
3070
4691
|
);
|
|
4692
|
+
// Reading's ring: a band of surface, then the accent tint, both drawn as
|
|
4693
|
+
// spread inside the punch. A border would eat the fill instead.
|
|
3071
4694
|
expect(css).toMatch(
|
|
3072
|
-
/\.
|
|
4695
|
+
/\.thread-item::before\s*\{[^}]*box-shadow:\s*0 0 0 var\(--site-thread-marker-ring-inset\)\s*var\(--site-thread-marker-surface\),\s*0 0 0\s*calc\(\s*var\(--site-thread-marker-ring-inset\) \+\s*var\(--site-thread-marker-ring-width\)\s*\)\s*var\(--site-thread-dot-ring\),/,
|
|
3073
4696
|
);
|
|
4697
|
+
expect(css).not.toMatch(/\.thread-item::before\s*\{[^}]*border:\s/);
|
|
4698
|
+
});
|
|
4699
|
+
|
|
4700
|
+
it("marks the post a view is about with an accent fill, not a bigger dot", () => {
|
|
4701
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4702
|
+
|
|
4703
|
+
// Feed hero, curated highlight and the opened detail post share one rule,
|
|
4704
|
+
// matching compose's `.is-current`. Size is not a second signal.
|
|
3074
4705
|
expect(css).toMatch(
|
|
3075
|
-
/\.
|
|
4706
|
+
/\.thread-item-hero::before,\s*\.thread-item-curated::before,\s*\.thread-item-current::before\s*\{\s*background-color:\s*var\(--site-thread-marker-current\);\s*\}/,
|
|
3076
4707
|
);
|
|
4708
|
+
// Both rails take the current-post colour from one token, and that token is
|
|
4709
|
+
// a tint — the raw accent made a 7px dot the loudest thing on the page.
|
|
3077
4710
|
expect(css).toMatch(
|
|
3078
|
-
/\.compose-
|
|
4711
|
+
/\.compose-editor-row\.is-current \.compose-thread-dot::before\s*\{\s*background-color:\s*var\(--site-thread-marker-current\);/,
|
|
4712
|
+
);
|
|
4713
|
+
expect(css).toMatch(
|
|
4714
|
+
/--site-thread-marker-current:\s*color-mix\(\s*in srgb,\s*var\(--site-accent\) 90%,\s*var\(--site-threadline\)\s*\);/,
|
|
4715
|
+
);
|
|
4716
|
+
expect(css).not.toMatch(
|
|
4717
|
+
/\.thread-item-current::before\s*\{[^}]*var\(--site-accent\)[^-]/,
|
|
3079
4718
|
);
|
|
4719
|
+
// Every dot's geometry now derives from the shared token.
|
|
3080
4720
|
expect(css).toMatch(
|
|
3081
|
-
|
|
4721
|
+
/\.thread-item::before\s*\{[^}]*width:\s*var\(--site-thread-marker-size\);/,
|
|
3082
4722
|
);
|
|
4723
|
+
expect(css).not.toMatch(/\.thread-item::before\s*\{[^}]*width:\s*\d+px;/);
|
|
4724
|
+
});
|
|
4725
|
+
|
|
4726
|
+
it("centres every rail child on the line by giving them a shared track", () => {
|
|
4727
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4728
|
+
|
|
4729
|
+
// The line's x is derived from the track, never from a dot's own size — a
|
|
4730
|
+
// hardcoded half-marker is what put the line down the dot's edge before.
|
|
3083
4731
|
expect(css).toMatch(
|
|
3084
|
-
|
|
4732
|
+
/--compose-thread-rail-left:\s*calc\(\s*var\(--compose-thread-padding-left\) \+ var\(--compose-thread-rail-width\) \/ 2\s*\);/,
|
|
3085
4733
|
);
|
|
4734
|
+
expect(css).not.toMatch(/--compose-thread-rail-left:[^;]*\+ \d/);
|
|
4735
|
+
|
|
4736
|
+
// Post dot: fills the track, centres the circle in it.
|
|
3086
4737
|
expect(css).toMatch(
|
|
3087
|
-
/\.compose-
|
|
4738
|
+
/\.compose-thread-dot\s*\{[\s\S]*justify-content:\s*center;[\s\S]*width:\s*var\(--compose-thread-rail-width\);/,
|
|
3088
4739
|
);
|
|
4740
|
+
// "Add to thread" placeholder: fills the track outright, so no offset.
|
|
3089
4741
|
expect(css).toMatch(
|
|
3090
|
-
/\.compose-
|
|
4742
|
+
/\.compose-thread-add-dot\s*\{[\s\S]*width:\s*var\(--compose-thread-rail-width\);[\s\S]*height:\s*var\(--compose-thread-rail-width\);/,
|
|
3091
4743
|
);
|
|
4744
|
+
expect(css).not.toMatch(/\.compose-thread-add-dot\s*\{[^}]*margin-left:/);
|
|
4745
|
+
// Reply meta hangs off the same track as the rows above it.
|
|
3092
4746
|
expect(css).toMatch(
|
|
3093
|
-
/\.compose-reply-
|
|
4747
|
+
/\.compose-reply-meta\s*\{[\s\S]*var\(--compose-thread-gap\) \+\s*var\(--compose-thread-rail-width\)/,
|
|
3094
4748
|
);
|
|
4749
|
+
});
|
|
4750
|
+
|
|
4751
|
+
it("accents the dot of the post holding the cursor, not every editor row", () => {
|
|
4752
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4753
|
+
|
|
4754
|
+
// Which selector carries the highlight — the colour itself is pinned by
|
|
4755
|
+
// "marks the post a view is about…", so this stays token-agnostic.
|
|
3095
4756
|
expect(css).toMatch(
|
|
3096
|
-
/\.compose-
|
|
4757
|
+
/\.compose-editor-row\.is-current \.compose-thread-dot::before\s*\{\s*background-color:/,
|
|
4758
|
+
);
|
|
4759
|
+
// The old rule lit every post in a thread at once.
|
|
4760
|
+
expect(css).not.toMatch(
|
|
4761
|
+
/\.compose-editor-row \.compose-thread-dot\s*\{\s*background-color:/,
|
|
3097
4762
|
);
|
|
4763
|
+
});
|
|
4764
|
+
|
|
4765
|
+
it("moves the current-post marker as focus moves between thread posts", async () => {
|
|
4766
|
+
const el = await createElement();
|
|
4767
|
+
el._threadItems = [
|
|
4768
|
+
{ id: "thread-1", format: "note" },
|
|
4769
|
+
{ id: "thread-2", format: "note" },
|
|
4770
|
+
{ id: "thread-3", format: "note" },
|
|
4771
|
+
];
|
|
4772
|
+
await flushUpdates(el);
|
|
4773
|
+
|
|
4774
|
+
const currentIndexes = () =>
|
|
4775
|
+
Array.from(el.querySelectorAll<HTMLElement>(".compose-thread-post"))
|
|
4776
|
+
.map((row, index) =>
|
|
4777
|
+
row.classList.contains("is-current") ? index : -1,
|
|
4778
|
+
)
|
|
4779
|
+
.filter((index) => index >= 0);
|
|
4780
|
+
|
|
4781
|
+
// Exactly one at a time — the old rule accented all three.
|
|
4782
|
+
expect(currentIndexes()).toEqual([0]);
|
|
4783
|
+
|
|
4784
|
+
const second = requireElement(
|
|
4785
|
+
el.querySelector<HTMLElement>(
|
|
4786
|
+
'.compose-thread-post[data-thread-index="1"]',
|
|
4787
|
+
),
|
|
4788
|
+
"expected the second thread post",
|
|
4789
|
+
);
|
|
4790
|
+
second.dispatchEvent(new Event("focusin", { bubbles: true }));
|
|
4791
|
+
await flushUpdates(el);
|
|
4792
|
+
|
|
4793
|
+
expect(currentIndexes()).toEqual([1]);
|
|
4794
|
+
});
|
|
4795
|
+
|
|
4796
|
+
it("keeps passive footnote references quiet until the editor selects them", () => {
|
|
4797
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4798
|
+
|
|
3098
4799
|
expect(css).toMatch(
|
|
3099
|
-
/\.compose-
|
|
4800
|
+
/\.compose-tiptap-body \.tiptap \.tiptap-footnote-reference\s*\{[^}]*color:\s*var\(--site-footnote-marker\);/,
|
|
3100
4801
|
);
|
|
3101
4802
|
expect(css).toMatch(
|
|
3102
|
-
/\.
|
|
4803
|
+
/\.tiptap-footnote-reference\.ProseMirror-selectednode\s*\{[^}]*color:\s*var\(--site-accent\);[^}]*outline:\s*1px solid var\(--site-accent\);/,
|
|
3103
4804
|
);
|
|
3104
4805
|
});
|
|
3105
4806
|
|
|
@@ -3125,6 +4826,7 @@ describe("JantComposeDialog", () => {
|
|
|
3125
4826
|
alt: "",
|
|
3126
4827
|
error: null,
|
|
3127
4828
|
posterUrl: null,
|
|
4829
|
+
remoteUrl: null,
|
|
3128
4830
|
summary: null,
|
|
3129
4831
|
chars: null,
|
|
3130
4832
|
},
|
|
@@ -3167,6 +4869,7 @@ describe("JantComposeDialog", () => {
|
|
|
3167
4869
|
alt: "",
|
|
3168
4870
|
error: null,
|
|
3169
4871
|
posterUrl: null,
|
|
4872
|
+
remoteUrl: null,
|
|
3170
4873
|
summary: null,
|
|
3171
4874
|
chars: null,
|
|
3172
4875
|
},
|
|
@@ -3222,6 +4925,7 @@ describe("JantComposeDialog", () => {
|
|
|
3222
4925
|
alt: "A test image",
|
|
3223
4926
|
error: null,
|
|
3224
4927
|
posterUrl: null,
|
|
4928
|
+
remoteUrl: null,
|
|
3225
4929
|
summary: null,
|
|
3226
4930
|
chars: null,
|
|
3227
4931
|
},
|
|
@@ -3292,6 +4996,7 @@ describe("JantComposeDialog", () => {
|
|
|
3292
4996
|
alt: "Alt for pending",
|
|
3293
4997
|
error: null,
|
|
3294
4998
|
posterUrl: null,
|
|
4999
|
+
remoteUrl: null,
|
|
3295
5000
|
summary: null,
|
|
3296
5001
|
chars: null,
|
|
3297
5002
|
},
|
|
@@ -3367,7 +5072,97 @@ describe("JantComposeDialog", () => {
|
|
|
3367
5072
|
);
|
|
3368
5073
|
});
|
|
3369
5074
|
|
|
3370
|
-
// ──
|
|
5075
|
+
// ── Leaving compose ────────────────────────────────────────────────
|
|
5076
|
+
|
|
5077
|
+
it("names the dialog for assistive tech now that no title is drawn", async () => {
|
|
5078
|
+
// The visible title carried the dialog's accessible name; it moves to an
|
|
5079
|
+
// attribute rather than disappearing with the row.
|
|
5080
|
+
const dialog = document.createElement("dialog");
|
|
5081
|
+
document.body.appendChild(dialog);
|
|
5082
|
+
const el = document.createElement(
|
|
5083
|
+
"jant-compose-dialog",
|
|
5084
|
+
) as JantComposeDialog;
|
|
5085
|
+
el.collections = collections;
|
|
5086
|
+
el.labels = labels;
|
|
5087
|
+
dialog.appendChild(el);
|
|
5088
|
+
await flushUpdates(el);
|
|
5089
|
+
|
|
5090
|
+
expect(dialog.getAttribute("aria-label")).toBe(labels.composeDialogLabel);
|
|
5091
|
+
|
|
5092
|
+
// Tear the dialog down here: happy-dom throws when `body.innerHTML = ""`
|
|
5093
|
+
// in the shared beforeEach removes one it did not itself create.
|
|
5094
|
+
el.remove();
|
|
5095
|
+
dialog.remove();
|
|
5096
|
+
});
|
|
5097
|
+
|
|
5098
|
+
it("closes from the × in the post header row", async () => {
|
|
5099
|
+
const el = await createElement();
|
|
5100
|
+
await flushUpdates(el);
|
|
5101
|
+
const requestCloseSpy = vi.spyOn(el, "requestClose");
|
|
5102
|
+
|
|
5103
|
+
const closeBtn = requireElement(
|
|
5104
|
+
el.querySelector<HTMLButtonElement>(
|
|
5105
|
+
".compose-thread-post-header .compose-close-btn",
|
|
5106
|
+
),
|
|
5107
|
+
"expected close button in the post header",
|
|
5108
|
+
);
|
|
5109
|
+
expect(closeBtn.getAttribute("aria-label")).toBe(labels.cancel);
|
|
5110
|
+
|
|
5111
|
+
closeBtn.click();
|
|
5112
|
+
await flushUpdates(el);
|
|
5113
|
+
|
|
5114
|
+
expect(requestCloseSpy).toHaveBeenCalledTimes(1);
|
|
5115
|
+
});
|
|
5116
|
+
|
|
5117
|
+
it("hands the header slot back to per-post remove in a thread, and moves the exit into the options panel", async () => {
|
|
5118
|
+
const el = await createElement();
|
|
5119
|
+
el._threadItems = [
|
|
5120
|
+
{ id: "thread-1", format: "note" },
|
|
5121
|
+
{ id: "thread-2", format: "note" },
|
|
5122
|
+
];
|
|
5123
|
+
await flushUpdates(el);
|
|
5124
|
+
|
|
5125
|
+
// Each post owns its × for removing itself, so no close button competes.
|
|
5126
|
+
expect(el.querySelector(".compose-close-btn")).toBeNull();
|
|
5127
|
+
expect(
|
|
5128
|
+
el.querySelectorAll(".compose-thread-post-remove").length,
|
|
5129
|
+
).toBeGreaterThan(0);
|
|
5130
|
+
|
|
5131
|
+
await openPublishPanel(el);
|
|
5132
|
+
const closeRow = requireElement(
|
|
5133
|
+
Array.from(
|
|
5134
|
+
el.querySelectorAll<HTMLButtonElement>(
|
|
5135
|
+
".compose-publish-panel .compose-sheet-row",
|
|
5136
|
+
),
|
|
5137
|
+
).find(
|
|
5138
|
+
(row) =>
|
|
5139
|
+
row.querySelector(".compose-sheet-title")?.textContent?.trim() ===
|
|
5140
|
+
labels.closeCompose,
|
|
5141
|
+
) ?? null,
|
|
5142
|
+
"expected a close row in the options panel",
|
|
5143
|
+
);
|
|
5144
|
+
|
|
5145
|
+
const requestCloseSpy = vi.spyOn(el, "requestClose");
|
|
5146
|
+
closeRow.click();
|
|
5147
|
+
await flushUpdates(el);
|
|
5148
|
+
|
|
5149
|
+
expect(el._showPublishPanel).toBe(false);
|
|
5150
|
+
expect(requestCloseSpy).toHaveBeenCalledTimes(1);
|
|
5151
|
+
});
|
|
5152
|
+
|
|
5153
|
+
it("keeps the exit out of the options panel when the × already offers one", async () => {
|
|
5154
|
+
const el = await createElement();
|
|
5155
|
+
await openPublishPanel(el);
|
|
5156
|
+
|
|
5157
|
+
const titles = Array.from(
|
|
5158
|
+
el.querySelectorAll<HTMLElement>(
|
|
5159
|
+
".compose-publish-panel .compose-sheet-title",
|
|
5160
|
+
),
|
|
5161
|
+
).map((n) => n.textContent?.trim());
|
|
5162
|
+
|
|
5163
|
+
expect(titles).not.toContain(labels.closeCompose);
|
|
5164
|
+
expect(titles).toContain("Drafts");
|
|
5165
|
+
});
|
|
3371
5166
|
|
|
3372
5167
|
it("requestClose on empty form closes immediately without confirmation", async () => {
|
|
3373
5168
|
const el = await createElement();
|
|
@@ -3879,6 +5674,57 @@ describe("JantComposeDialog", () => {
|
|
|
3879
5674
|
expect(tiptap.getText()).toBe("");
|
|
3880
5675
|
});
|
|
3881
5676
|
|
|
5677
|
+
it("Escape cancels the table size picker without closing compose", async () => {
|
|
5678
|
+
const el = await createElement();
|
|
5679
|
+
const editor = requireElement(
|
|
5680
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
5681
|
+
"expected compose editor",
|
|
5682
|
+
);
|
|
5683
|
+
const tiptap = requireEditor(editor);
|
|
5684
|
+
|
|
5685
|
+
tiptap.commands.focus("end");
|
|
5686
|
+
tiptap.commands.insertContent("/tab");
|
|
5687
|
+
await flushUpdates(el);
|
|
5688
|
+
|
|
5689
|
+
const tableItem = Array.from(
|
|
5690
|
+
document.querySelectorAll<HTMLElement>(".tiptap-slash-item"),
|
|
5691
|
+
).find((item) => item.textContent?.includes("Table"));
|
|
5692
|
+
requireElement(
|
|
5693
|
+
tableItem ?? null,
|
|
5694
|
+
"expected Table slash item",
|
|
5695
|
+
).dispatchEvent(
|
|
5696
|
+
new globalThis.MouseEvent("mousedown", {
|
|
5697
|
+
bubbles: true,
|
|
5698
|
+
cancelable: true,
|
|
5699
|
+
}),
|
|
5700
|
+
);
|
|
5701
|
+
await vi.waitFor(() => {
|
|
5702
|
+
expect(
|
|
5703
|
+
document.querySelector(".tiptap-table-size-picker"),
|
|
5704
|
+
).not.toBeNull();
|
|
5705
|
+
});
|
|
5706
|
+
|
|
5707
|
+
const requestCloseSpy = vi.spyOn(el, "requestClose");
|
|
5708
|
+
const currentSize = requireElement(
|
|
5709
|
+
document.querySelector<HTMLButtonElement>(
|
|
5710
|
+
".tiptap-table-size-cell.is-current",
|
|
5711
|
+
),
|
|
5712
|
+
"expected current table size",
|
|
5713
|
+
);
|
|
5714
|
+
const escapeEvent = new globalThis.KeyboardEvent("keydown", {
|
|
5715
|
+
key: "Escape",
|
|
5716
|
+
bubbles: true,
|
|
5717
|
+
cancelable: true,
|
|
5718
|
+
});
|
|
5719
|
+
currentSize.dispatchEvent(escapeEvent);
|
|
5720
|
+
await flushUpdates(el);
|
|
5721
|
+
|
|
5722
|
+
expect(escapeEvent.defaultPrevented).toBe(true);
|
|
5723
|
+
expect(requestCloseSpy).not.toHaveBeenCalled();
|
|
5724
|
+
expect(document.querySelector(".tiptap-table-size-picker")).toBeNull();
|
|
5725
|
+
expect(tiptap.getText()).toBe("");
|
|
5726
|
+
});
|
|
5727
|
+
|
|
3882
5728
|
it("shows a clear empty state when slash commands have no matches", async () => {
|
|
3883
5729
|
const el = await createElement();
|
|
3884
5730
|
const editor = requireElement(
|
|
@@ -4101,10 +5947,12 @@ describe("JantComposeDialog", () => {
|
|
|
4101
5947
|
};
|
|
4102
5948
|
await editor.updateComplete;
|
|
4103
5949
|
|
|
4104
|
-
const submitSpy = vi
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
5950
|
+
const submitSpy = vi
|
|
5951
|
+
.spyOn(
|
|
5952
|
+
el as unknown as { _submit: (status: "published" | "draft") => void },
|
|
5953
|
+
"_submit",
|
|
5954
|
+
)
|
|
5955
|
+
.mockImplementation(() => {});
|
|
4108
5956
|
|
|
4109
5957
|
el.dispatchEvent(
|
|
4110
5958
|
new globalThis.KeyboardEvent("keydown", {
|
|
@@ -4117,6 +5965,139 @@ describe("JantComposeDialog", () => {
|
|
|
4117
5965
|
expect(submitSpy).toHaveBeenCalledWith("published");
|
|
4118
5966
|
});
|
|
4119
5967
|
|
|
5968
|
+
it("publishes the latest fullscreen content after handing it back", async () => {
|
|
5969
|
+
const el = await createElement();
|
|
5970
|
+
const editor = requireElement(
|
|
5971
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
5972
|
+
"expected compose editor",
|
|
5973
|
+
);
|
|
5974
|
+
const submitSpy = vi
|
|
5975
|
+
.spyOn(
|
|
5976
|
+
el as unknown as { _submit: (status: "published" | "draft") => void },
|
|
5977
|
+
"_submit",
|
|
5978
|
+
)
|
|
5979
|
+
.mockImplementation(() => {});
|
|
5980
|
+
const setEditorStateSpy = vi.spyOn(editor, "setEditorState");
|
|
5981
|
+
|
|
5982
|
+
(
|
|
5983
|
+
el as unknown as {
|
|
5984
|
+
_handleFullscreenClose: (
|
|
5985
|
+
event: CustomEvent<ComposeFullscreenCloseDetail>,
|
|
5986
|
+
) => void;
|
|
5987
|
+
}
|
|
5988
|
+
)._handleFullscreenClose(
|
|
5989
|
+
new CustomEvent<ComposeFullscreenCloseDetail>("jant:fullscreen-close", {
|
|
5990
|
+
detail: {
|
|
5991
|
+
json: {
|
|
5992
|
+
type: "doc",
|
|
5993
|
+
content: [
|
|
5994
|
+
{
|
|
5995
|
+
type: "paragraph",
|
|
5996
|
+
content: [{ type: "text", text: "Written while fullscreen" }],
|
|
5997
|
+
},
|
|
5998
|
+
],
|
|
5999
|
+
},
|
|
6000
|
+
title: "Fullscreen title",
|
|
6001
|
+
showTitle: true,
|
|
6002
|
+
replyExpanded: false,
|
|
6003
|
+
intent: "publish",
|
|
6004
|
+
editorIndex: 0,
|
|
6005
|
+
},
|
|
6006
|
+
}),
|
|
6007
|
+
);
|
|
6008
|
+
|
|
6009
|
+
expect(setEditorStateSpy).toHaveBeenCalled();
|
|
6010
|
+
expect(editor.getData()).toMatchObject({
|
|
6011
|
+
title: "Fullscreen title",
|
|
6012
|
+
body: expect.stringContaining("Written while fullscreen"),
|
|
6013
|
+
});
|
|
6014
|
+
expect(submitSpy).toHaveBeenCalledWith("published");
|
|
6015
|
+
});
|
|
6016
|
+
|
|
6017
|
+
it("does not submit an empty fullscreen post", async () => {
|
|
6018
|
+
const el = await createElement();
|
|
6019
|
+
const submitSpy = vi.spyOn(
|
|
6020
|
+
el as unknown as { _submit: (status: "published" | "draft") => void },
|
|
6021
|
+
"_submit",
|
|
6022
|
+
);
|
|
6023
|
+
|
|
6024
|
+
(
|
|
6025
|
+
el as unknown as {
|
|
6026
|
+
_handleFullscreenClose: (
|
|
6027
|
+
event: CustomEvent<ComposeFullscreenCloseDetail>,
|
|
6028
|
+
) => void;
|
|
6029
|
+
}
|
|
6030
|
+
)._handleFullscreenClose(
|
|
6031
|
+
new CustomEvent<ComposeFullscreenCloseDetail>("jant:fullscreen-close", {
|
|
6032
|
+
detail: {
|
|
6033
|
+
json: { type: "doc", content: [{ type: "paragraph" }] },
|
|
6034
|
+
title: "",
|
|
6035
|
+
showTitle: false,
|
|
6036
|
+
replyExpanded: false,
|
|
6037
|
+
intent: "publish",
|
|
6038
|
+
editorIndex: 0,
|
|
6039
|
+
},
|
|
6040
|
+
}),
|
|
6041
|
+
);
|
|
6042
|
+
|
|
6043
|
+
expect(submitSpy).not.toHaveBeenCalled();
|
|
6044
|
+
});
|
|
6045
|
+
|
|
6046
|
+
it("tracks the source editor for fullscreen thread editing", async () => {
|
|
6047
|
+
const el = await createElement();
|
|
6048
|
+
el._threadItems = [
|
|
6049
|
+
{ id: "thread-1", format: "note" },
|
|
6050
|
+
{ id: "thread-2", format: "note" },
|
|
6051
|
+
];
|
|
6052
|
+
await el.updateComplete;
|
|
6053
|
+
|
|
6054
|
+
const editors = Array.from(
|
|
6055
|
+
el.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
|
|
6056
|
+
);
|
|
6057
|
+
const openDetail: ComposeFullscreenOpenDetail = {
|
|
6058
|
+
json: null,
|
|
6059
|
+
title: "",
|
|
6060
|
+
showTitle: false,
|
|
6061
|
+
};
|
|
6062
|
+
editors[1]?.dispatchEvent(
|
|
6063
|
+
new CustomEvent<ComposeFullscreenOpenDetail>("jant:fullscreen-open", {
|
|
6064
|
+
bubbles: true,
|
|
6065
|
+
detail: openDetail,
|
|
6066
|
+
}),
|
|
6067
|
+
);
|
|
6068
|
+
|
|
6069
|
+
expect(openDetail).toMatchObject({ editorIndex: 1 });
|
|
6070
|
+
|
|
6071
|
+
(
|
|
6072
|
+
el as unknown as {
|
|
6073
|
+
_handleFullscreenClose: (
|
|
6074
|
+
event: CustomEvent<ComposeFullscreenCloseDetail>,
|
|
6075
|
+
) => void;
|
|
6076
|
+
}
|
|
6077
|
+
)._handleFullscreenClose(
|
|
6078
|
+
new CustomEvent<ComposeFullscreenCloseDetail>("jant:fullscreen-close", {
|
|
6079
|
+
detail: {
|
|
6080
|
+
json: {
|
|
6081
|
+
type: "doc",
|
|
6082
|
+
content: [
|
|
6083
|
+
{
|
|
6084
|
+
type: "paragraph",
|
|
6085
|
+
content: [{ type: "text", text: "Second thread post" }],
|
|
6086
|
+
},
|
|
6087
|
+
],
|
|
6088
|
+
},
|
|
6089
|
+
title: "",
|
|
6090
|
+
showTitle: false,
|
|
6091
|
+
replyExpanded: false,
|
|
6092
|
+
editorIndex: 1,
|
|
6093
|
+
},
|
|
6094
|
+
}),
|
|
6095
|
+
);
|
|
6096
|
+
|
|
6097
|
+
expect(editors[0]?.getData().body).toBe("");
|
|
6098
|
+
expect(editors[1]?.getData().body).toContain("Second thread post");
|
|
6099
|
+
});
|
|
6100
|
+
|
|
4120
6101
|
it("Cmd/Ctrl+Enter focuses the link URL when the URL is invalid", async () => {
|
|
4121
6102
|
const el = await createElement();
|
|
4122
6103
|
el._format = "link";
|
|
@@ -4643,9 +6624,12 @@ describe("JantComposeDialog", () => {
|
|
|
4643
6624
|
el._format = "note";
|
|
4644
6625
|
await el.updateComplete;
|
|
4645
6626
|
|
|
4646
|
-
// Format switcher should be visible
|
|
6627
|
+
// Format switcher should be visible, and the submit label says this is a
|
|
6628
|
+
// new post rather than an edit.
|
|
4647
6629
|
expect(el.querySelector(".compose-segmented")).not.toBeNull();
|
|
4648
|
-
expect(el.querySelector(".compose-
|
|
6630
|
+
expect(el.querySelector(".compose-publish-main")?.textContent?.trim()).toBe(
|
|
6631
|
+
"Post",
|
|
6632
|
+
);
|
|
4649
6633
|
|
|
4650
6634
|
// Button should say "Post", not "Done"
|
|
4651
6635
|
const postBtn = requireElement(
|
|
@@ -4730,11 +6714,94 @@ describe("JantComposeDialog", () => {
|
|
|
4730
6714
|
expect((receivedDetail as unknown as ComposeSubmitDetail).editPostId).toBe(
|
|
4731
6715
|
"draft789",
|
|
4732
6716
|
);
|
|
6717
|
+
expect(
|
|
6718
|
+
(receivedDetail as unknown as ComposeSubmitDetail).draftSourceId,
|
|
6719
|
+
).toBe("draft789");
|
|
4733
6720
|
expect((receivedDetail as unknown as ComposeSubmitDetail).status).toBe(
|
|
4734
6721
|
"published",
|
|
4735
6722
|
);
|
|
4736
6723
|
});
|
|
4737
6724
|
|
|
6725
|
+
it("opens a thread draft without deleting it and preserves its draft identity", async () => {
|
|
6726
|
+
const rootId = "pst_draft_root";
|
|
6727
|
+
const replyId = "pst_draft_reply";
|
|
6728
|
+
const requests: Array<{ url: string; method: string }> = [];
|
|
6729
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
|
|
6730
|
+
const url = String(input);
|
|
6731
|
+
requests.push({ url, method: init?.method ?? "GET" });
|
|
6732
|
+
|
|
6733
|
+
if (url === `/api/posts/${rootId}`) {
|
|
6734
|
+
return new Response(
|
|
6735
|
+
JSON.stringify({
|
|
6736
|
+
id: rootId,
|
|
6737
|
+
threadId: rootId,
|
|
6738
|
+
format: "note",
|
|
6739
|
+
status: "draft",
|
|
6740
|
+
slug: "draft-thread",
|
|
6741
|
+
title: "Draft thread",
|
|
6742
|
+
body: null,
|
|
6743
|
+
attachments: [],
|
|
6744
|
+
collectionIds: [],
|
|
6745
|
+
}),
|
|
6746
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
6747
|
+
);
|
|
6748
|
+
}
|
|
6749
|
+
|
|
6750
|
+
if (url === "/api/posts?status=draft&limit=50") {
|
|
6751
|
+
return new Response(
|
|
6752
|
+
JSON.stringify({
|
|
6753
|
+
posts: [
|
|
6754
|
+
{
|
|
6755
|
+
id: replyId,
|
|
6756
|
+
threadId: rootId,
|
|
6757
|
+
replyToId: rootId,
|
|
6758
|
+
format: "quote",
|
|
6759
|
+
status: "draft",
|
|
6760
|
+
quoteText: "Draft reply",
|
|
6761
|
+
attachments: [],
|
|
6762
|
+
},
|
|
6763
|
+
],
|
|
6764
|
+
}),
|
|
6765
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
6766
|
+
);
|
|
6767
|
+
}
|
|
6768
|
+
|
|
6769
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
6770
|
+
});
|
|
6771
|
+
|
|
6772
|
+
const el = await createElement();
|
|
6773
|
+
await el.openDraft(rootId);
|
|
6774
|
+
await flushUpdates(el);
|
|
6775
|
+
|
|
6776
|
+
expect(el._draftSourceId).toBe(rootId);
|
|
6777
|
+
expect(el.querySelectorAll("jant-compose-editor")).toHaveLength(2);
|
|
6778
|
+
expect(requests).toEqual([
|
|
6779
|
+
{ url: `/api/posts/${rootId}`, method: "GET" },
|
|
6780
|
+
{ url: "/api/posts?status=draft&limit=50", method: "GET" },
|
|
6781
|
+
]);
|
|
6782
|
+
|
|
6783
|
+
let receivedDetail: ComposeSubmitDetail | null = null;
|
|
6784
|
+
el.addEventListener("jant:compose-submit-deferred", (event) => {
|
|
6785
|
+
receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
|
|
6786
|
+
});
|
|
6787
|
+
requireElement(
|
|
6788
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
6789
|
+
"expected publish button",
|
|
6790
|
+
).click();
|
|
6791
|
+
|
|
6792
|
+
expect(receivedDetail).not.toBeNull();
|
|
6793
|
+
expect(
|
|
6794
|
+
(receivedDetail as unknown as ComposeSubmitDetail).draftSourceId,
|
|
6795
|
+
).toBe(rootId);
|
|
6796
|
+
expect((receivedDetail as unknown as ComposeSubmitDetail).editPostId).toBe(
|
|
6797
|
+
rootId,
|
|
6798
|
+
);
|
|
6799
|
+
expect(
|
|
6800
|
+
(receivedDetail as unknown as ComposeSubmitDetail).threadPosts,
|
|
6801
|
+
).toHaveLength(2);
|
|
6802
|
+
expect(requests.every((request) => request.method === "GET")).toBe(true);
|
|
6803
|
+
});
|
|
6804
|
+
|
|
4738
6805
|
it("draft button confirm save dispatches draft then opens drafts panel", async () => {
|
|
4739
6806
|
const el = await createElement();
|
|
4740
6807
|
const editor = requireElement(
|
|
@@ -4764,13 +6831,10 @@ describe("JantComposeDialog", () => {
|
|
|
4764
6831
|
receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
|
|
4765
6832
|
});
|
|
4766
6833
|
|
|
4767
|
-
// Click
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
);
|
|
4772
|
-
draftBtn.click();
|
|
4773
|
-
await el.updateComplete;
|
|
6834
|
+
// Click the drafts row in the options panel → confirm panel
|
|
6835
|
+
await openPublishPanel(el);
|
|
6836
|
+
requireElement(draftsRow(el), "expected drafts row").click();
|
|
6837
|
+
await flushUpdates(el);
|
|
4774
6838
|
expect(el._confirmPanelOpen).toBe(true);
|
|
4775
6839
|
|
|
4776
6840
|
// Click "Save"
|
|
@@ -4835,13 +6899,10 @@ describe("JantComposeDialog", () => {
|
|
|
4835
6899
|
submitFired = true;
|
|
4836
6900
|
});
|
|
4837
6901
|
|
|
4838
|
-
// Click
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
);
|
|
4843
|
-
draftBtn.click();
|
|
4844
|
-
await el.updateComplete;
|
|
6902
|
+
// Click the drafts row in the options panel → confirm panel
|
|
6903
|
+
await openPublishPanel(el);
|
|
6904
|
+
requireElement(draftsRow(el), "expected drafts row").click();
|
|
6905
|
+
await flushUpdates(el);
|
|
4845
6906
|
|
|
4846
6907
|
// Click "Don't save"
|
|
4847
6908
|
requireElement(
|
|
@@ -4883,6 +6944,7 @@ describe("JantComposeDialog", () => {
|
|
|
4883
6944
|
alt: "",
|
|
4884
6945
|
error: null,
|
|
4885
6946
|
posterUrl: null,
|
|
6947
|
+
remoteUrl: null,
|
|
4886
6948
|
summary: null,
|
|
4887
6949
|
chars: null,
|
|
4888
6950
|
},
|