create-nextblock 0.16.1 → 0.16.2
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/docker-template/.dockerignore +1 -1
- package/libs/db/tsconfig.lib.json +3 -3
- package/libs/editor/tsconfig.lib.json +3 -3
- package/libs/ui/tsconfig.lib.json +3 -3
- package/libs/utils/tsconfig.json +3 -3
- package/package.json +50 -50
- package/project.json +19 -19
- package/templates/nextblock-template/.dockerignore +1 -1
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/AGENTS.md +9 -9
- package/templates/nextblock-template/CLAUDE.md +1 -1
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
- package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
- package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
- package/templates/nextblock-template/app/actions/email.ts +241 -241
- package/templates/nextblock-template/app/actions/formActions.ts +245 -245
- package/templates/nextblock-template/app/actions/interactions.ts +489 -489
- package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
- package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
- package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -221
- package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -186
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -3092
- package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
- package/templates/nextblock-template/app/api/search/route.ts +43 -43
- package/templates/nextblock-template/app/api/view/route.ts +114 -114
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
- package/templates/nextblock-template/app/cart/page.tsx +7 -7
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/checkout/page.tsx +57 -57
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -558
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +406 -406
- package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
- package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/layout.tsx +101 -101
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
- package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
- package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
- package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
- package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +649 -649
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +618 -618
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
- package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
- package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
- package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
- package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
- package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
- package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
- package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
- package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
- package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
- package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
- package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
- package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
- package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
- package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
- package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
- package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/layout.tsx +671 -671
- package/templates/nextblock-template/app/lib/seo.ts +319 -319
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
- package/templates/nextblock-template/app/providers.tsx +96 -96
- package/templates/nextblock-template/app/robots.ts +123 -123
- package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
- package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
- package/templates/nextblock-template/app/thread/layout.tsx +15 -15
- package/templates/nextblock-template/app/thread/page.tsx +98 -98
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
- package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
- package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -32
- package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -137
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -244
- package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
- package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
- package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
- package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
- package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-icon.tsx +78 -78
- package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
- package/templates/nextblock-template/docs/README.md +42 -42
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
- package/templates/nextblock-template/index.d.ts +7 -7
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
- package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
- package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
- package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
- package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
- package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
- package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
- package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
- package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -86
- package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
- package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
- package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
- package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
- package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
- package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
- package/templates/nextblock-template/lib/messages/threads.ts +579 -579
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -154
- package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
- package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
- package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
- package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -140
- package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -350
- package/templates/nextblock-template/lib/seo/page-document.ts +412 -412
- package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
- package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
- package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
- package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
- package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
- package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -177
- package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
- package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
- package/templates/nextblock-template/lib/site-url.test.ts +89 -89
- package/templates/nextblock-template/lib/site-url.ts +102 -102
- package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
- package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
- package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +1 -2
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
- package/templates/nextblock-template/tools/update.mjs +1303 -1303
- package/tsconfig.base.json +3 -3
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import type { Database } from '@nextblock-cms/db';
|
|
2
|
-
|
|
3
|
-
type BlockRow = Database['public']['Tables']['blocks']['Row'];
|
|
4
|
-
|
|
5
|
-
const WORDS_PER_MINUTE = 200;
|
|
6
|
-
|
|
7
|
-
function extractPlainTextFromHtml(html: string) {
|
|
8
|
-
return html
|
|
9
|
-
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
10
|
-
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
11
|
-
.replace(/<[^>]+>/g, ' ')
|
|
12
|
-
.replace(/ /g, ' ')
|
|
13
|
-
.replace(/&/g, '&')
|
|
14
|
-
.replace(/\s+/g, ' ')
|
|
15
|
-
.trim();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function countWords(text: string) {
|
|
19
|
-
if (!text) {
|
|
20
|
-
return 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return text.split(' ').length;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function estimateReadTimeMinutesFromHtmlFragments(htmlFragments: string[]) {
|
|
27
|
-
const words = htmlFragments.reduce((total, html) => {
|
|
28
|
-
const plainText = extractPlainTextFromHtml(html);
|
|
29
|
-
return total + countWords(plainText);
|
|
30
|
-
}, 0);
|
|
31
|
-
|
|
32
|
-
return Math.max(1, Math.round(words / WORDS_PER_MINUTE));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function estimateReadTimeMinutesFromBlocks(
|
|
36
|
-
blocks: Array<Pick<BlockRow, 'block_type' | 'content'>> | undefined
|
|
37
|
-
) {
|
|
38
|
-
if (!blocks || blocks.length === 0) {
|
|
39
|
-
return 1;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const htmlFragments = blocks.reduce<string[]>((fragments, block) => {
|
|
43
|
-
if (block.block_type !== 'text') {
|
|
44
|
-
return fragments;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const html =
|
|
48
|
-
typeof block.content === 'object' && block.content && 'html_content' in block.content
|
|
49
|
-
? String((block.content as { html_content?: string }).html_content ?? '')
|
|
50
|
-
: '';
|
|
51
|
-
|
|
52
|
-
if (html) {
|
|
53
|
-
fragments.push(html);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return fragments;
|
|
57
|
-
}, []);
|
|
58
|
-
|
|
59
|
-
return estimateReadTimeMinutesFromHtmlFragments(htmlFragments);
|
|
60
|
-
}
|
|
1
|
+
import type { Database } from '@nextblock-cms/db';
|
|
2
|
+
|
|
3
|
+
type BlockRow = Database['public']['Tables']['blocks']['Row'];
|
|
4
|
+
|
|
5
|
+
const WORDS_PER_MINUTE = 200;
|
|
6
|
+
|
|
7
|
+
function extractPlainTextFromHtml(html: string) {
|
|
8
|
+
return html
|
|
9
|
+
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
10
|
+
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
11
|
+
.replace(/<[^>]+>/g, ' ')
|
|
12
|
+
.replace(/ /g, ' ')
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/\s+/g, ' ')
|
|
15
|
+
.trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function countWords(text: string) {
|
|
19
|
+
if (!text) {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return text.split(' ').length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function estimateReadTimeMinutesFromHtmlFragments(htmlFragments: string[]) {
|
|
27
|
+
const words = htmlFragments.reduce((total, html) => {
|
|
28
|
+
const plainText = extractPlainTextFromHtml(html);
|
|
29
|
+
return total + countWords(plainText);
|
|
30
|
+
}, 0);
|
|
31
|
+
|
|
32
|
+
return Math.max(1, Math.round(words / WORDS_PER_MINUTE));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function estimateReadTimeMinutesFromBlocks(
|
|
36
|
+
blocks: Array<Pick<BlockRow, 'block_type' | 'content'>> | undefined
|
|
37
|
+
) {
|
|
38
|
+
if (!blocks || blocks.length === 0) {
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const htmlFragments = blocks.reduce<string[]>((fragments, block) => {
|
|
43
|
+
if (block.block_type !== 'text') {
|
|
44
|
+
return fragments;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const html =
|
|
48
|
+
typeof block.content === 'object' && block.content && 'html_content' in block.content
|
|
49
|
+
? String((block.content as { html_content?: string }).html_content ?? '')
|
|
50
|
+
: '';
|
|
51
|
+
|
|
52
|
+
if (html) {
|
|
53
|
+
fragments.push(html);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return fragments;
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
return estimateReadTimeMinutesFromHtmlFragments(htmlFragments);
|
|
60
|
+
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build a link to the `/api/view` entry point used by the CMS "Preview" and
|
|
3
|
-
* "View Live" buttons.
|
|
4
|
-
*
|
|
5
|
-
* Always pass the content's own language. The public site resolves language from
|
|
6
|
-
* a cookie, not the URL, so a link without `lang` renders in whatever language
|
|
7
|
-
* the editor happens to be browsing in — which is how opening a French page
|
|
8
|
-
* landed you on the English one. See `app/api/view/route.ts`.
|
|
9
|
-
*/
|
|
10
|
-
export function buildViewUrl(options: {
|
|
11
|
-
/** Root-relative public path, e.g. "/about" or "/article/hello". */
|
|
12
|
-
path: string;
|
|
13
|
-
/** The content row's language code, e.g. "fr". */
|
|
14
|
-
languageCode?: string | null;
|
|
15
|
-
/** True to enter Live Draft Mode (preview unpublished content). */
|
|
16
|
-
draft?: boolean;
|
|
17
|
-
}): string {
|
|
18
|
-
const params = new URLSearchParams({ path: options.path });
|
|
19
|
-
if (options.languageCode) {
|
|
20
|
-
params.set("lang", options.languageCode);
|
|
21
|
-
}
|
|
22
|
-
if (options.draft) {
|
|
23
|
-
params.set("draft", "1");
|
|
24
|
-
}
|
|
25
|
-
return `/api/view?${params.toString()}`;
|
|
26
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Build a link to the `/api/view` entry point used by the CMS "Preview" and
|
|
3
|
+
* "View Live" buttons.
|
|
4
|
+
*
|
|
5
|
+
* Always pass the content's own language. The public site resolves language from
|
|
6
|
+
* a cookie, not the URL, so a link without `lang` renders in whatever language
|
|
7
|
+
* the editor happens to be browsing in — which is how opening a French page
|
|
8
|
+
* landed you on the English one. See `app/api/view/route.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export function buildViewUrl(options: {
|
|
11
|
+
/** Root-relative public path, e.g. "/about" or "/article/hello". */
|
|
12
|
+
path: string;
|
|
13
|
+
/** The content row's language code, e.g. "fr". */
|
|
14
|
+
languageCode?: string | null;
|
|
15
|
+
/** True to enter Live Draft Mode (preview unpublished content). */
|
|
16
|
+
draft?: boolean;
|
|
17
|
+
}): string {
|
|
18
|
+
const params = new URLSearchParams({ path: options.path });
|
|
19
|
+
if (options.languageCode) {
|
|
20
|
+
params.set("lang", options.languageCode);
|
|
21
|
+
}
|
|
22
|
+
if (options.draft) {
|
|
23
|
+
params.set("draft", "1");
|
|
24
|
+
}
|
|
25
|
+
return `/api/view?${params.toString()}`;
|
|
26
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
export type GlobalSearchResultType = 'page' | 'post' | 'product';
|
|
2
|
-
|
|
3
|
-
export type GlobalSearchFilter = GlobalSearchResultType | 'all';
|
|
4
|
-
|
|
5
|
-
export interface GlobalSearchResult {
|
|
6
|
-
id: string;
|
|
7
|
-
type: GlobalSearchResultType;
|
|
8
|
-
title: string;
|
|
9
|
-
description: string | null;
|
|
10
|
-
snippet: string | null;
|
|
11
|
-
href: string;
|
|
12
|
-
locale: string | null;
|
|
13
|
-
imageUrl: string | null;
|
|
14
|
-
score: number;
|
|
15
|
-
meta: {
|
|
16
|
-
label?: string | null;
|
|
17
|
-
sku?: string | null;
|
|
18
|
-
publishedAt?: string | null;
|
|
19
|
-
updatedAt?: string | null;
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface GlobalSearchResponse {
|
|
24
|
-
query: string;
|
|
25
|
-
results: GlobalSearchResult[];
|
|
26
|
-
counts: Record<GlobalSearchResultType | 'all', number>;
|
|
27
|
-
}
|
|
1
|
+
export type GlobalSearchResultType = 'page' | 'post' | 'product';
|
|
2
|
+
|
|
3
|
+
export type GlobalSearchFilter = GlobalSearchResultType | 'all';
|
|
4
|
+
|
|
5
|
+
export interface GlobalSearchResult {
|
|
6
|
+
id: string;
|
|
7
|
+
type: GlobalSearchResultType;
|
|
8
|
+
title: string;
|
|
9
|
+
description: string | null;
|
|
10
|
+
snippet: string | null;
|
|
11
|
+
href: string;
|
|
12
|
+
locale: string | null;
|
|
13
|
+
imageUrl: string | null;
|
|
14
|
+
score: number;
|
|
15
|
+
meta: {
|
|
16
|
+
label?: string | null;
|
|
17
|
+
sku?: string | null;
|
|
18
|
+
publishedAt?: string | null;
|
|
19
|
+
updatedAt?: string | null;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface GlobalSearchResponse {
|
|
24
|
+
query: string;
|
|
25
|
+
results: GlobalSearchResult[];
|
|
26
|
+
counts: Record<GlobalSearchResultType | 'all', number>;
|
|
27
|
+
}
|
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
altTextImageIdentity,
|
|
5
|
-
resolveAltTextWriteBack,
|
|
6
|
-
STALE_ALT_TEXT_MESSAGE,
|
|
7
|
-
} from './alt-text-write-back';
|
|
8
|
-
|
|
9
|
-
type ImageContent = {
|
|
10
|
-
alt_text: string | null;
|
|
11
|
-
caption: string | null;
|
|
12
|
-
external_url?: string | null;
|
|
13
|
-
media_id: string | null;
|
|
14
|
-
object_key: string | null;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
function content(overrides: Partial<ImageContent> = {}): ImageContent {
|
|
18
|
-
return {
|
|
19
|
-
alt_text: '',
|
|
20
|
-
caption: '',
|
|
21
|
-
external_url: null,
|
|
22
|
-
media_id: 'media-1',
|
|
23
|
-
object_key: 'uploads/harvest.jpg',
|
|
24
|
-
...overrides,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe('altTextImageIdentity', () => {
|
|
29
|
-
it('identifies a stored image by its object key', () => {
|
|
30
|
-
expect(altTextImageIdentity(content())).toBe('stored:uploads/harvest.jpg');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('falls back to the editor selection while content has not echoed it back yet', () => {
|
|
34
|
-
expect(altTextImageIdentity(content({ object_key: null }), 'uploads/just-picked.jpg')).toBe(
|
|
35
|
-
'stored:uploads/just-picked.jpg'
|
|
36
|
-
);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('prefers the external URL, because that is the image the editor renders and sends', () => {
|
|
40
|
-
expect(
|
|
41
|
-
altTextImageIdentity(
|
|
42
|
-
content({ external_url: 'https://images.example.com/a.jpg', object_key: 'uploads/old.jpg' }),
|
|
43
|
-
'uploads/older.jpg'
|
|
44
|
-
)
|
|
45
|
-
).toBe('external:https://images.example.com/a.jpg');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('returns an empty identity when there is no image at all', () => {
|
|
49
|
-
expect(altTextImageIdentity(content({ media_id: null, object_key: null }))).toBe('');
|
|
50
|
-
expect(altTextImageIdentity(content({ object_key: ' ' }))).toBe('');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('never confuses an external URL with an object key of the same text', () => {
|
|
54
|
-
expect(altTextImageIdentity({ external_url: 'x', object_key: null })).not.toBe(
|
|
55
|
-
altTextImageIdentity({ external_url: null, object_key: 'x' })
|
|
56
|
-
);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('resolveAltTextWriteBack', () => {
|
|
61
|
-
it('writes the description into the CURRENT content, not the copy captured at click time', () => {
|
|
62
|
-
// The caption was typed while the vision call was in flight. Merging into the captured
|
|
63
|
-
// copy would silently revert it.
|
|
64
|
-
const current = content({ caption: 'Typed while generating' });
|
|
65
|
-
|
|
66
|
-
expect(
|
|
67
|
-
resolveAltTextWriteBack({
|
|
68
|
-
capturedIdentity: 'stored:uploads/harvest.jpg',
|
|
69
|
-
currentContent: current,
|
|
70
|
-
currentIdentity: 'stored:uploads/harvest.jpg',
|
|
71
|
-
generatedAltText: 'A field of chamomile at dawn.',
|
|
72
|
-
objectKey: 'uploads/harvest.jpg',
|
|
73
|
-
})
|
|
74
|
-
).toEqual({
|
|
75
|
-
alt_text: 'A field of chamomile at dawn.',
|
|
76
|
-
caption: 'Typed while generating',
|
|
77
|
-
external_url: null,
|
|
78
|
-
media_id: 'media-1',
|
|
79
|
-
object_key: 'uploads/harvest.jpg',
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('writes object_key from the editor selection, matching the typed-alt-text shape exactly', () => {
|
|
84
|
-
const result = resolveAltTextWriteBack({
|
|
85
|
-
capturedIdentity: 'stored:uploads/just-picked.jpg',
|
|
86
|
-
currentContent: content({ object_key: null }),
|
|
87
|
-
currentIdentity: 'stored:uploads/just-picked.jpg',
|
|
88
|
-
generatedAltText: 'Chamomile.',
|
|
89
|
-
objectKey: 'uploads/just-picked.jpg',
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
expect(result?.object_key).toBe('uploads/just-picked.jpg');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('drops the response when the image was replaced mid-flight', () => {
|
|
96
|
-
// The regression this whole module exists for: alt text describing image A must never
|
|
97
|
-
// be attached to image B.
|
|
98
|
-
expect(
|
|
99
|
-
resolveAltTextWriteBack({
|
|
100
|
-
capturedIdentity: 'stored:uploads/harvest.jpg',
|
|
101
|
-
currentContent: content({ object_key: 'uploads/packaging.jpg' }),
|
|
102
|
-
currentIdentity: 'stored:uploads/packaging.jpg',
|
|
103
|
-
generatedAltText: 'A field of chamomile at dawn.',
|
|
104
|
-
objectKey: 'uploads/packaging.jpg',
|
|
105
|
-
})
|
|
106
|
-
).toBeNull();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('drops the response when the image was removed mid-flight, so nothing is resurrected', () => {
|
|
110
|
-
expect(
|
|
111
|
-
resolveAltTextWriteBack({
|
|
112
|
-
capturedIdentity: 'stored:uploads/harvest.jpg',
|
|
113
|
-
currentContent: content({ media_id: null, object_key: null }),
|
|
114
|
-
currentIdentity: '',
|
|
115
|
-
generatedAltText: 'A field of chamomile at dawn.',
|
|
116
|
-
objectKey: null,
|
|
117
|
-
})
|
|
118
|
-
).toBeNull();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('drops the response when a stored image was swapped for an external URL', () => {
|
|
122
|
-
const current = content({
|
|
123
|
-
external_url: 'https://images.example.com/new.jpg',
|
|
124
|
-
media_id: null,
|
|
125
|
-
object_key: null,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
expect(
|
|
129
|
-
resolveAltTextWriteBack({
|
|
130
|
-
capturedIdentity: 'stored:uploads/harvest.jpg',
|
|
131
|
-
currentContent: current,
|
|
132
|
-
currentIdentity: altTextImageIdentity(current),
|
|
133
|
-
generatedAltText: 'A field of chamomile at dawn.',
|
|
134
|
-
objectKey: null,
|
|
135
|
-
})
|
|
136
|
-
).toBeNull();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('refuses to treat two empty identities as a match', () => {
|
|
140
|
-
expect(
|
|
141
|
-
resolveAltTextWriteBack({
|
|
142
|
-
capturedIdentity: '',
|
|
143
|
-
currentContent: content({ media_id: null, object_key: null }),
|
|
144
|
-
currentIdentity: '',
|
|
145
|
-
generatedAltText: 'A field of chamomile at dawn.',
|
|
146
|
-
objectKey: null,
|
|
147
|
-
})
|
|
148
|
-
).toBeNull();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('offers an explanation that tells the operator the description was discarded', () => {
|
|
152
|
-
expect(STALE_ALT_TEXT_MESSAGE).toContain('discarded');
|
|
153
|
-
});
|
|
154
|
-
});
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
altTextImageIdentity,
|
|
5
|
+
resolveAltTextWriteBack,
|
|
6
|
+
STALE_ALT_TEXT_MESSAGE,
|
|
7
|
+
} from './alt-text-write-back';
|
|
8
|
+
|
|
9
|
+
type ImageContent = {
|
|
10
|
+
alt_text: string | null;
|
|
11
|
+
caption: string | null;
|
|
12
|
+
external_url?: string | null;
|
|
13
|
+
media_id: string | null;
|
|
14
|
+
object_key: string | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function content(overrides: Partial<ImageContent> = {}): ImageContent {
|
|
18
|
+
return {
|
|
19
|
+
alt_text: '',
|
|
20
|
+
caption: '',
|
|
21
|
+
external_url: null,
|
|
22
|
+
media_id: 'media-1',
|
|
23
|
+
object_key: 'uploads/harvest.jpg',
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('altTextImageIdentity', () => {
|
|
29
|
+
it('identifies a stored image by its object key', () => {
|
|
30
|
+
expect(altTextImageIdentity(content())).toBe('stored:uploads/harvest.jpg');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('falls back to the editor selection while content has not echoed it back yet', () => {
|
|
34
|
+
expect(altTextImageIdentity(content({ object_key: null }), 'uploads/just-picked.jpg')).toBe(
|
|
35
|
+
'stored:uploads/just-picked.jpg'
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('prefers the external URL, because that is the image the editor renders and sends', () => {
|
|
40
|
+
expect(
|
|
41
|
+
altTextImageIdentity(
|
|
42
|
+
content({ external_url: 'https://images.example.com/a.jpg', object_key: 'uploads/old.jpg' }),
|
|
43
|
+
'uploads/older.jpg'
|
|
44
|
+
)
|
|
45
|
+
).toBe('external:https://images.example.com/a.jpg');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns an empty identity when there is no image at all', () => {
|
|
49
|
+
expect(altTextImageIdentity(content({ media_id: null, object_key: null }))).toBe('');
|
|
50
|
+
expect(altTextImageIdentity(content({ object_key: ' ' }))).toBe('');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('never confuses an external URL with an object key of the same text', () => {
|
|
54
|
+
expect(altTextImageIdentity({ external_url: 'x', object_key: null })).not.toBe(
|
|
55
|
+
altTextImageIdentity({ external_url: null, object_key: 'x' })
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('resolveAltTextWriteBack', () => {
|
|
61
|
+
it('writes the description into the CURRENT content, not the copy captured at click time', () => {
|
|
62
|
+
// The caption was typed while the vision call was in flight. Merging into the captured
|
|
63
|
+
// copy would silently revert it.
|
|
64
|
+
const current = content({ caption: 'Typed while generating' });
|
|
65
|
+
|
|
66
|
+
expect(
|
|
67
|
+
resolveAltTextWriteBack({
|
|
68
|
+
capturedIdentity: 'stored:uploads/harvest.jpg',
|
|
69
|
+
currentContent: current,
|
|
70
|
+
currentIdentity: 'stored:uploads/harvest.jpg',
|
|
71
|
+
generatedAltText: 'A field of chamomile at dawn.',
|
|
72
|
+
objectKey: 'uploads/harvest.jpg',
|
|
73
|
+
})
|
|
74
|
+
).toEqual({
|
|
75
|
+
alt_text: 'A field of chamomile at dawn.',
|
|
76
|
+
caption: 'Typed while generating',
|
|
77
|
+
external_url: null,
|
|
78
|
+
media_id: 'media-1',
|
|
79
|
+
object_key: 'uploads/harvest.jpg',
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('writes object_key from the editor selection, matching the typed-alt-text shape exactly', () => {
|
|
84
|
+
const result = resolveAltTextWriteBack({
|
|
85
|
+
capturedIdentity: 'stored:uploads/just-picked.jpg',
|
|
86
|
+
currentContent: content({ object_key: null }),
|
|
87
|
+
currentIdentity: 'stored:uploads/just-picked.jpg',
|
|
88
|
+
generatedAltText: 'Chamomile.',
|
|
89
|
+
objectKey: 'uploads/just-picked.jpg',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result?.object_key).toBe('uploads/just-picked.jpg');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('drops the response when the image was replaced mid-flight', () => {
|
|
96
|
+
// The regression this whole module exists for: alt text describing image A must never
|
|
97
|
+
// be attached to image B.
|
|
98
|
+
expect(
|
|
99
|
+
resolveAltTextWriteBack({
|
|
100
|
+
capturedIdentity: 'stored:uploads/harvest.jpg',
|
|
101
|
+
currentContent: content({ object_key: 'uploads/packaging.jpg' }),
|
|
102
|
+
currentIdentity: 'stored:uploads/packaging.jpg',
|
|
103
|
+
generatedAltText: 'A field of chamomile at dawn.',
|
|
104
|
+
objectKey: 'uploads/packaging.jpg',
|
|
105
|
+
})
|
|
106
|
+
).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('drops the response when the image was removed mid-flight, so nothing is resurrected', () => {
|
|
110
|
+
expect(
|
|
111
|
+
resolveAltTextWriteBack({
|
|
112
|
+
capturedIdentity: 'stored:uploads/harvest.jpg',
|
|
113
|
+
currentContent: content({ media_id: null, object_key: null }),
|
|
114
|
+
currentIdentity: '',
|
|
115
|
+
generatedAltText: 'A field of chamomile at dawn.',
|
|
116
|
+
objectKey: null,
|
|
117
|
+
})
|
|
118
|
+
).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('drops the response when a stored image was swapped for an external URL', () => {
|
|
122
|
+
const current = content({
|
|
123
|
+
external_url: 'https://images.example.com/new.jpg',
|
|
124
|
+
media_id: null,
|
|
125
|
+
object_key: null,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(
|
|
129
|
+
resolveAltTextWriteBack({
|
|
130
|
+
capturedIdentity: 'stored:uploads/harvest.jpg',
|
|
131
|
+
currentContent: current,
|
|
132
|
+
currentIdentity: altTextImageIdentity(current),
|
|
133
|
+
generatedAltText: 'A field of chamomile at dawn.',
|
|
134
|
+
objectKey: null,
|
|
135
|
+
})
|
|
136
|
+
).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('refuses to treat two empty identities as a match', () => {
|
|
140
|
+
expect(
|
|
141
|
+
resolveAltTextWriteBack({
|
|
142
|
+
capturedIdentity: '',
|
|
143
|
+
currentContent: content({ media_id: null, object_key: null }),
|
|
144
|
+
currentIdentity: '',
|
|
145
|
+
generatedAltText: 'A field of chamomile at dawn.',
|
|
146
|
+
objectKey: null,
|
|
147
|
+
})
|
|
148
|
+
).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('offers an explanation that tells the operator the description was discarded', () => {
|
|
152
|
+
expect(STALE_ALT_TEXT_MESSAGE).toContain('discarded');
|
|
153
|
+
});
|
|
154
|
+
});
|