@jant/core 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const { showConfirmDialogMock, showToastMock, queueToastMock } = vi.hoisted(
|
|
6
|
+
() => ({
|
|
7
|
+
showConfirmDialogMock: vi.fn(),
|
|
8
|
+
showToastMock: vi.fn(),
|
|
9
|
+
queueToastMock: vi.fn(),
|
|
10
|
+
}),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
vi.mock("../../confirm.js", () => ({
|
|
14
|
+
showConfirmDialog: showConfirmDialogMock,
|
|
15
|
+
}));
|
|
16
|
+
vi.mock("../../toast.js", () => ({
|
|
17
|
+
showToast: showToastMock,
|
|
18
|
+
queueToastForNextPage: queueToastMock,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import "../jant-settings-language.js";
|
|
22
|
+
import type { JantSettingsLanguage } from "../jant-settings-language.js";
|
|
23
|
+
|
|
24
|
+
const labels = {
|
|
25
|
+
siteSection: "Site",
|
|
26
|
+
dashboardSection: "Dashboard",
|
|
27
|
+
contentLanguage: "Content language",
|
|
28
|
+
contentLanguageHelp: "The language your readers and search engines see.",
|
|
29
|
+
primaryLanguage: "Primary language",
|
|
30
|
+
primaryLanguageHelp: "The root address (/, /feed) shows this language.",
|
|
31
|
+
dashboardLanguage: "Dashboard language",
|
|
32
|
+
dashboardLanguageHelp: "The language of your admin pages. Only you see this.",
|
|
33
|
+
followContent: "Follow content language",
|
|
34
|
+
multilingual: "Multilingual content",
|
|
35
|
+
multilingualHelp: "Give each language its own home page, archive, and feed.",
|
|
36
|
+
multilingualDocs: "Multilingual guide",
|
|
37
|
+
multilingualDocsHelp:
|
|
38
|
+
"URL structure, per-language feeds, and linking translations.",
|
|
39
|
+
statusOn: "On",
|
|
40
|
+
turnOn: "Turn on",
|
|
41
|
+
addMissingLanguage: "Add {language}",
|
|
42
|
+
viewPosts: "View these posts",
|
|
43
|
+
languagesLabel: "Languages",
|
|
44
|
+
primaryBadge: "Primary",
|
|
45
|
+
makePrimary: "Make primary",
|
|
46
|
+
otherLanguages: "Other languages",
|
|
47
|
+
addLanguage: "Add language",
|
|
48
|
+
removeLanguage: "Remove {language}",
|
|
49
|
+
languageMenu: "Options for {language}",
|
|
50
|
+
enableTitle: "Turn on multilingual content",
|
|
51
|
+
enableReassurance:
|
|
52
|
+
"Post addresses do not change, and you can turn this off again at any time.",
|
|
53
|
+
enableMarkTitle: "One-time change to your existing posts",
|
|
54
|
+
// Lingui resolves the plural server-side against the real count, so what
|
|
55
|
+
// reaches the component already has a number and only `{language}` left.
|
|
56
|
+
enableMarkWarning: "Your 347 existing posts will be marked as {language}.",
|
|
57
|
+
enableFixHint:
|
|
58
|
+
"Any post written in another language can be corrected from its own menu afterwards.",
|
|
59
|
+
enableNeedsLanguage: "Add at least one more language to turn this on.",
|
|
60
|
+
changePrimaryTitle: "Change the primary language?",
|
|
61
|
+
changePrimaryBody:
|
|
62
|
+
"{next} will be served at the root address, and {previous} moves to {prefix}.",
|
|
63
|
+
changePrimaryConfirm: "Switch",
|
|
64
|
+
disableTitle: "Turn off multilingual content?",
|
|
65
|
+
disableBody: "The {prefix} addresses redirect to the root.",
|
|
66
|
+
disableConfirm: "Turn off",
|
|
67
|
+
removeLastTitle: "Turn off multilingual content?",
|
|
68
|
+
removeLastBody:
|
|
69
|
+
"{language} is the only other language on this site, so removing it turns multilingual content off. {prefix} stops working.",
|
|
70
|
+
removeLastConfirm: "Remove and turn off",
|
|
71
|
+
cancel: "Cancel",
|
|
72
|
+
save: "Save",
|
|
73
|
+
saving: "Saving…",
|
|
74
|
+
searchPlaceholder: "Search…",
|
|
75
|
+
noMatches: "No matches.",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const locales = [
|
|
79
|
+
{ tag: "en", native: "English", english: "English", coverage: 1 },
|
|
80
|
+
{
|
|
81
|
+
tag: "zh-Hans",
|
|
82
|
+
native: "简体中文",
|
|
83
|
+
english: "Simplified Chinese",
|
|
84
|
+
coverage: 1,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
tag: "zh-Hant",
|
|
88
|
+
native: "繁體中文",
|
|
89
|
+
english: "Traditional Chinese",
|
|
90
|
+
coverage: 1,
|
|
91
|
+
},
|
|
92
|
+
{ tag: "ja", native: "日本語", english: "Japanese", coverage: 0 },
|
|
93
|
+
{ tag: "fi", native: "suomi", english: "Finnish", coverage: 0 },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
interface InitialState {
|
|
97
|
+
contentLanguage?: string;
|
|
98
|
+
dashboardLanguage?: string;
|
|
99
|
+
multilingualEnabled?: boolean;
|
|
100
|
+
additionalLanguages?: string[];
|
|
101
|
+
unmarkedPostCount?: number;
|
|
102
|
+
sitePathPrefix?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function seedInitialState(state: InitialState) {
|
|
106
|
+
const script = document.createElement("script");
|
|
107
|
+
script.type = "application/json";
|
|
108
|
+
script.id = "language-settings-initial-data";
|
|
109
|
+
script.textContent = JSON.stringify({
|
|
110
|
+
contentLanguage: "zh-Hans",
|
|
111
|
+
dashboardLanguage: "",
|
|
112
|
+
multilingualEnabled: false,
|
|
113
|
+
additionalLanguages: [],
|
|
114
|
+
unmarkedPostCount: 0,
|
|
115
|
+
sitePathPrefix: "",
|
|
116
|
+
...state,
|
|
117
|
+
});
|
|
118
|
+
document.body.appendChild(script);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function createElement(
|
|
122
|
+
state: InitialState = {},
|
|
123
|
+
): Promise<JantSettingsLanguage> {
|
|
124
|
+
seedInitialState(state);
|
|
125
|
+
const el = document.createElement(
|
|
126
|
+
"jant-settings-language",
|
|
127
|
+
) as JantSettingsLanguage;
|
|
128
|
+
el.labels = labels;
|
|
129
|
+
el.locales = locales;
|
|
130
|
+
document.body.appendChild(el);
|
|
131
|
+
await el.updateComplete;
|
|
132
|
+
return el;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Stub fetch with a fixed JSON response and record every call. */
|
|
136
|
+
function mockFetch(response: unknown, ok = true) {
|
|
137
|
+
const calls: { url: string; body: unknown }[] = [];
|
|
138
|
+
vi.stubGlobal(
|
|
139
|
+
"fetch",
|
|
140
|
+
vi.fn(async (url: string, init?: { body?: string }) => {
|
|
141
|
+
calls.push({
|
|
142
|
+
url,
|
|
143
|
+
body: init?.body ? JSON.parse(init.body) : undefined,
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
ok,
|
|
147
|
+
status: ok ? 200 : 400,
|
|
148
|
+
text: async () => JSON.stringify(response),
|
|
149
|
+
} as unknown as Response;
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
return calls;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function findByText<T extends globalThis.Element>(
|
|
156
|
+
el: globalThis.Element,
|
|
157
|
+
selector: string,
|
|
158
|
+
text: string,
|
|
159
|
+
): T | undefined {
|
|
160
|
+
return Array.from(el.querySelectorAll<T>(selector)).find((node) =>
|
|
161
|
+
node.textContent?.includes(text),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Open the "⋯" actions menu on a language's row. */
|
|
166
|
+
async function openRowMenu(el: JantSettingsLanguage, language: string) {
|
|
167
|
+
el.querySelector<HTMLButtonElement>(
|
|
168
|
+
`[data-language-menu][aria-label="Options for ${language}"]`,
|
|
169
|
+
)?.click();
|
|
170
|
+
await el.updateComplete;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
describe("JantSettingsLanguage", () => {
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
document.body.innerHTML = "";
|
|
176
|
+
showConfirmDialogMock.mockReset();
|
|
177
|
+
showToastMock.mockReset();
|
|
178
|
+
queueToastMock.mockReset();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
afterEach(() => {
|
|
182
|
+
vi.unstubAllGlobals();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("single-language site", () => {
|
|
186
|
+
it("shows the content language and a quiet multilingual toggle", async () => {
|
|
187
|
+
const el = await createElement();
|
|
188
|
+
|
|
189
|
+
expect(el.textContent).toContain(labels.contentLanguage);
|
|
190
|
+
expect(el.textContent).toContain(labels.contentLanguageHelp);
|
|
191
|
+
// Multilingual is its own section, off by default: a description and
|
|
192
|
+
// a way to turn it on — no badge announcing the default state.
|
|
193
|
+
expect(el.textContent).toContain(labels.multilingual);
|
|
194
|
+
expect(el.textContent).toContain(labels.turnOn);
|
|
195
|
+
expect(el.querySelector(".badge-secondary")).toBeNull();
|
|
196
|
+
expect(el.querySelector("[data-multilingual-setup]")).not.toBeNull();
|
|
197
|
+
// No language management shows before the author opts in.
|
|
198
|
+
expect(el.textContent).not.toContain(labels.addLanguage);
|
|
199
|
+
expect(el.textContent).not.toContain(labels.otherLanguages);
|
|
200
|
+
expect(el.textContent).not.toContain(labels.languagesLabel);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("shows the language's own name, not its tag", async () => {
|
|
204
|
+
const el = await createElement({ contentLanguage: "zh-Hant" });
|
|
205
|
+
|
|
206
|
+
expect(el.textContent).toContain("繁體中文");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("saves a new content language without a confirmation", async () => {
|
|
210
|
+
const el = await createElement();
|
|
211
|
+
const calls = mockFetch({ toast: "Language updated." });
|
|
212
|
+
|
|
213
|
+
const trigger = el.querySelector<HTMLButtonElement>(
|
|
214
|
+
'[data-language-picker] button[aria-labelledby="language-primary-label"]',
|
|
215
|
+
);
|
|
216
|
+
trigger?.click();
|
|
217
|
+
await el.updateComplete;
|
|
218
|
+
|
|
219
|
+
const option = findByText<HTMLButtonElement>(
|
|
220
|
+
el,
|
|
221
|
+
'[role="option"]',
|
|
222
|
+
"日本語",
|
|
223
|
+
);
|
|
224
|
+
option?.click();
|
|
225
|
+
await el.updateComplete;
|
|
226
|
+
await vi.waitFor(() => expect(calls.length).toBe(1));
|
|
227
|
+
|
|
228
|
+
expect(calls[0]?.url).toBe("/settings/language");
|
|
229
|
+
expect(calls[0]?.body).toEqual({ contentLanguage: "ja" });
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("language picker", () => {
|
|
234
|
+
it("filters as the author types", async () => {
|
|
235
|
+
const el = await createElement();
|
|
236
|
+
const trigger = el.querySelector<HTMLButtonElement>(
|
|
237
|
+
'[data-language-picker] button[aria-labelledby="language-primary-label"]',
|
|
238
|
+
);
|
|
239
|
+
trigger?.click();
|
|
240
|
+
await el.updateComplete;
|
|
241
|
+
|
|
242
|
+
expect(trigger?.getAttribute("aria-expanded")).toBe("true");
|
|
243
|
+
expect(el.querySelectorAll('[role="option"]').length).toBe(
|
|
244
|
+
locales.length,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const search = el.querySelector<HTMLInputElement>(
|
|
248
|
+
"[data-language-search]",
|
|
249
|
+
);
|
|
250
|
+
if (search) {
|
|
251
|
+
search.value = "finn";
|
|
252
|
+
search.dispatchEvent(new Event("input", { bubbles: true }));
|
|
253
|
+
}
|
|
254
|
+
await el.updateComplete;
|
|
255
|
+
|
|
256
|
+
const filtered = el.querySelectorAll('[role="option"]');
|
|
257
|
+
expect(filtered.length).toBe(1);
|
|
258
|
+
expect(filtered[0]?.textContent).toContain("suomi");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("says so when nothing matches", async () => {
|
|
262
|
+
const el = await createElement();
|
|
263
|
+
el.querySelector<HTMLButtonElement>(
|
|
264
|
+
'[data-language-picker] button[aria-labelledby="language-primary-label"]',
|
|
265
|
+
)?.click();
|
|
266
|
+
await el.updateComplete;
|
|
267
|
+
|
|
268
|
+
const search = el.querySelector<HTMLInputElement>(
|
|
269
|
+
"[data-language-search]",
|
|
270
|
+
);
|
|
271
|
+
if (search) {
|
|
272
|
+
search.value = "zzzz";
|
|
273
|
+
search.dispatchEvent(new Event("input", { bubbles: true }));
|
|
274
|
+
}
|
|
275
|
+
await el.updateComplete;
|
|
276
|
+
|
|
277
|
+
expect(el.textContent).toContain(labels.noMatches);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("closes on Escape", async () => {
|
|
281
|
+
const el = await createElement();
|
|
282
|
+
const trigger = el.querySelector<HTMLButtonElement>(
|
|
283
|
+
'[data-language-picker] button[aria-labelledby="language-primary-label"]',
|
|
284
|
+
);
|
|
285
|
+
trigger?.click();
|
|
286
|
+
await el.updateComplete;
|
|
287
|
+
expect(el.querySelector("[data-language-search]")).not.toBeNull();
|
|
288
|
+
|
|
289
|
+
document.dispatchEvent(
|
|
290
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
291
|
+
);
|
|
292
|
+
await el.updateComplete;
|
|
293
|
+
|
|
294
|
+
expect(el.querySelector("[data-language-search]")).toBeNull();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("closes on an outside click", async () => {
|
|
298
|
+
const el = await createElement();
|
|
299
|
+
el.querySelector<HTMLButtonElement>(
|
|
300
|
+
'[data-language-picker] button[aria-labelledby="language-primary-label"]',
|
|
301
|
+
)?.click();
|
|
302
|
+
await el.updateComplete;
|
|
303
|
+
|
|
304
|
+
document.body.dispatchEvent(new Event("click", { bubbles: true }));
|
|
305
|
+
await el.updateComplete;
|
|
306
|
+
|
|
307
|
+
expect(el.querySelector("[data-language-search]")).toBeNull();
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("enable dialog", () => {
|
|
312
|
+
it("quotes the number of posts that will be marked", async () => {
|
|
313
|
+
const el = await createElement({ unmarkedPostCount: 347 });
|
|
314
|
+
|
|
315
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-setup]")?.click();
|
|
316
|
+
await el.updateComplete;
|
|
317
|
+
|
|
318
|
+
const dialog = el.querySelector("[data-enable-dialog]");
|
|
319
|
+
expect(dialog).not.toBeNull();
|
|
320
|
+
expect(dialog?.textContent).toContain("347 existing posts");
|
|
321
|
+
expect(dialog?.textContent).toContain("简体中文");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("fills the language slot the server left open", async () => {
|
|
325
|
+
const el = await createElement({ unmarkedPostCount: 347 });
|
|
326
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-setup]")?.click();
|
|
327
|
+
await el.updateComplete;
|
|
328
|
+
|
|
329
|
+
const dialog = el.querySelector("[data-enable-dialog]");
|
|
330
|
+
expect(dialog?.textContent).toContain("简体中文");
|
|
331
|
+
expect(dialog?.textContent).not.toContain("{language}");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("shows no marking warning when nothing needs marking", async () => {
|
|
335
|
+
const el = await createElement({ unmarkedPostCount: 0 });
|
|
336
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-setup]")?.click();
|
|
337
|
+
await el.updateComplete;
|
|
338
|
+
|
|
339
|
+
// Every post already carries a language (or there are none) — an alert
|
|
340
|
+
// about a change that will not happen would only confuse.
|
|
341
|
+
const dialog = el.querySelector("[data-enable-dialog]");
|
|
342
|
+
expect(dialog).not.toBeNull();
|
|
343
|
+
expect(dialog?.textContent).not.toContain(labels.enableMarkTitle);
|
|
344
|
+
expect(dialog?.textContent).not.toContain(labels.enableFixHint);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("keeps the warning in step with the primary language chosen in the dialog", async () => {
|
|
348
|
+
const el = await createElement({ unmarkedPostCount: 5 });
|
|
349
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-setup]")?.click();
|
|
350
|
+
await el.updateComplete;
|
|
351
|
+
|
|
352
|
+
const dialog = el.querySelector("[data-enable-dialog]");
|
|
353
|
+
expect(dialog?.textContent).toContain("简体中文");
|
|
354
|
+
|
|
355
|
+
el.querySelector<HTMLButtonElement>(
|
|
356
|
+
'button[aria-labelledby="language-enable-primary-label"]',
|
|
357
|
+
)?.click();
|
|
358
|
+
await el.updateComplete;
|
|
359
|
+
findByText<HTMLButtonElement>(el, '[role="option"]', "English")?.click();
|
|
360
|
+
await el.updateComplete;
|
|
361
|
+
|
|
362
|
+
expect(el.querySelector("[data-enable-dialog]")?.textContent).toContain(
|
|
363
|
+
"English",
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("shows a refusal inside the dialog and stays open", async () => {
|
|
368
|
+
// Re-enabling with a language dropped from the list while its posts
|
|
369
|
+
// remain — the server refuses, and a toast would hide under the modal.
|
|
370
|
+
const el = await createElement({ unmarkedPostCount: 0 });
|
|
371
|
+
const calls = mockFetch(
|
|
372
|
+
{ error: "3 posts are written in 繁體中文.", language: "zh-Hant" },
|
|
373
|
+
false,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-setup]")?.click();
|
|
377
|
+
await el.updateComplete;
|
|
378
|
+
el.querySelector<HTMLButtonElement>(
|
|
379
|
+
'button[aria-labelledby="language-enable-add-label"]',
|
|
380
|
+
)?.click();
|
|
381
|
+
await el.updateComplete;
|
|
382
|
+
findByText<HTMLButtonElement>(el, '[role="option"]', "English")?.click();
|
|
383
|
+
await el.updateComplete;
|
|
384
|
+
el.querySelector<HTMLButtonElement>("[data-enable-confirm]")?.click();
|
|
385
|
+
await vi.waitFor(() => expect(calls.length).toBe(1));
|
|
386
|
+
await vi.waitFor(() =>
|
|
387
|
+
expect(
|
|
388
|
+
el.querySelector('[data-enable-dialog] [role="alert"]'),
|
|
389
|
+
).not.toBeNull(),
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
expect(
|
|
393
|
+
el.querySelector('[data-enable-dialog] [role="alert"]')?.textContent,
|
|
394
|
+
).toContain("3 posts are written in 繁體中文.");
|
|
395
|
+
expect(showToastMock).not.toHaveBeenCalled();
|
|
396
|
+
|
|
397
|
+
// The refusal names the language, so putting it back is one click.
|
|
398
|
+
el.querySelector<HTMLButtonElement>("[data-enable-add-back]")?.click();
|
|
399
|
+
await el.updateComplete;
|
|
400
|
+
|
|
401
|
+
const dialog = el.querySelector("[data-enable-dialog]");
|
|
402
|
+
expect(dialog?.textContent).toContain("繁體中文");
|
|
403
|
+
expect(dialog?.querySelector('[role="alert"]')).toBeNull();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("cannot be confirmed without a second language", async () => {
|
|
407
|
+
const el = await createElement({ unmarkedPostCount: 3 });
|
|
408
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-setup]")?.click();
|
|
409
|
+
await el.updateComplete;
|
|
410
|
+
|
|
411
|
+
const confirm = el.querySelector<HTMLButtonElement>(
|
|
412
|
+
"[data-enable-confirm]",
|
|
413
|
+
);
|
|
414
|
+
expect(confirm?.disabled).toBe(true);
|
|
415
|
+
expect(el.textContent).toContain(labels.enableNeedsLanguage);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("posts the chosen languages and updates the page", async () => {
|
|
419
|
+
const el = await createElement({ unmarkedPostCount: 3 });
|
|
420
|
+
const calls = mockFetch({ toast: "Multilingual content is on." });
|
|
421
|
+
|
|
422
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-setup]")?.click();
|
|
423
|
+
await el.updateComplete;
|
|
424
|
+
|
|
425
|
+
el.querySelector<HTMLButtonElement>(
|
|
426
|
+
'button[aria-labelledby="language-enable-add-label"]',
|
|
427
|
+
)?.click();
|
|
428
|
+
await el.updateComplete;
|
|
429
|
+
findByText<HTMLButtonElement>(el, '[role="option"]', "English")?.click();
|
|
430
|
+
await el.updateComplete;
|
|
431
|
+
|
|
432
|
+
el.querySelector<HTMLButtonElement>("[data-enable-confirm]")?.click();
|
|
433
|
+
// The enabled view only exists once the change is saved.
|
|
434
|
+
await vi.waitFor(() =>
|
|
435
|
+
expect(el.querySelector("[data-multilingual-off]")).not.toBeNull(),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
expect(calls[0]?.url).toBe("/settings/language/enable");
|
|
439
|
+
expect(calls[0]?.body).toEqual({
|
|
440
|
+
primary: "zh-Hans",
|
|
441
|
+
additional: ["en"],
|
|
442
|
+
});
|
|
443
|
+
expect(el.textContent).toContain(labels.languagesLabel);
|
|
444
|
+
expect(el.textContent).toContain("English");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("carries the confirmation across the reload", async () => {
|
|
448
|
+
const el = await createElement({ unmarkedPostCount: 3 });
|
|
449
|
+
mockFetch({
|
|
450
|
+
toast: "Multilingual content is on. 3 posts were marked as 简体中文.",
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-setup]")?.click();
|
|
454
|
+
await el.updateComplete;
|
|
455
|
+
el.querySelector<HTMLButtonElement>(
|
|
456
|
+
'button[aria-labelledby="language-enable-add-label"]',
|
|
457
|
+
)?.click();
|
|
458
|
+
await el.updateComplete;
|
|
459
|
+
findByText<HTMLButtonElement>(el, '[role="option"]', "English")?.click();
|
|
460
|
+
await el.updateComplete;
|
|
461
|
+
el.querySelector<HTMLButtonElement>("[data-enable-confirm]")?.click();
|
|
462
|
+
await vi.waitFor(() =>
|
|
463
|
+
expect(el.querySelector("[data-multilingual-off]")).not.toBeNull(),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// The stamped count is the one thing the reloaded page cannot show.
|
|
467
|
+
expect(queueToastMock).toHaveBeenCalledWith(
|
|
468
|
+
"Multilingual content is on. 3 posts were marked as 简体中文.",
|
|
469
|
+
);
|
|
470
|
+
expect(showToastMock).not.toHaveBeenCalled();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("stays off when the dialog is cancelled", async () => {
|
|
474
|
+
const el = await createElement({ unmarkedPostCount: 3 });
|
|
475
|
+
const calls = mockFetch({});
|
|
476
|
+
|
|
477
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-setup]")?.click();
|
|
478
|
+
await el.updateComplete;
|
|
479
|
+
expect(el.querySelector("[data-enable-dialog]")).not.toBeNull();
|
|
480
|
+
|
|
481
|
+
findByText<HTMLButtonElement>(el, "button", labels.cancel)?.click();
|
|
482
|
+
await el.updateComplete;
|
|
483
|
+
|
|
484
|
+
// Nothing was saved: the page still offers turning it on.
|
|
485
|
+
expect(el.querySelector("[data-enable-dialog]")).toBeNull();
|
|
486
|
+
expect(el.querySelector("[data-multilingual-setup]")).not.toBeNull();
|
|
487
|
+
expect(el.textContent).not.toContain(labels.languagesLabel);
|
|
488
|
+
expect(calls).toHaveLength(0);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("offers turning it off once the change is saved", async () => {
|
|
492
|
+
const el = await createElement({ unmarkedPostCount: 3 });
|
|
493
|
+
mockFetch({ toast: "Multilingual content is on." });
|
|
494
|
+
|
|
495
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-setup]")?.click();
|
|
496
|
+
await el.updateComplete;
|
|
497
|
+
|
|
498
|
+
el.querySelector<HTMLButtonElement>(
|
|
499
|
+
'button[aria-labelledby="language-enable-add-label"]',
|
|
500
|
+
)?.click();
|
|
501
|
+
await el.updateComplete;
|
|
502
|
+
findByText<HTMLButtonElement>(el, '[role="option"]', "English")?.click();
|
|
503
|
+
await el.updateComplete;
|
|
504
|
+
el.querySelector<HTMLButtonElement>("[data-enable-confirm]")?.click();
|
|
505
|
+
await vi.waitFor(() =>
|
|
506
|
+
expect(el.querySelector("[data-multilingual-off]")).not.toBeNull(),
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
expect(el.querySelector("[data-multilingual-off]")).not.toBeNull();
|
|
510
|
+
expect(el.querySelector("[data-multilingual-setup]")).toBeNull();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("closes on Escape without saving", async () => {
|
|
514
|
+
const el = await createElement({ unmarkedPostCount: 3 });
|
|
515
|
+
const calls = mockFetch({});
|
|
516
|
+
|
|
517
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-setup]")?.click();
|
|
518
|
+
await el.updateComplete;
|
|
519
|
+
document.dispatchEvent(
|
|
520
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
521
|
+
);
|
|
522
|
+
await el.updateComplete;
|
|
523
|
+
|
|
524
|
+
expect(el.querySelector("[data-enable-dialog]")).toBeNull();
|
|
525
|
+
expect(calls).toHaveLength(0);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
describe("multilingual site", () => {
|
|
530
|
+
const enabled = {
|
|
531
|
+
multilingualEnabled: true,
|
|
532
|
+
additionalLanguages: ["en", "ja"],
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
it("lists every language with its reader URL, primary first", async () => {
|
|
536
|
+
const el = await createElement(enabled);
|
|
537
|
+
|
|
538
|
+
expect(el.textContent).toContain(labels.languagesLabel);
|
|
539
|
+
expect(el.textContent).toContain(labels.primaryBadge);
|
|
540
|
+
expect(el.textContent).not.toContain(labels.contentLanguageHelp);
|
|
541
|
+
|
|
542
|
+
const codes = Array.from(el.querySelectorAll("code")).map((node) =>
|
|
543
|
+
node.textContent?.trim(),
|
|
544
|
+
);
|
|
545
|
+
expect(codes).toEqual(["/", "/en", "/ja"]);
|
|
546
|
+
// Each address is a real link, opened away from the settings page.
|
|
547
|
+
const links = Array.from(
|
|
548
|
+
el.querySelectorAll<globalThis.HTMLAnchorElement>("li a"),
|
|
549
|
+
);
|
|
550
|
+
expect(links.map((link) => link.getAttribute("href"))).toEqual([
|
|
551
|
+
"/",
|
|
552
|
+
"/en",
|
|
553
|
+
"/ja",
|
|
554
|
+
]);
|
|
555
|
+
for (const link of links) {
|
|
556
|
+
expect(link.getAttribute("target")).toBe("_blank");
|
|
557
|
+
expect(link.getAttribute("rel")).toBe("noopener noreferrer");
|
|
558
|
+
}
|
|
559
|
+
// The primary row carries the badge; the others fold their rare
|
|
560
|
+
// actions behind a "⋯" menu instead of a row of buttons.
|
|
561
|
+
const primaryRow = findByText<globalThis.HTMLLIElement>(
|
|
562
|
+
el,
|
|
563
|
+
"li",
|
|
564
|
+
"简体中文",
|
|
565
|
+
);
|
|
566
|
+
expect(primaryRow?.textContent).toContain(labels.primaryBadge);
|
|
567
|
+
expect(primaryRow?.querySelector("[data-language-menu]")).toBeNull();
|
|
568
|
+
expect(el.querySelectorAll("[data-language-menu]").length).toBe(2);
|
|
569
|
+
expect(el.textContent).not.toContain(labels.makePrimary);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("dismisses a row menu on Escape", async () => {
|
|
573
|
+
const el = await createElement(enabled);
|
|
574
|
+
|
|
575
|
+
await openRowMenu(el, "English");
|
|
576
|
+
expect(el.querySelector('[role="menu"]')).not.toBeNull();
|
|
577
|
+
|
|
578
|
+
document.dispatchEvent(
|
|
579
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
580
|
+
);
|
|
581
|
+
await el.updateComplete;
|
|
582
|
+
|
|
583
|
+
expect(el.querySelector('[role="menu"]')).toBeNull();
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("honors the hosted site path prefix in those URLs", async () => {
|
|
587
|
+
const el = await createElement({ ...enabled, sitePathPrefix: "/blog" });
|
|
588
|
+
|
|
589
|
+
const codes = Array.from(el.querySelectorAll("code")).map((node) =>
|
|
590
|
+
node.textContent?.trim(),
|
|
591
|
+
);
|
|
592
|
+
expect(codes).toEqual(["/blog/", "/blog/en", "/blog/ja"]);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("adds a language", async () => {
|
|
596
|
+
const el = await createElement(enabled);
|
|
597
|
+
const calls = mockFetch({ toast: "Language added." });
|
|
598
|
+
|
|
599
|
+
el.querySelector<HTMLButtonElement>(
|
|
600
|
+
'[data-language-picker] button[aria-labelledby="language-list-label"]',
|
|
601
|
+
)?.click();
|
|
602
|
+
await el.updateComplete;
|
|
603
|
+
findByText<HTMLButtonElement>(el, '[role="option"]', "繁體中文")?.click();
|
|
604
|
+
await vi.waitFor(() => expect(calls.length).toBe(1));
|
|
605
|
+
await el.updateComplete;
|
|
606
|
+
|
|
607
|
+
expect(calls[0]?.url).toBe("/settings/language/add");
|
|
608
|
+
expect(calls[0]?.body).toEqual({ language: "zh-Hant" });
|
|
609
|
+
expect(el.textContent).toContain("繁體中文");
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("keeps a language on screen when the server refuses to remove it", async () => {
|
|
613
|
+
const el = await createElement(enabled);
|
|
614
|
+
const calls = mockFetch(
|
|
615
|
+
{ error: "2 posts are still written in this language." },
|
|
616
|
+
false,
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
await openRowMenu(el, "English");
|
|
620
|
+
findByText<HTMLButtonElement>(
|
|
621
|
+
el,
|
|
622
|
+
'[role="menuitem"]',
|
|
623
|
+
"Remove English",
|
|
624
|
+
)?.click();
|
|
625
|
+
await vi.waitFor(() => expect(calls.length).toBe(1));
|
|
626
|
+
await el.updateComplete;
|
|
627
|
+
|
|
628
|
+
expect(calls[0]?.url).toBe("/settings/language/remove");
|
|
629
|
+
expect(el.textContent).toContain("English");
|
|
630
|
+
|
|
631
|
+
// The refusal shows under that language's row, with a way to the posts.
|
|
632
|
+
await vi.waitFor(() =>
|
|
633
|
+
expect(el.querySelector('[role="alert"]')).not.toBeNull(),
|
|
634
|
+
);
|
|
635
|
+
const alert = el.querySelector('[role="alert"]');
|
|
636
|
+
expect(alert?.textContent).toContain(
|
|
637
|
+
"2 posts are still written in this language.",
|
|
638
|
+
);
|
|
639
|
+
const link = alert?.querySelector("a");
|
|
640
|
+
expect(link?.getAttribute("href")).toBe("/en/archive");
|
|
641
|
+
expect(link?.getAttribute("target")).toBe("_blank");
|
|
642
|
+
expect(link?.textContent).toContain(labels.viewPosts);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("drops a language the server accepted", async () => {
|
|
646
|
+
const el = await createElement(enabled);
|
|
647
|
+
const calls = mockFetch({ toast: "Language removed." });
|
|
648
|
+
|
|
649
|
+
await openRowMenu(el, "日本語");
|
|
650
|
+
findByText<HTMLButtonElement>(
|
|
651
|
+
el,
|
|
652
|
+
'[role="menuitem"]',
|
|
653
|
+
"Remove 日本語",
|
|
654
|
+
)?.click();
|
|
655
|
+
await vi.waitFor(() =>
|
|
656
|
+
expect(
|
|
657
|
+
el.querySelector('[aria-label="Options for 日本語"]'),
|
|
658
|
+
).toBeNull(),
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
expect(calls[0]?.url).toBe("/settings/language/remove");
|
|
662
|
+
expect(
|
|
663
|
+
el.querySelector('[aria-label="Options for English"]'),
|
|
664
|
+
).not.toBeNull();
|
|
665
|
+
const codes = Array.from(el.querySelectorAll("code")).map((node) =>
|
|
666
|
+
node.textContent?.trim(),
|
|
667
|
+
);
|
|
668
|
+
expect(codes).toEqual(["/", "/en"]);
|
|
669
|
+
// A language with others beside it is removed without ceremony.
|
|
670
|
+
expect(showConfirmDialogMock).not.toHaveBeenCalled();
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it("asks before removing the last other language", async () => {
|
|
674
|
+
const el = await createElement({
|
|
675
|
+
multilingualEnabled: true,
|
|
676
|
+
additionalLanguages: ["en"],
|
|
677
|
+
});
|
|
678
|
+
const calls = mockFetch({});
|
|
679
|
+
showConfirmDialogMock.mockResolvedValue(false);
|
|
680
|
+
|
|
681
|
+
await openRowMenu(el, "English");
|
|
682
|
+
findByText<HTMLButtonElement>(
|
|
683
|
+
el,
|
|
684
|
+
'[role="menuitem"]',
|
|
685
|
+
"Remove English",
|
|
686
|
+
)?.click();
|
|
687
|
+
await vi.waitFor(() => expect(showConfirmDialogMock).toHaveBeenCalled());
|
|
688
|
+
await el.updateComplete;
|
|
689
|
+
|
|
690
|
+
// Declined — the language stays and nothing is written.
|
|
691
|
+
expect(calls).toHaveLength(0);
|
|
692
|
+
expect(
|
|
693
|
+
el.querySelector('[aria-label="Options for English"]'),
|
|
694
|
+
).not.toBeNull();
|
|
695
|
+
|
|
696
|
+
const message = showConfirmDialogMock.mock.calls[0]?.[0]
|
|
697
|
+
?.message as string;
|
|
698
|
+
expect(message).toContain("English");
|
|
699
|
+
expect(message).toContain("/en");
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("turns multilingual off with the last other language", async () => {
|
|
703
|
+
const el = await createElement({
|
|
704
|
+
multilingualEnabled: true,
|
|
705
|
+
additionalLanguages: ["en"],
|
|
706
|
+
});
|
|
707
|
+
const calls = mockFetch({
|
|
708
|
+
toast: "Language removed. Multilingual content is off.",
|
|
709
|
+
});
|
|
710
|
+
showConfirmDialogMock.mockResolvedValue(true);
|
|
711
|
+
|
|
712
|
+
await openRowMenu(el, "English");
|
|
713
|
+
findByText<HTMLButtonElement>(
|
|
714
|
+
el,
|
|
715
|
+
'[role="menuitem"]',
|
|
716
|
+
"Remove English",
|
|
717
|
+
)?.click();
|
|
718
|
+
await vi.waitFor(() => expect(calls.length).toBe(1));
|
|
719
|
+
await el.updateComplete;
|
|
720
|
+
|
|
721
|
+
expect(calls[0]?.url).toBe("/settings/language/remove");
|
|
722
|
+
expect(calls[0]?.body).toEqual({ language: "en" });
|
|
723
|
+
// The page is a single-language page again, without a refresh.
|
|
724
|
+
expect(el.querySelector("[data-language-menu]")).toBeNull();
|
|
725
|
+
expect(el.textContent).toContain(labels.contentLanguageHelp);
|
|
726
|
+
expect(el.querySelector("[data-multilingual-setup]")).not.toBeNull();
|
|
727
|
+
// The confirmation has to survive the reload that drops the switcher.
|
|
728
|
+
expect(queueToastMock).toHaveBeenCalledWith(
|
|
729
|
+
"Language removed. Multilingual content is off.",
|
|
730
|
+
);
|
|
731
|
+
expect(showToastMock).not.toHaveBeenCalled();
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it("confirms before moving the root URLs to another language", async () => {
|
|
735
|
+
const el = await createElement(enabled);
|
|
736
|
+
const calls = mockFetch({ toast: "Primary language changed." });
|
|
737
|
+
const confirmSpy = vi.fn().mockResolvedValue(false);
|
|
738
|
+
showConfirmDialogMock.mockImplementation(confirmSpy);
|
|
739
|
+
|
|
740
|
+
await openRowMenu(el, "English");
|
|
741
|
+
findByText<HTMLButtonElement>(
|
|
742
|
+
el,
|
|
743
|
+
'[role="menuitem"]',
|
|
744
|
+
labels.makePrimary,
|
|
745
|
+
)?.click();
|
|
746
|
+
await vi.waitFor(() => expect(confirmSpy).toHaveBeenCalled());
|
|
747
|
+
|
|
748
|
+
// Declined — nothing is written.
|
|
749
|
+
expect(calls).toHaveLength(0);
|
|
750
|
+
const message = confirmSpy.mock.calls[0]?.[0]?.message as string;
|
|
751
|
+
expect(message).toContain("English");
|
|
752
|
+
expect(message).toContain("简体中文");
|
|
753
|
+
expect(message).toContain("/zh-hans");
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it("swaps the two lists once the change is confirmed", async () => {
|
|
757
|
+
const el = await createElement(enabled);
|
|
758
|
+
const calls = mockFetch({ toast: "Primary language changed." });
|
|
759
|
+
showConfirmDialogMock.mockResolvedValue(true);
|
|
760
|
+
|
|
761
|
+
await openRowMenu(el, "English");
|
|
762
|
+
findByText<HTMLButtonElement>(
|
|
763
|
+
el,
|
|
764
|
+
'[role="menuitem"]',
|
|
765
|
+
labels.makePrimary,
|
|
766
|
+
)?.click();
|
|
767
|
+
await vi.waitFor(() => expect(calls.length).toBe(1));
|
|
768
|
+
await el.updateComplete;
|
|
769
|
+
|
|
770
|
+
expect(calls[0]?.url).toBe("/settings/language/primary");
|
|
771
|
+
expect(calls[0]?.body).toEqual({ language: "en" });
|
|
772
|
+
const codes = Array.from(el.querySelectorAll("code")).map((node) =>
|
|
773
|
+
node.textContent?.trim(),
|
|
774
|
+
);
|
|
775
|
+
// English takes the root; Simplified Chinese picks up a prefix.
|
|
776
|
+
expect(codes).toEqual(["/", "/ja", "/zh-hans"]);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it("keeps the views when turning it off is declined", async () => {
|
|
780
|
+
const el = await createElement(enabled);
|
|
781
|
+
const calls = mockFetch({});
|
|
782
|
+
showConfirmDialogMock.mockResolvedValue(false);
|
|
783
|
+
|
|
784
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-off]")?.click();
|
|
785
|
+
await vi.waitFor(() => expect(showConfirmDialogMock).toHaveBeenCalled());
|
|
786
|
+
await el.updateComplete;
|
|
787
|
+
|
|
788
|
+
expect(calls).toHaveLength(0);
|
|
789
|
+
expect(el.querySelector("[data-language-menu]")).not.toBeNull();
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it("confirms before turning multilingual off", async () => {
|
|
793
|
+
const el = await createElement(enabled);
|
|
794
|
+
const calls = mockFetch({ toast: "Multilingual content is off." });
|
|
795
|
+
showConfirmDialogMock.mockResolvedValue(true);
|
|
796
|
+
|
|
797
|
+
el.querySelector<HTMLButtonElement>("[data-multilingual-off]")?.click();
|
|
798
|
+
await vi.waitFor(() => expect(calls.length).toBe(1));
|
|
799
|
+
await el.updateComplete;
|
|
800
|
+
|
|
801
|
+
expect(calls[0]?.url).toBe("/settings/language/disable");
|
|
802
|
+
// The languages stay configured; only the views stop.
|
|
803
|
+
expect(el.querySelector("[data-language-menu]")).toBeNull();
|
|
804
|
+
expect(el.textContent).toContain(labels.contentLanguageHelp);
|
|
805
|
+
// Reloading is what removes the header switcher, so the confirmation
|
|
806
|
+
// has to survive it.
|
|
807
|
+
expect(queueToastMock).toHaveBeenCalledWith(
|
|
808
|
+
"Multilingual content is off.",
|
|
809
|
+
);
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
describe("dashboard language", () => {
|
|
814
|
+
it("posts only the dashboard field", async () => {
|
|
815
|
+
const el = await createElement();
|
|
816
|
+
const calls = mockFetch({ toast: "Language updated." });
|
|
817
|
+
|
|
818
|
+
const select = el.querySelector(
|
|
819
|
+
"select",
|
|
820
|
+
) as globalThis.HTMLSelectElement | null;
|
|
821
|
+
if (select) {
|
|
822
|
+
select.value = "zh-Hant";
|
|
823
|
+
select.dispatchEvent(new Event("change", { bubbles: true }));
|
|
824
|
+
}
|
|
825
|
+
await vi.waitFor(() => expect(calls.length).toBe(1));
|
|
826
|
+
|
|
827
|
+
// Only the dashboard field: the content language is a separate control
|
|
828
|
+
// on the same page, and sending it here would overwrite it.
|
|
829
|
+
expect(calls[0]?.url).toBe("/settings/language");
|
|
830
|
+
expect(calls[0]?.body).toEqual({ dashboardLanguage: "zh-Hant" });
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it("offers following the content language", async () => {
|
|
834
|
+
const el = await createElement();
|
|
835
|
+
const options = Array.from(
|
|
836
|
+
el.querySelectorAll(
|
|
837
|
+
"select option",
|
|
838
|
+
) as unknown as globalThis.HTMLOptionElement[],
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
expect(options[0]?.value).toBe("");
|
|
842
|
+
expect(options[0]?.textContent).toContain(labels.followContent);
|
|
843
|
+
expect(options.map((option) => option.value)).toEqual([
|
|
844
|
+
"",
|
|
845
|
+
"en",
|
|
846
|
+
"zh-Hans",
|
|
847
|
+
"zh-Hant",
|
|
848
|
+
]);
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
});
|