@prenta/admin 0.92.0 → 0.94.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/CHANGELOG.md +663 -0
- package/design-system/tokens.css +173 -71
- package/design-system/tokens.json +244 -63
- package/design-system/tokens.scss +203 -98
- package/design-system/tokens.tailwind.cjs +93 -38
- package/dist/AdminRoot.d.ts +27 -6
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/breakpoint-controls.render.test.d.ts +2 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.js +51 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.js.map +1 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts +2 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.js +37 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.js.map +1 -0
- package/dist/__tests__/components/editor-rail.render.test.d.ts +2 -0
- package/dist/__tests__/components/editor-rail.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/editor-rail.render.test.js +138 -0
- package/dist/__tests__/components/editor-rail.render.test.js.map +1 -0
- package/dist/__tests__/components/media-block.render.test.d.ts +2 -0
- package/dist/__tests__/components/media-block.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/media-block.render.test.js +108 -0
- package/dist/__tests__/components/media-block.render.test.js.map +1 -0
- package/dist/__tests__/components/page-audit-panel.render.test.d.ts +2 -0
- package/dist/__tests__/components/page-audit-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/page-audit-panel.render.test.js +59 -0
- package/dist/__tests__/components/page-audit-panel.render.test.js.map +1 -0
- package/dist/__tests__/components/reflow-meter.render.test.d.ts +2 -0
- package/dist/__tests__/components/reflow-meter.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/reflow-meter.render.test.js +94 -0
- package/dist/__tests__/components/reflow-meter.render.test.js.map +1 -0
- package/dist/__tests__/components/seo-char-count.test.js +24 -6
- package/dist/__tests__/components/seo-char-count.test.js.map +1 -1
- package/dist/__tests__/components/serp-preview.render.test.d.ts +2 -0
- package/dist/__tests__/components/serp-preview.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/serp-preview.render.test.js +25 -0
- package/dist/__tests__/components/serp-preview.render.test.js.map +1 -0
- package/dist/__tests__/lib/breakpoints.test.d.ts +2 -0
- package/dist/__tests__/lib/breakpoints.test.d.ts.map +1 -0
- package/dist/__tests__/lib/breakpoints.test.js +67 -0
- package/dist/__tests__/lib/breakpoints.test.js.map +1 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts +2 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts.map +1 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.js +44 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.js.map +1 -0
- package/dist/__tests__/lib/media-format.test.d.ts +2 -0
- package/dist/__tests__/lib/media-format.test.d.ts.map +1 -0
- package/dist/__tests__/lib/media-format.test.js +70 -0
- package/dist/__tests__/lib/media-format.test.js.map +1 -0
- package/dist/__tests__/lib/page-audit-input.test.d.ts +2 -0
- package/dist/__tests__/lib/page-audit-input.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-audit-input.test.js +122 -0
- package/dist/__tests__/lib/page-audit-input.test.js.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.js +2 -0
- package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
- package/dist/__tests__/lib/page-redirect.test.d.ts +2 -0
- package/dist/__tests__/lib/page-redirect.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-redirect.test.js +26 -0
- package/dist/__tests__/lib/page-redirect.test.js.map +1 -0
- package/dist/__tests__/lib/rail-width.test.d.ts +2 -0
- package/dist/__tests__/lib/rail-width.test.d.ts.map +1 -0
- package/dist/__tests__/lib/rail-width.test.js +69 -0
- package/dist/__tests__/lib/rail-width.test.js.map +1 -0
- package/dist/__tests__/lib/reflow-baseline.test.d.ts +2 -0
- package/dist/__tests__/lib/reflow-baseline.test.d.ts.map +1 -0
- package/dist/__tests__/lib/reflow-baseline.test.js +56 -0
- package/dist/__tests__/lib/reflow-baseline.test.js.map +1 -0
- package/dist/__tests__/lib/reflow-probe.test.d.ts +2 -0
- package/dist/__tests__/lib/reflow-probe.test.d.ts.map +1 -0
- package/dist/__tests__/lib/reflow-probe.test.js +73 -0
- package/dist/__tests__/lib/reflow-probe.test.js.map +1 -0
- package/dist/__tests__/lib/version-diff.test.d.ts +2 -0
- package/dist/__tests__/lib/version-diff.test.d.ts.map +1 -0
- package/dist/__tests__/lib/version-diff.test.js +230 -0
- package/dist/__tests__/lib/version-diff.test.js.map +1 -0
- package/dist/__tests__/lib/version-history.test.d.ts +2 -0
- package/dist/__tests__/lib/version-history.test.d.ts.map +1 -0
- package/dist/__tests__/lib/version-history.test.js +125 -0
- package/dist/__tests__/lib/version-history.test.js.map +1 -0
- package/dist/__tests__/styles/canvas-container-queries.test.d.ts +2 -0
- package/dist/__tests__/styles/canvas-container-queries.test.d.ts.map +1 -0
- package/dist/__tests__/styles/canvas-container-queries.test.js +87 -0
- package/dist/__tests__/styles/canvas-container-queries.test.js.map +1 -0
- package/dist/__tests__/styles/packaging.test.d.ts +2 -0
- package/dist/__tests__/styles/packaging.test.d.ts.map +1 -0
- package/dist/__tests__/styles/packaging.test.js +31 -0
- package/dist/__tests__/styles/packaging.test.js.map +1 -0
- package/dist/__tests__/styles/responsive-layer.test.d.ts +2 -0
- package/dist/__tests__/styles/responsive-layer.test.d.ts.map +1 -0
- package/dist/__tests__/styles/responsive-layer.test.js +127 -0
- package/dist/__tests__/styles/responsive-layer.test.js.map +1 -0
- package/dist/__tests__/styles/tokens-drift.test.d.ts +2 -0
- package/dist/__tests__/styles/tokens-drift.test.d.ts.map +1 -0
- package/dist/__tests__/styles/tokens-drift.test.js +92 -0
- package/dist/__tests__/styles/tokens-drift.test.js.map +1 -0
- package/dist/__tests__/views/admin-renderer-compat.test.d.ts +2 -0
- package/dist/__tests__/views/admin-renderer-compat.test.d.ts.map +1 -0
- package/dist/__tests__/views/admin-renderer-compat.test.js +43 -0
- package/dist/__tests__/views/admin-renderer-compat.test.js.map +1 -0
- package/dist/__tests__/views/admin-section-renderers.test.js +52 -4
- package/dist/__tests__/views/admin-section-renderers.test.js.map +1 -1
- package/dist/__tests__/views/canvas-review-mode.render.test.d.ts +2 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.js +70 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.js.map +1 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js +30 -1
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/page-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-tab.render.test.js +151 -0
- package/dist/__tests__/views/page-tab.render.test.js.map +1 -0
- package/dist/__tests__/views/section-class-parity.test.d.ts +2 -0
- package/dist/__tests__/views/section-class-parity.test.d.ts.map +1 -0
- package/dist/__tests__/views/section-class-parity.test.js +81 -0
- package/dist/__tests__/views/section-class-parity.test.js.map +1 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts +2 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.js +86 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.js.map +1 -0
- package/dist/__tests__/views/version-history-panel.render.test.d.ts +2 -0
- package/dist/__tests__/views/version-history-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/version-history-panel.render.test.js +153 -0
- package/dist/__tests__/views/version-history-panel.render.test.js.map +1 -0
- package/dist/components/BreakpointPicker.d.ts +18 -0
- package/dist/components/BreakpointPicker.d.ts.map +1 -0
- package/dist/components/BreakpointPicker.js +31 -0
- package/dist/components/BreakpointPicker.js.map +1 -0
- package/dist/components/CanvasEditTip.d.ts +6 -0
- package/dist/components/CanvasEditTip.d.ts.map +1 -0
- package/dist/components/CanvasEditTip.js +22 -0
- package/dist/components/CanvasEditTip.js.map +1 -0
- package/dist/components/EditorRail.d.ts +36 -0
- package/dist/components/EditorRail.d.ts.map +1 -0
- package/dist/components/EditorRail.js +104 -0
- package/dist/components/EditorRail.js.map +1 -0
- package/dist/components/EditorRailTabs.d.ts +32 -0
- package/dist/components/EditorRailTabs.d.ts.map +1 -0
- package/dist/components/EditorRailTabs.js +45 -0
- package/dist/components/EditorRailTabs.js.map +1 -0
- package/dist/components/MediaBlock.d.ts +49 -0
- package/dist/components/MediaBlock.d.ts.map +1 -0
- package/dist/components/MediaBlock.js +50 -0
- package/dist/components/MediaBlock.js.map +1 -0
- package/dist/components/ReflowMeter.d.ts +28 -0
- package/dist/components/ReflowMeter.d.ts.map +1 -0
- package/dist/components/ReflowMeter.js +42 -0
- package/dist/components/ReflowMeter.js.map +1 -0
- package/dist/components/ZoomChip.d.ts +17 -0
- package/dist/components/ZoomChip.d.ts.map +1 -0
- package/dist/components/ZoomChip.js +17 -0
- package/dist/components/ZoomChip.js.map +1 -0
- package/dist/components/breakpoint-icons.d.ts +3 -0
- package/dist/components/breakpoint-icons.d.ts.map +1 -0
- package/dist/components/breakpoint-icons.js +16 -0
- package/dist/components/breakpoint-icons.js.map +1 -0
- package/dist/components/page-tab/AuthorPicker.d.ts +31 -0
- package/dist/components/page-tab/AuthorPicker.d.ts.map +1 -0
- package/dist/components/page-tab/AuthorPicker.js +95 -0
- package/dist/components/page-tab/AuthorPicker.js.map +1 -0
- package/dist/components/page-tab/DirectiveSwitch.d.ts +38 -0
- package/dist/components/page-tab/DirectiveSwitch.d.ts.map +1 -0
- package/dist/components/page-tab/DirectiveSwitch.js +33 -0
- package/dist/components/page-tab/DirectiveSwitch.js.map +1 -0
- package/dist/components/seo/EditorSeoScoreBadge.d.ts +8 -1
- package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -1
- package/dist/components/seo/EditorSeoScoreBadge.js +2 -2
- package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -1
- package/dist/components/seo/PageAuditPanel.d.ts +14 -0
- package/dist/components/seo/PageAuditPanel.d.ts.map +1 -0
- package/dist/components/seo/PageAuditPanel.js +59 -0
- package/dist/components/seo/PageAuditPanel.js.map +1 -0
- package/dist/components/seo/SeoCharCount.d.ts +19 -7
- package/dist/components/seo/SeoCharCount.d.ts.map +1 -1
- package/dist/components/seo/SeoCharCount.js +30 -16
- package/dist/components/seo/SeoCharCount.js.map +1 -1
- package/dist/components/seo/SeoEditorPane.d.ts +15 -1
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorPane.js +118 -21
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/components/seo/SerpPreview.d.ts +20 -0
- package/dist/components/seo/SerpPreview.d.ts.map +1 -0
- package/dist/components/seo/SerpPreview.js +6 -0
- package/dist/components/seo/SerpPreview.js.map +1 -0
- package/dist/hooks/usePageAudit.d.ts +41 -0
- package/dist/hooks/usePageAudit.d.ts.map +1 -0
- package/dist/hooks/usePageAudit.js +68 -0
- package/dist/hooks/usePageAudit.js.map +1 -0
- package/dist/hooks/useReflowGuard.d.ts +47 -0
- package/dist/hooks/useReflowGuard.d.ts.map +1 -0
- package/dist/hooks/useReflowGuard.js +52 -0
- package/dist/hooks/useReflowGuard.js.map +1 -0
- package/dist/layout/primitives/AdminShell.d.ts.map +1 -1
- package/dist/layout/primitives/AdminShell.js +2 -2
- package/dist/layout/primitives/AdminShell.js.map +1 -1
- package/dist/lib/breakpoints.d.ts +51 -0
- package/dist/lib/breakpoints.d.ts.map +1 -0
- package/dist/lib/breakpoints.js +84 -0
- package/dist/lib/breakpoints.js.map +1 -0
- package/dist/lib/canvas-edit-chrome.d.ts +22 -0
- package/dist/lib/canvas-edit-chrome.d.ts.map +1 -0
- package/dist/lib/canvas-edit-chrome.js +81 -0
- package/dist/lib/canvas-edit-chrome.js.map +1 -0
- package/dist/lib/media-format.d.ts +55 -0
- package/dist/lib/media-format.d.ts.map +1 -0
- package/dist/lib/media-format.js +93 -0
- package/dist/lib/media-format.js.map +1 -0
- package/dist/lib/page-audit-input.d.ts +45 -0
- package/dist/lib/page-audit-input.d.ts.map +1 -0
- package/dist/lib/page-audit-input.js +53 -0
- package/dist/lib/page-audit-input.js.map +1 -0
- package/dist/lib/page-editor-service.d.ts +12 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +6 -0
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/page-redirect.d.ts +39 -0
- package/dist/lib/page-redirect.d.ts.map +1 -0
- package/dist/lib/page-redirect.js +38 -0
- package/dist/lib/page-redirect.js.map +1 -0
- package/dist/lib/rail-width.d.ts +23 -0
- package/dist/lib/rail-width.d.ts.map +1 -0
- package/dist/lib/rail-width.js +59 -0
- package/dist/lib/rail-width.js.map +1 -0
- package/dist/lib/reflow-baseline.d.ts +45 -0
- package/dist/lib/reflow-baseline.d.ts.map +1 -0
- package/dist/lib/reflow-baseline.js +38 -0
- package/dist/lib/reflow-baseline.js.map +1 -0
- package/dist/lib/reflow-probe.d.ts +159 -0
- package/dist/lib/reflow-probe.d.ts.map +1 -0
- package/dist/lib/reflow-probe.js +228 -0
- package/dist/lib/reflow-probe.js.map +1 -0
- package/dist/lib/seo-service.d.ts +10 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/lib/version-diff.d.ts +50 -0
- package/dist/lib/version-diff.d.ts.map +1 -0
- package/dist/lib/version-diff.js +294 -0
- package/dist/lib/version-diff.js.map +1 -0
- package/dist/lib/version-history.d.ts +73 -0
- package/dist/lib/version-history.d.ts.map +1 -0
- package/dist/lib/version-history.js +126 -0
- package/dist/lib/version-history.js.map +1 -0
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-editor/CanvasMetaLine.d.ts +19 -0
- package/dist/views/page-editor/CanvasMetaLine.d.ts.map +1 -0
- package/dist/views/page-editor/CanvasMetaLine.js +23 -0
- package/dist/views/page-editor/CanvasMetaLine.js.map +1 -0
- package/dist/views/page-editor/EditorCanvas.d.ts +47 -8
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorCanvas.js +68 -34
- package/dist/views/page-editor/EditorCanvas.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +21 -2
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +13 -20
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +383 -78
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/PageTab.d.ts +36 -0
- package/dist/views/page-editor/PageTab.d.ts.map +1 -0
- package/dist/views/page-editor/PageTab.js +65 -0
- package/dist/views/page-editor/PageTab.js.map +1 -0
- package/dist/views/page-editor/ReviewBar.d.ts +21 -0
- package/dist/views/page-editor/ReviewBar.d.ts.map +1 -0
- package/dist/views/page-editor/ReviewBar.js +22 -0
- package/dist/views/page-editor/ReviewBar.js.map +1 -0
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +64 -20
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/page-editor/SectionsPanel.d.ts +30 -5
- package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -1
- package/dist/views/page-editor/SectionsPanel.js +38 -7
- package/dist/views/page-editor/SectionsPanel.js.map +1 -1
- package/dist/views/page-editor/SitePreviewFrame.d.ts.map +1 -1
- package/dist/views/page-editor/SitePreviewFrame.js +3 -0
- package/dist/views/page-editor/SitePreviewFrame.js.map +1 -1
- package/dist/views/page-editor/VersionHistoryPanel.d.ts +29 -0
- package/dist/views/page-editor/VersionHistoryPanel.d.ts.map +1 -0
- package/dist/views/page-editor/VersionHistoryPanel.js +81 -0
- package/dist/views/page-editor/VersionHistoryPanel.js.map +1 -0
- package/dist/views/page-editor/reflow-context.d.ts +28 -0
- package/dist/views/page-editor/reflow-context.d.ts.map +1 -0
- package/dist/views/page-editor/reflow-context.js +14 -0
- package/dist/views/page-editor/reflow-context.js.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/ByTheNumbersSection.js +1 -1
- package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.js +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -1
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/FeatureHighlightSection.js +2 -2
- package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -1
- package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/GallerySection.js +7 -3
- package/dist/views/page-editor/sections/GallerySection.js.map +1 -1
- package/dist/views/page-editor/sections/HeroBannerSection.js +1 -1
- package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -1
- package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/MissionSection.js +1 -1
- package/dist/views/page-editor/sections/MissionSection.js.map +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.js +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.js +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts +13 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.js +66 -26
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
- package/dist/views/page-editor/sections/admin-renderers.d.ts +47 -15
- package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -1
- package/dist/views/page-editor/sections/admin-renderers.js +27 -15
- package/dist/views/page-editor/sections/admin-renderers.js.map +1 -1
- package/dist/views/page-editor/sections/parts.d.ts +0 -1
- package/dist/views/page-editor/sections/parts.d.ts.map +1 -1
- package/dist/views/page-editor/sections/parts.js +41 -16
- package/dist/views/page-editor/sections/parts.js.map +1 -1
- package/dist/views/page-editor/viewports.d.ts +7 -14
- package/dist/views/page-editor/viewports.d.ts.map +1 -1
- package/dist/views/page-editor/viewports.js +7 -12
- package/dist/views/page-editor/viewports.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts +2 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +5 -2
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +2 -20
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.js +4 -1
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
- package/package.json +5 -3
- package/src/AdminRoot.tsx +27 -6
- package/src/__tests__/components/breakpoint-controls.render.test.tsx +57 -0
- package/src/__tests__/components/canvas-edit-tip.render.test.tsx +41 -0
- package/src/__tests__/components/editor-rail.render.test.tsx +192 -0
- package/src/__tests__/components/media-block.render.test.tsx +126 -0
- package/src/__tests__/components/page-audit-panel.render.test.tsx +64 -0
- package/src/__tests__/components/reflow-meter.render.test.tsx +144 -0
- package/src/__tests__/components/seo-char-count.test.tsx +26 -6
- package/src/__tests__/components/serp-preview.render.test.tsx +32 -0
- package/src/__tests__/lib/breakpoints.test.ts +84 -0
- package/src/__tests__/lib/canvas-edit-chrome.test.ts +56 -0
- package/src/__tests__/lib/media-format.test.ts +89 -0
- package/src/__tests__/lib/page-audit-input.test.ts +144 -0
- package/src/__tests__/lib/page-editor-service.test.ts +2 -0
- package/src/__tests__/lib/page-redirect.test.ts +29 -0
- package/src/__tests__/lib/rail-width.test.ts +86 -0
- package/src/__tests__/lib/reflow-baseline.test.ts +64 -0
- package/src/__tests__/lib/reflow-probe.test.ts +95 -0
- package/src/__tests__/lib/version-diff.test.ts +278 -0
- package/src/__tests__/lib/version-history.test.ts +166 -0
- package/src/__tests__/styles/canvas-container-queries.test.ts +103 -0
- package/src/__tests__/styles/packaging.test.ts +38 -0
- package/src/__tests__/styles/responsive-layer.test.ts +152 -0
- package/src/__tests__/styles/tokens-drift.test.ts +110 -0
- package/src/__tests__/views/admin-renderer-compat.test.ts +48 -0
- package/src/__tests__/views/admin-section-renderers.test.tsx +70 -4
- package/src/__tests__/views/canvas-review-mode.render.test.tsx +94 -0
- package/src/__tests__/views/editor-top-bar.render.test.tsx +43 -1
- package/src/__tests__/views/page-tab.render.test.tsx +226 -0
- package/src/__tests__/views/section-class-parity.test.ts +98 -0
- package/src/__tests__/views/sections-panel-design-owned.render.test.tsx +104 -0
- package/src/__tests__/views/version-history-panel.render.test.tsx +195 -0
- package/src/components/BreakpointPicker.tsx +114 -0
- package/src/components/CanvasEditTip.tsx +46 -0
- package/src/components/EditorRail.tsx +219 -0
- package/src/components/EditorRailTabs.tsx +117 -0
- package/src/components/MediaBlock.tsx +223 -0
- package/src/components/ReflowMeter.tsx +125 -0
- package/src/components/ZoomChip.tsx +39 -0
- package/src/components/breakpoint-icons.ts +17 -0
- package/src/components/page-tab/AuthorPicker.tsx +240 -0
- package/src/components/page-tab/DirectiveSwitch.tsx +89 -0
- package/src/components/seo/EditorSeoScoreBadge.tsx +9 -1
- package/src/components/seo/PageAuditPanel.tsx +192 -0
- package/src/components/seo/SeoCharCount.tsx +55 -24
- package/src/components/seo/SeoEditorPane.tsx +213 -31
- package/src/components/seo/SerpPreview.tsx +41 -0
- package/src/hooks/usePageAudit.ts +98 -0
- package/src/hooks/useReflowGuard.ts +96 -0
- package/src/layout/primitives/AdminShell.tsx +13 -1
- package/src/lib/breakpoints.ts +107 -0
- package/src/lib/canvas-edit-chrome.ts +82 -0
- package/src/lib/media-format.ts +99 -0
- package/src/lib/page-audit-input.ts +104 -0
- package/src/lib/page-editor-service.ts +18 -0
- package/src/lib/page-redirect.ts +60 -0
- package/src/lib/rail-width.ts +58 -0
- package/src/lib/reflow-baseline.ts +70 -0
- package/src/lib/reflow-probe.ts +299 -0
- package/src/lib/seo-service.ts +10 -0
- package/src/lib/version-diff.ts +376 -0
- package/src/lib/version-history.ts +192 -0
- package/src/styles/prenta-tokens.css +120 -42
- package/src/styles/responsive.css +281 -0
- package/src/styles/theme.css +138 -0
- package/src/views/page-editor/CanvasMetaLine.tsx +50 -0
- package/src/views/page-editor/EditorCanvas.tsx +140 -54
- package/src/views/page-editor/EditorTopBar.tsx +55 -51
- package/src/views/page-editor/PageSectionEditor.tsx +575 -185
- package/src/views/page-editor/PageTab.tsx +433 -0
- package/src/views/page-editor/ReviewBar.tsx +76 -0
- package/src/views/page-editor/SectionInspector.tsx +84 -42
- package/src/views/page-editor/SectionsPanel.tsx +282 -141
- package/src/views/page-editor/SitePreviewFrame.tsx +3 -0
- package/src/views/page-editor/VersionHistoryPanel.tsx +302 -0
- package/src/views/page-editor/reflow-context.ts +43 -0
- package/src/views/page-editor/sections/ByTheNumbersSection.tsx +3 -2
- package/src/views/page-editor/sections/CtaBannerSection.tsx +3 -1
- package/src/views/page-editor/sections/FeatureHighlightSection.tsx +3 -2
- package/src/views/page-editor/sections/GallerySection.tsx +7 -3
- package/src/views/page-editor/sections/HeroBannerSection.tsx +2 -2
- package/src/views/page-editor/sections/MissionSection.tsx +4 -2
- package/src/views/page-editor/sections/PullQuoteSection.tsx +3 -1
- package/src/views/page-editor/sections/RelatedPostsSection.tsx +2 -1
- package/src/views/page-editor/sections/SectionRenderer.tsx +115 -34
- package/src/views/page-editor/sections/admin-renderers.tsx +54 -15
- package/src/views/page-editor/sections/parts.tsx +46 -18
- package/src/views/page-editor/viewports.ts +13 -26
- package/src/views/post-editor/PostEditorCanvas.tsx +18 -11
- package/src/views/post-editor/PostSectionEditor.tsx +1 -56
- package/src/views/post-editor/PostTemplateEditor.tsx +3 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
4
|
+
import { EditorRail, RailHeader } from '../../components/EditorRail.js'
|
|
5
|
+
import { EditorRailTabs, type RailTab } from '../../components/EditorRailTabs.js'
|
|
6
|
+
|
|
7
|
+
const storage = new Map<string, string>()
|
|
8
|
+
vi.stubGlobal('localStorage', {
|
|
9
|
+
getItem: (k: string) => storage.get(k) ?? null,
|
|
10
|
+
setItem: (k: string, v: string) => void storage.set(k, String(v)),
|
|
11
|
+
removeItem: (k: string) => void storage.delete(k),
|
|
12
|
+
clear: () => storage.clear(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
function renderRail() {
|
|
16
|
+
return render(
|
|
17
|
+
<EditorRail collapsedLabel="Sections">
|
|
18
|
+
{({ width, dragging, collapse }) => (
|
|
19
|
+
<>
|
|
20
|
+
<RailHeader
|
|
21
|
+
title="Page content"
|
|
22
|
+
width={width}
|
|
23
|
+
dragging={dragging}
|
|
24
|
+
onCollapse={collapse}
|
|
25
|
+
/>
|
|
26
|
+
<p>panel body</p>
|
|
27
|
+
</>
|
|
28
|
+
)}
|
|
29
|
+
</EditorRail>,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('EditorRail', () => {
|
|
34
|
+
it('exposes the resize handle as an operable separator', () => {
|
|
35
|
+
// A bare div with a pointerdown handler is invisible to keyboard and
|
|
36
|
+
// assistive-tech users — the role and value range are what make the rail
|
|
37
|
+
// resizable without a mouse.
|
|
38
|
+
renderRail()
|
|
39
|
+
const handle = screen.getByRole('separator', { name: 'Resize panel' })
|
|
40
|
+
expect(handle.getAttribute('aria-orientation')).toBe('vertical')
|
|
41
|
+
expect(handle.getAttribute('aria-valuemin')).toBe('300')
|
|
42
|
+
expect(handle.getAttribute('aria-valuemax')).toBe('620')
|
|
43
|
+
expect(handle.getAttribute('tabindex')).toBe('0')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('resizes with arrow keys and clamps at the range edges', () => {
|
|
47
|
+
renderRail()
|
|
48
|
+
const handle = screen.getByRole('separator', { name: 'Resize panel' })
|
|
49
|
+
|
|
50
|
+
fireEvent.keyDown(handle, { key: 'ArrowRight' })
|
|
51
|
+
expect(handle.getAttribute('aria-valuenow')).toBe('376')
|
|
52
|
+
|
|
53
|
+
fireEvent.keyDown(handle, { key: 'ArrowLeft', shiftKey: true })
|
|
54
|
+
expect(handle.getAttribute('aria-valuenow')).toBe('328')
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < 10; i++) fireEvent.keyDown(handle, { key: 'ArrowLeft', shiftKey: true })
|
|
57
|
+
expect(handle.getAttribute('aria-valuenow')).toBe('300')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('resets to the default width on Home', () => {
|
|
61
|
+
renderRail()
|
|
62
|
+
const handle = screen.getByRole('separator', { name: 'Resize panel' })
|
|
63
|
+
fireEvent.keyDown(handle, { key: 'ArrowRight' })
|
|
64
|
+
fireEvent.keyDown(handle, { key: 'Home' })
|
|
65
|
+
expect(handle.getAttribute('aria-valuenow')).toBe('360')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('resets to the default width on double-click', () => {
|
|
69
|
+
renderRail()
|
|
70
|
+
const handle = screen.getByRole('separator', { name: 'Resize panel' })
|
|
71
|
+
fireEvent.keyDown(handle, { key: 'ArrowRight' })
|
|
72
|
+
fireEvent.doubleClick(handle)
|
|
73
|
+
expect(handle.getAttribute('aria-valuenow')).toBe('360')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('persists the width across mounts', () => {
|
|
77
|
+
const first = renderRail()
|
|
78
|
+
fireEvent.keyDown(screen.getByRole('separator', { name: 'Resize panel' }), {
|
|
79
|
+
key: 'ArrowRight',
|
|
80
|
+
shiftKey: true,
|
|
81
|
+
})
|
|
82
|
+
expect(localStorage.getItem('pb.panelW')).toBe('408')
|
|
83
|
+
first.unmount()
|
|
84
|
+
|
|
85
|
+
renderRail()
|
|
86
|
+
expect(
|
|
87
|
+
screen.getByRole('separator', { name: 'Resize panel' }).getAttribute('aria-valuenow'),
|
|
88
|
+
).toBe('408')
|
|
89
|
+
storage.clear()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('unmounts the panel when collapsed instead of leaving a zero-width sliver', () => {
|
|
93
|
+
// A 0-width panel keeps its contents in the accessibility tree, so a
|
|
94
|
+
// screen-reader user lands inside a panel sighted users cannot see.
|
|
95
|
+
renderRail()
|
|
96
|
+
fireEvent.click(screen.getByRole('button', { name: 'Collapse Page content panel' }))
|
|
97
|
+
|
|
98
|
+
expect(screen.queryByText('panel body')).toBeNull()
|
|
99
|
+
expect(screen.queryByRole('separator', { name: 'Resize panel' })).toBeNull()
|
|
100
|
+
expect(screen.getByRole('button', { name: 'Expand Sections panel' })).toBeTruthy()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('restores the panel from the collapsed strip', () => {
|
|
104
|
+
renderRail()
|
|
105
|
+
fireEvent.click(screen.getByRole('button', { name: 'Collapse Page content panel' }))
|
|
106
|
+
fireEvent.click(screen.getByRole('button', { name: 'Expand Sections panel' }))
|
|
107
|
+
expect(screen.getByText('panel body')).toBeTruthy()
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const TABS: RailTab[] = [
|
|
112
|
+
{ id: 'sections', label: 'Sections', title: 'Page content', count: 4 },
|
|
113
|
+
{ id: 'seo', label: 'SEO & GEO', title: 'Search & AI readiness', score: 72 },
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
describe('EditorRailTabs', () => {
|
|
117
|
+
it('marks the active tab and links it to its panel', () => {
|
|
118
|
+
render(<EditorRailTabs tabs={TABS} active="sections" onChange={vi.fn()} />)
|
|
119
|
+
const active = screen.getByRole('tab', { selected: true })
|
|
120
|
+
expect(active.textContent).toContain('Sections')
|
|
121
|
+
expect(active.getAttribute('aria-controls')).toBe('rail-panel-sections')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('shows the section count without opening the tab', () => {
|
|
125
|
+
render(<EditorRailTabs tabs={TABS} active="seo" onChange={vi.fn()} />)
|
|
126
|
+
expect(screen.getByRole('tab', { name: /Sections/ }).textContent).toContain('4')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('carries the score as text, not colour alone', () => {
|
|
130
|
+
// The band dot is aria-hidden; anyone who cannot distinguish the hues still
|
|
131
|
+
// needs the number.
|
|
132
|
+
render(<EditorRailTabs tabs={TABS} active="sections" onChange={vi.fn()} />)
|
|
133
|
+
expect(screen.getByRole('tab', { name: /score 72 of 100/i })).toBeTruthy()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('moves between tabs with arrow keys', () => {
|
|
137
|
+
// Inactive tabs have tabIndex -1, so arrows are the only way to reach them.
|
|
138
|
+
const onChange = vi.fn()
|
|
139
|
+
render(<EditorRailTabs tabs={TABS} active="sections" onChange={onChange} />)
|
|
140
|
+
fireEvent.keyDown(screen.getByRole('tab', { selected: true }), { key: 'ArrowRight' })
|
|
141
|
+
expect(onChange).toHaveBeenCalledWith('seo')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('bands the dot on the audit scale, not the content-analysis one', () => {
|
|
145
|
+
// The two disagree between 55 and 59: `gradeFromScore` calls 57 "poor",
|
|
146
|
+
// the audit calls it "fair". The dot summarises the audit's rings, so a
|
|
147
|
+
// page scoring 57 must not read worse on the tab than in the panel.
|
|
148
|
+
render(
|
|
149
|
+
<EditorRailTabs
|
|
150
|
+
tabs={[{ id: 'seo', label: 'SEO & GEO', title: 'x', score: 57 }]}
|
|
151
|
+
active="seo"
|
|
152
|
+
onChange={vi.fn()}
|
|
153
|
+
/>,
|
|
154
|
+
)
|
|
155
|
+
const dot = screen.getByRole('tab').querySelector('span[aria-hidden]')
|
|
156
|
+
expect(dot?.className).toContain('bg-warning')
|
|
157
|
+
expect(dot?.className).not.toContain('bg-destructive')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('shows no dot at all until a score is known', () => {
|
|
161
|
+
// An absent audit must not borrow a number from another scorer — no dot
|
|
162
|
+
// beats a dot computed by a different model with different bands.
|
|
163
|
+
render(
|
|
164
|
+
<EditorRailTabs
|
|
165
|
+
tabs={[{ id: 'seo', label: 'SEO & GEO', title: 'x' }]}
|
|
166
|
+
active="seo"
|
|
167
|
+
onChange={vi.fn()}
|
|
168
|
+
/>,
|
|
169
|
+
)
|
|
170
|
+
expect(screen.getByRole('tab').querySelector('span[aria-hidden]')).toBeNull()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('reads a deliberately unrated page as not rated, never as a low score', () => {
|
|
174
|
+
render(
|
|
175
|
+
<EditorRailTabs
|
|
176
|
+
tabs={[{ id: 'seo', label: 'SEO & GEO', title: 'x', score: null }]}
|
|
177
|
+
active="seo"
|
|
178
|
+
onChange={vi.fn()}
|
|
179
|
+
/>,
|
|
180
|
+
)
|
|
181
|
+
expect(screen.getByRole('tab', { name: /not rated/i })).toBeTruthy()
|
|
182
|
+
const dot = screen.getByRole('tab').querySelector('span[aria-hidden]')
|
|
183
|
+
expect(dot?.className).not.toContain('bg-destructive')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('wraps around at the ends', () => {
|
|
187
|
+
const onChange = vi.fn()
|
|
188
|
+
render(<EditorRailTabs tabs={TABS} active="sections" onChange={onChange} />)
|
|
189
|
+
fireEvent.keyDown(screen.getByRole('tab', { selected: true }), { key: 'ArrowLeft' })
|
|
190
|
+
expect(onChange).toHaveBeenCalledWith('seo')
|
|
191
|
+
})
|
|
192
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it, vi, afterEach } from 'vitest'
|
|
3
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
4
|
+
|
|
5
|
+
const LIBRARY = {
|
|
6
|
+
items: [
|
|
7
|
+
{
|
|
8
|
+
id: 'm1',
|
|
9
|
+
filename: 'team-offsite-2026.jpg',
|
|
10
|
+
storageKey: 'k1',
|
|
11
|
+
url: '/media/team-offsite-2026.jpg',
|
|
12
|
+
mimeType: 'image/jpeg',
|
|
13
|
+
width: 2400,
|
|
14
|
+
height: 1600,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'm2',
|
|
18
|
+
filename: 'notes.pdf',
|
|
19
|
+
storageKey: 'k2',
|
|
20
|
+
url: '/media/notes.pdf',
|
|
21
|
+
mimeType: 'application/pdf',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `useApiData` is mocked rather than `fetch`: the hook layers caching, retries
|
|
28
|
+
* and abort handling on top, none of which this component owns, and driving it
|
|
29
|
+
* through a fake network makes the test about the hook instead of the block.
|
|
30
|
+
*/
|
|
31
|
+
let requested: string | null = null
|
|
32
|
+
|
|
33
|
+
vi.mock('../../lib/useApiData.js', () => ({
|
|
34
|
+
useApiData: (endpoint: string | null) => {
|
|
35
|
+
requested = endpoint
|
|
36
|
+
return { data: endpoint ? LIBRARY : null, loading: false, error: null, refetch: vi.fn() }
|
|
37
|
+
},
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
const { MediaBlock } = await import('../../components/MediaBlock.js')
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
cleanup()
|
|
44
|
+
requested = null
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('MediaBlock', () => {
|
|
48
|
+
it('shows the toolbar without hovering, in both states', () => {
|
|
49
|
+
// Replace and Remove are the two things an editor comes here to do. Behind
|
|
50
|
+
// a hover they are undiscoverable on touch and invisible when scanning.
|
|
51
|
+
render(<MediaBlock value="" onChange={() => {}} />)
|
|
52
|
+
expect(screen.getByRole('button', { name: 'Choose' })).toBeTruthy()
|
|
53
|
+
cleanup()
|
|
54
|
+
|
|
55
|
+
render(<MediaBlock value="/media/hero.jpg" onChange={() => {}} label="Hero image" />)
|
|
56
|
+
expect(screen.getByRole('button', { name: 'Replace' })).toBeTruthy()
|
|
57
|
+
expect(screen.getByRole('button', { name: 'Remove hero image' })).toBeTruthy()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('names the file and its format rather than the raw URL', () => {
|
|
61
|
+
render(
|
|
62
|
+
<MediaBlock value="https://cdn.example.com/a/team-offsite.jpg?sig=abc" onChange={() => {}} />,
|
|
63
|
+
)
|
|
64
|
+
expect(screen.getByText('team-offsite.jpg')).toBeTruthy()
|
|
65
|
+
expect(screen.getByText('JPG')).toBeTruthy()
|
|
66
|
+
// The signing token is noise in a narrow toolbar and never what is being read.
|
|
67
|
+
expect(screen.queryByText(/sig=abc/)).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('opens the picker inside the shell, not as a dialog', () => {
|
|
71
|
+
// Choosing an image is a comparison against the slot it goes into. A modal
|
|
72
|
+
// covering the slot removes the only context that matters.
|
|
73
|
+
render(<MediaBlock value="" onChange={() => {}} />)
|
|
74
|
+
fireEvent.click(screen.getByRole('button', { name: 'Choose' }))
|
|
75
|
+
expect(screen.getByText('team-offsite-2026.jpg')).toBeTruthy()
|
|
76
|
+
expect(screen.queryByRole('dialog')).toBeNull()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('reports each asset’s real dimensions and ratio', () => {
|
|
80
|
+
render(<MediaBlock value="" onChange={() => {}} />)
|
|
81
|
+
fireEvent.click(screen.getByRole('button', { name: 'Choose' }))
|
|
82
|
+
// The frame shows the template's crop, so the asset's own shape has to be
|
|
83
|
+
// stated somewhere or an editor cannot tell what will be cropped.
|
|
84
|
+
expect(screen.getByText('2400 × 1600 · 3:2')).toBeTruthy()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('offers only images', () => {
|
|
88
|
+
render(<MediaBlock value="" onChange={() => {}} />)
|
|
89
|
+
fireEvent.click(screen.getByRole('button', { name: 'Choose' }))
|
|
90
|
+
expect(screen.getByText('team-offsite-2026.jpg')).toBeTruthy()
|
|
91
|
+
expect(screen.queryByText('notes.pdf')).toBeNull()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('returns the chosen asset with its dimensions', () => {
|
|
95
|
+
const onChange = vi.fn()
|
|
96
|
+
render(<MediaBlock value="" onChange={onChange} />)
|
|
97
|
+
fireEvent.click(screen.getByRole('button', { name: 'Choose' }))
|
|
98
|
+
fireEvent.click(screen.getByText('team-offsite-2026.jpg'))
|
|
99
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
100
|
+
'/media/team-offsite-2026.jpg',
|
|
101
|
+
expect.objectContaining({ width: 2400, height: 1600 }),
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('clears the slot with an empty url', () => {
|
|
106
|
+
const onChange = vi.fn()
|
|
107
|
+
render(<MediaBlock value="/media/hero.jpg" onChange={onChange} />)
|
|
108
|
+
fireEvent.click(screen.getByRole('button', { name: 'Remove image' }))
|
|
109
|
+
expect(onChange).toHaveBeenCalledWith('')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('disables every control when the editor cannot edit', () => {
|
|
113
|
+
render(<MediaBlock value="/media/hero.jpg" onChange={() => {}} disabled />)
|
|
114
|
+
expect(screen.getByRole('button', { name: 'Replace' }).hasAttribute('disabled')).toBe(true)
|
|
115
|
+
expect(screen.getByRole('button', { name: 'Remove image' }).hasAttribute('disabled')).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('MediaBlock — library requests', () => {
|
|
120
|
+
it('does not fetch the library until the picker is opened', () => {
|
|
121
|
+
render(<MediaBlock value="/media/hero.jpg" onChange={() => {}} />)
|
|
122
|
+
expect(requested).toBeNull()
|
|
123
|
+
fireEvent.click(screen.getByRole('button', { name: 'Replace' }))
|
|
124
|
+
expect(requested).toContain('/media')
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { render, screen } from '@testing-library/react'
|
|
4
|
+
import { auditPage, type PageAuditInput } from '@prenta/core/seo/page-audit'
|
|
5
|
+
import { PageAuditPanel } from '../../components/seo/PageAuditPanel.js'
|
|
6
|
+
|
|
7
|
+
const blank: PageAuditInput = {
|
|
8
|
+
sections: [],
|
|
9
|
+
seo: {
|
|
10
|
+
title: '',
|
|
11
|
+
description: '',
|
|
12
|
+
slug: '',
|
|
13
|
+
keyword: '',
|
|
14
|
+
schema: '',
|
|
15
|
+
ogImage: null,
|
|
16
|
+
robots: { index: true, snippet: true },
|
|
17
|
+
},
|
|
18
|
+
llmsTxt: false,
|
|
19
|
+
brand: 'Prenta',
|
|
20
|
+
author: null,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('PageAuditPanel', () => {
|
|
24
|
+
it('states both scores in text, not colour alone', () => {
|
|
25
|
+
render(<PageAuditPanel audit={auditPage(blank)} />)
|
|
26
|
+
expect(screen.getByRole('img', { name: /SEO score: 0 of 100/i })).toBeTruthy()
|
|
27
|
+
expect(screen.getByRole('img', { name: /GEO score: 0 of 100/i })).toBeTruthy()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('names the denominator so a moving score is explainable', () => {
|
|
31
|
+
// The score changes when the set of applicable checks changes. Showing
|
|
32
|
+
// only the number is what made the old scorer feel arbitrary.
|
|
33
|
+
render(<PageAuditPanel audit={auditPage(blank)} />)
|
|
34
|
+
expect(screen.getAllByText(/of \d+ apply/).length).toBe(2)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('reads a gated group as not rated rather than zero', () => {
|
|
38
|
+
const audit = auditPage({
|
|
39
|
+
...blank,
|
|
40
|
+
seo: { ...blank.seo, robots: { index: false, snippet: true } },
|
|
41
|
+
})
|
|
42
|
+
render(<PageAuditPanel audit={audit} />)
|
|
43
|
+
expect(screen.getByRole('img', { name: /SEO: not rated/i })).toBeTruthy()
|
|
44
|
+
expect(screen.getByText('not rated')).toBeTruthy()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('gives every check an outcome in text for screen readers', () => {
|
|
48
|
+
render(<PageAuditPanel audit={auditPage(blank)} />)
|
|
49
|
+
// The status square is aria-hidden, so the outcome has to live in the row.
|
|
50
|
+
expect(screen.getAllByText(/needs attention/).length).toBeGreaterThan(0)
|
|
51
|
+
expect(screen.getAllByText(/not applicable/).length).toBeGreaterThan(0)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('explains a not-applicable check instead of just greying it', () => {
|
|
55
|
+
render(<PageAuditPanel audit={auditPage(blank)} />)
|
|
56
|
+
// All three placement checks explain themselves — none is silently greyed.
|
|
57
|
+
expect(screen.getAllByText(/Set a focus keyword to enable this check/i)).toHaveLength(3)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('tells the editor what to do about a failing check', () => {
|
|
61
|
+
render(<PageAuditPanel audit={auditPage(blank)} />)
|
|
62
|
+
expect(screen.getByText(/Aim for 30–60 characters/i)).toBeTruthy()
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
4
|
+
import { ReflowMeter } from '../../components/ReflowMeter.js'
|
|
5
|
+
import type { ReflowGuardResult, ReflowRow } from '../../hooks/useReflowGuard.js'
|
|
6
|
+
import { BREAKPOINTS } from '../../lib/breakpoints.js'
|
|
7
|
+
|
|
8
|
+
function rows(overrides: Partial<Record<string, Partial<ReflowRow>>> = {}): ReflowRow[] {
|
|
9
|
+
return BREAKPOINTS.map((bp) => {
|
|
10
|
+
const o = overrides[bp.id] ?? {}
|
|
11
|
+
const lines = o.lines ?? 2
|
|
12
|
+
const was = o.was ?? 2
|
|
13
|
+
return { id: bp.id, lines, was, diff: lines - was, state: o.state ?? 'same' }
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function guard(partial: Partial<ReflowGuardResult> = {}): ReflowGuardResult {
|
|
18
|
+
const r = partial.rows ?? rows()
|
|
19
|
+
return {
|
|
20
|
+
rows: r,
|
|
21
|
+
grown: partial.grown ?? r.filter((x) => x.diff > 0),
|
|
22
|
+
worst: partial.worst ?? null,
|
|
23
|
+
delta: partial.delta ?? 0,
|
|
24
|
+
...partial,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('ReflowMeter', () => {
|
|
29
|
+
it('reports the line count for the breakpoint being viewed, not the worst one', () => {
|
|
30
|
+
// "lines here" has to mean the canvas the editor is actually looking at.
|
|
31
|
+
// Showing the worst breakpoint's count under a canvas rendering a different
|
|
32
|
+
// one makes the editor mistrust both numbers.
|
|
33
|
+
const { container } = render(
|
|
34
|
+
<ReflowMeter
|
|
35
|
+
guard={guard({ rows: rows({ mobile: { lines: 5 }, laptop: { lines: 2 } }) })}
|
|
36
|
+
length={64}
|
|
37
|
+
device="laptop"
|
|
38
|
+
onProof={() => {}}
|
|
39
|
+
/>,
|
|
40
|
+
)
|
|
41
|
+
const readout = container.querySelector('span')!.textContent!.replace(/\s+/g, ' ')
|
|
42
|
+
expect(readout).toContain('2 lines here')
|
|
43
|
+
expect(readout).not.toContain('5 lines here')
|
|
44
|
+
cleanup()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('states the character delta with its direction', () => {
|
|
48
|
+
render(
|
|
49
|
+
<ReflowMeter guard={guard({ delta: 12 })} length={80} device="wide" onProof={() => {}} />,
|
|
50
|
+
)
|
|
51
|
+
expect(screen.getByText('(+12)')).toBeTruthy()
|
|
52
|
+
cleanup()
|
|
53
|
+
|
|
54
|
+
render(
|
|
55
|
+
<ReflowMeter guard={guard({ delta: -9 })} length={40} device="wide" onProof={() => {}} />,
|
|
56
|
+
)
|
|
57
|
+
expect(screen.getByText('(-9)')).toBeTruthy()
|
|
58
|
+
cleanup()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('names the line count and its published value on every dot', () => {
|
|
62
|
+
// Colour alone must not carry the finding. Each dot's accessible name says
|
|
63
|
+
// what it measured and what it was, so the whole readout is reachable
|
|
64
|
+
// without seeing the tones.
|
|
65
|
+
render(
|
|
66
|
+
<ReflowMeter
|
|
67
|
+
guard={guard({
|
|
68
|
+
rows: rows({ mobile: { lines: 4, was: 2, state: 'grew-badly' } }),
|
|
69
|
+
worst: 'mobile',
|
|
70
|
+
})}
|
|
71
|
+
length={64}
|
|
72
|
+
device="wide"
|
|
73
|
+
onProof={() => {}}
|
|
74
|
+
/>,
|
|
75
|
+
)
|
|
76
|
+
const dot = screen.getByRole('button', { name: /^Mobile: 4 lines/ })
|
|
77
|
+
expect(dot.textContent).toContain('was 2 when published')
|
|
78
|
+
// The dot and the alert's proof button are distinguishable by name — two
|
|
79
|
+
// controls reading "Proof on Mobile" in one region are the same control to
|
|
80
|
+
// anyone navigating by voice or screen reader.
|
|
81
|
+
expect(screen.getByRole('button', { name: 'Proof on Mobile' })).not.toBe(dot)
|
|
82
|
+
cleanup()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('takes the canvas to the breakpoint a dot names', () => {
|
|
86
|
+
const onProof = vi.fn()
|
|
87
|
+
render(
|
|
88
|
+
<ReflowMeter
|
|
89
|
+
guard={guard({ rows: rows({ mobile: { lines: 4, was: 2, state: 'grew-badly' } }) })}
|
|
90
|
+
length={64}
|
|
91
|
+
device="wide"
|
|
92
|
+
onProof={onProof}
|
|
93
|
+
/>,
|
|
94
|
+
)
|
|
95
|
+
fireEvent.click(screen.getByRole('button', { name: /^Mobile: 4 lines/ }))
|
|
96
|
+
expect(onProof).toHaveBeenCalledWith('mobile')
|
|
97
|
+
cleanup()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('stays silent when nothing grew', () => {
|
|
101
|
+
// The alert is the interrupting element. It must appear only when wrapping
|
|
102
|
+
// actually changed, or editors learn to scroll past it.
|
|
103
|
+
render(<ReflowMeter guard={guard()} length={40} device="wide" onProof={() => {}} />)
|
|
104
|
+
expect(screen.queryByText(/Wrapping changed/)).toBeNull()
|
|
105
|
+
cleanup()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('says "every breakpoint" rather than listing all six', () => {
|
|
109
|
+
const all = rows(
|
|
110
|
+
Object.fromEntries(BREAKPOINTS.map((b) => [b.id, { lines: 3, was: 2, state: 'grew' }])),
|
|
111
|
+
)
|
|
112
|
+
render(
|
|
113
|
+
<ReflowMeter
|
|
114
|
+
guard={guard({ rows: all, worst: 'mobile' })}
|
|
115
|
+
length={90}
|
|
116
|
+
device="wide"
|
|
117
|
+
onProof={() => {}}
|
|
118
|
+
/>,
|
|
119
|
+
)
|
|
120
|
+
expect(screen.getByText('every breakpoint')).toBeTruthy()
|
|
121
|
+
cleanup()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('names the breakpoints while the list is short enough to read', () => {
|
|
125
|
+
render(
|
|
126
|
+
<ReflowMeter
|
|
127
|
+
guard={guard({
|
|
128
|
+
rows: rows({
|
|
129
|
+
mobile: { lines: 4, was: 2, state: 'grew-badly' },
|
|
130
|
+
tabletP: { lines: 3, was: 2, state: 'grew' },
|
|
131
|
+
}),
|
|
132
|
+
worst: 'mobile',
|
|
133
|
+
})}
|
|
134
|
+
length={90}
|
|
135
|
+
device="wide"
|
|
136
|
+
onProof={() => {}}
|
|
137
|
+
/>,
|
|
138
|
+
)
|
|
139
|
+
expect(screen.getByText('Tablet Portrait, Mobile')).toBeTruthy()
|
|
140
|
+
// The proof button names the one worth looking at, not the list.
|
|
141
|
+
expect(screen.getByRole('button', { name: 'Proof on Mobile' })).toBeTruthy()
|
|
142
|
+
cleanup()
|
|
143
|
+
})
|
|
144
|
+
})
|
|
@@ -6,22 +6,42 @@ import { SeoCharCount } from '../../components/seo/SeoCharCount.js'
|
|
|
6
6
|
describe('SeoCharCount', () => {
|
|
7
7
|
it('shows a neutral empty state', () => {
|
|
8
8
|
render(<SeoCharCount value="" min={30} max={60} />)
|
|
9
|
-
expect(screen.getByText(
|
|
9
|
+
expect(screen.getByText(/0 \/ 60 characters/)).toBeTruthy()
|
|
10
10
|
})
|
|
11
11
|
|
|
12
12
|
it('marks a value within the ideal range as good', () => {
|
|
13
13
|
render(<SeoCharCount value={'x'.repeat(40)} min={30} max={60} />)
|
|
14
|
-
expect(screen.getByText(/40\/60 · good/)).toBeTruthy()
|
|
14
|
+
expect(screen.getByText(/40 \/ 60 characters · good/)).toBeTruthy()
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
it('
|
|
17
|
+
it('says the tail will be truncated rather than just "too long"', () => {
|
|
18
|
+
// Over and under are different problems. "Will be truncated" tells the
|
|
19
|
+
// editor the words are written but nobody will read them.
|
|
18
20
|
const { container } = render(<SeoCharCount value={'x'.repeat(70)} min={30} max={60} />)
|
|
19
|
-
expect(screen.getByText(/70\/60 ·
|
|
21
|
+
expect(screen.getByText(/70 \/ 60 characters · will be truncated/)).toBeTruthy()
|
|
20
22
|
expect(container.querySelector('.text-destructive')).toBeTruthy()
|
|
21
23
|
})
|
|
22
24
|
|
|
23
|
-
it('
|
|
25
|
+
it('says too short rather than restating the target range', () => {
|
|
26
|
+
// "Aim for 30–60" describes the goal without saying which way to edit.
|
|
24
27
|
render(<SeoCharCount value={'x'.repeat(10)} min={30} max={60} />)
|
|
25
|
-
expect(screen.getByText(/
|
|
28
|
+
expect(screen.getByText(/10 \/ 60 characters · too short/)).toBeTruthy()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('caps the meter at full instead of overflowing', () => {
|
|
32
|
+
// A bar that kept growing past the maximum would imply more is better.
|
|
33
|
+
const { container } = render(<SeoCharCount value={'x'.repeat(200)} min={30} max={60} />)
|
|
34
|
+
const fill = container.querySelector('span[aria-hidden] > span') as HTMLElement | null
|
|
35
|
+
expect(fill?.style.width).toBe('100%')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('has no short or good state for a cap-only field', () => {
|
|
39
|
+
// Without a minimum there is no target to hit, only a ceiling not to cross.
|
|
40
|
+
// The count is split across JSX text nodes, so match on the rendered text.
|
|
41
|
+
const { container } = render(<SeoCharCount value={'x'.repeat(5)} max={200} />)
|
|
42
|
+
const text = container.textContent ?? ''
|
|
43
|
+
expect(text).toContain('5 / 200 characters')
|
|
44
|
+
expect(text).not.toContain('too short')
|
|
45
|
+
expect(text).not.toContain('good')
|
|
26
46
|
})
|
|
27
47
|
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { render, screen } from '@testing-library/react'
|
|
4
|
+
import { SerpPreview } from '../../components/seo/SerpPreview.js'
|
|
5
|
+
|
|
6
|
+
describe('SerpPreview', () => {
|
|
7
|
+
it('shows url, title and description as a searcher sees them', () => {
|
|
8
|
+
render(
|
|
9
|
+
<SerpPreview
|
|
10
|
+
title="Headless CMS for modern teams"
|
|
11
|
+
url="https://example.com/about"
|
|
12
|
+
description="Prenta gives developers an API-first architecture."
|
|
13
|
+
/>,
|
|
14
|
+
)
|
|
15
|
+
expect(screen.getByText('https://example.com/about')).toBeTruthy()
|
|
16
|
+
expect(screen.getByText('Headless CMS for modern teams')).toBeTruthy()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('renders the title in the search-result blue, not the brand accent', () => {
|
|
20
|
+
// The card's job is to look like the thing being previewed. Painted in the
|
|
21
|
+
// brand colour it previews the admin instead.
|
|
22
|
+
render(<SerpPreview title="T" url="u" description="d" />)
|
|
23
|
+
// Assert on the class list rather than a CSS selector — escaping `[`, `#`
|
|
24
|
+
// and `]` in a selector is brittle and fails for the wrong reason.
|
|
25
|
+
expect(screen.getByText('T').className).toContain('#1a0dab')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('says what a missing description means rather than rendering blank', () => {
|
|
29
|
+
render(<SerpPreview title="T" url="u" description="" />)
|
|
30
|
+
expect(screen.getByText(/Search engines will improvise one/i)).toBeTruthy()
|
|
31
|
+
})
|
|
32
|
+
})
|