@nextsparkjs/theme-default 0.1.0-beta.17 → 0.1.0-beta.171
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/api/ai/chat/stream/route.ts +4 -1
- package/api/ai/orchestrator/route.ts +10 -3
- package/api/ai/single-agent/route.ts +10 -3
- package/api/ai/usage/route.ts +4 -1
- package/blocks/benefits/component.tsx +4 -4
- package/blocks/cta-section/component.tsx +4 -4
- package/blocks/faq-accordion/component.tsx +2 -2
- package/blocks/features-grid/component.tsx +5 -5
- package/blocks/hero/component.tsx +2 -2
- package/blocks/hero/fields.ts +1 -1
- package/blocks/hero-with-form/component.tsx +7 -7
- package/blocks/hero-with-form/fields.ts +1 -1
- package/blocks/jumbotron/component.tsx +7 -7
- package/blocks/jumbotron/fields.ts +1 -1
- package/blocks/logo-cloud/component.tsx +6 -6
- package/blocks/logo-cloud/fields.ts +1 -1
- package/blocks/post-content/component.tsx +2 -2
- package/blocks/pricing-table/component.tsx +5 -5
- package/blocks/split-content/component.tsx +5 -5
- package/blocks/split-content/fields.ts +1 -1
- package/blocks/stats-counter/component.tsx +9 -9
- package/blocks/testimonials/component.tsx +4 -4
- package/blocks/testimonials/fields.ts +1 -1
- package/blocks/text-content/component.tsx +12 -10
- package/blocks/timeline/component.tsx +12 -12
- package/blocks/video-hero/component.tsx +7 -7
- package/blocks/video-hero/fields.ts +1 -1
- package/components/ai-chat/ChatPanel.tsx +7 -7
- package/components/ai-chat/Message.tsx +2 -2
- package/components/ai-chat/MessageInput.tsx +3 -3
- package/components/ai-chat/MessageList.tsx +3 -3
- package/components/ai-chat/TypingIndicator.tsx +2 -2
- package/config/app.config.ts +90 -62
- package/config/billing.config.ts +5 -10
- package/config/dashboard.config.ts +14 -0
- package/config/features.config.ts +10 -0
- package/config/permissions.config.ts +22 -23
- package/docs/{01-overview → public/01-overview}/01-introduction.md +5 -0
- package/docs/{01-overview → public/01-overview}/02-customization.md +5 -0
- package/docs/{02-features → public/02-features}/03-tasks-entity.md +5 -0
- package/docs/{03-ai → public/03-ai}/01-overview.md +5 -0
- package/docs/{03-ai → public/03-ai}/02-customization.md +5 -0
- package/docs/superadmin/01-setup/01-configuration.md +79 -0
- package/docs/superadmin/01-setup/02-deployment.md +82 -0
- package/docs/superadmin/02-management/01-users.md +83 -0
- package/docs/superadmin/03-integrations/01-langchain.md +139 -0
- package/emails/verify-email.ts +33 -0
- package/entities/customers/api/docs.md +107 -0
- package/entities/customers/api/presets.ts +80 -0
- package/entities/pages/api/docs.md +114 -0
- package/entities/pages/api/presets.ts +72 -0
- package/entities/posts/api/docs.md +120 -0
- package/entities/posts/api/presets.ts +74 -0
- package/entities/tasks/api/docs.md +126 -0
- package/entities/tasks/api/presets.ts +84 -0
- package/lib/selectors.ts +7 -4
- package/messages/de/admin.json +45 -0
- package/messages/en/admin.json +56 -0
- package/messages/en/navigation.json +2 -1
- package/messages/es/admin.json +56 -0
- package/messages/es/navigation.json +2 -1
- package/messages/fr/admin.json +45 -0
- package/messages/it/admin.json +45 -0
- package/messages/pt/admin.json +45 -0
- package/migrations/089_add_editor_team_role.sql +15 -23
- package/migrations/090_demo_users_teams.sql +11 -11
- package/migrations/091_greek_teams_billing.sql +15 -15
- package/migrations/093_pages_sample_data.sql +7 -7
- package/migrations/098_patterns_sample_data.sql +234 -0
- package/nextsparkjs-theme-default-0.1.0-beta.137.tgz +0 -0
- package/package.json +4 -3
- package/styles/globals.css +42 -0
- package/templates/(public)/page.tsx +1 -1
- package/tests/cypress/e2e/_utils/devtools/access.bdd.md +262 -0
- package/tests/cypress/e2e/_utils/devtools/access.cy.ts +171 -0
- package/tests/cypress/e2e/_utils/devtools/navigation.bdd.md +261 -0
- package/tests/cypress/e2e/_utils/devtools/navigation.cy.ts +157 -0
- package/tests/cypress/e2e/_utils/devtools/pages.bdd.md +303 -0
- package/tests/cypress/e2e/_utils/devtools/pages.cy.ts +184 -0
- package/tests/cypress/e2e/_utils/docs/README.md +215 -0
- package/tests/cypress/e2e/_utils/selectors/auth.bdd.md +354 -0
- package/tests/cypress/e2e/_utils/selectors/auth.cy.ts +310 -0
- package/tests/cypress/e2e/_utils/selectors/billing.bdd.md +276 -0
- package/tests/cypress/e2e/_utils/selectors/billing.cy.ts +182 -0
- package/tests/cypress/e2e/_utils/selectors/block-editor.bdd.md +615 -0
- package/tests/cypress/e2e/_utils/selectors/block-editor.cy.ts +783 -0
- package/tests/cypress/e2e/_utils/selectors/dashboard-container.cy.ts +52 -0
- package/tests/cypress/e2e/_utils/selectors/dashboard-mobile.bdd.md +205 -0
- package/tests/cypress/e2e/_utils/selectors/dashboard-mobile.cy.ts +137 -0
- package/tests/cypress/e2e/_utils/selectors/dashboard-navigation.bdd.md +147 -0
- package/tests/cypress/e2e/_utils/selectors/dashboard-navigation.cy.ts +114 -0
- package/tests/cypress/e2e/_utils/selectors/dashboard-sidebar.bdd.md +76 -0
- package/tests/cypress/e2e/_utils/selectors/dashboard-sidebar.cy.ts +68 -0
- package/tests/cypress/e2e/_utils/selectors/dashboard-topnav.bdd.md +326 -0
- package/tests/cypress/e2e/_utils/selectors/dashboard-topnav.cy.ts +177 -0
- package/tests/cypress/e2e/_utils/selectors/devtools.bdd.md +306 -0
- package/tests/cypress/e2e/_utils/selectors/devtools.cy.ts +273 -0
- package/tests/cypress/e2e/_utils/selectors/global-search.bdd.md +115 -0
- package/tests/cypress/e2e/_utils/selectors/global-search.cy.ts +93 -0
- package/tests/cypress/e2e/_utils/selectors/patterns.bdd.md +388 -0
- package/tests/cypress/e2e/_utils/selectors/patterns.cy.ts +559 -0
- package/tests/cypress/e2e/_utils/selectors/public.cy.ts +112 -0
- package/tests/cypress/e2e/_utils/selectors/settings-api-keys.bdd.md +266 -0
- package/tests/cypress/e2e/_utils/selectors/settings-api-keys.cy.ts +233 -0
- package/tests/cypress/e2e/_utils/selectors/settings-billing.bdd.md +78 -0
- package/tests/cypress/e2e/_utils/selectors/settings-billing.cy.ts +108 -0
- package/tests/cypress/e2e/_utils/selectors/settings-layout.bdd.md +129 -0
- package/tests/cypress/e2e/_utils/selectors/settings-layout.cy.ts +115 -0
- package/tests/cypress/e2e/_utils/selectors/settings-password.bdd.md +82 -0
- package/tests/cypress/e2e/_utils/selectors/settings-password.cy.ts +74 -0
- package/tests/cypress/e2e/_utils/selectors/settings-profile.bdd.md +77 -0
- package/tests/cypress/e2e/_utils/selectors/settings-profile.cy.ts +79 -0
- package/tests/cypress/e2e/_utils/selectors/settings-teams.bdd.md +130 -0
- package/tests/cypress/e2e/_utils/selectors/settings-teams.cy.ts +86 -0
- package/tests/cypress/e2e/_utils/selectors/superadmin.bdd.md +261 -0
- package/tests/cypress/e2e/_utils/selectors/superadmin.cy.ts +193 -0
- package/tests/cypress/e2e/_utils/selectors/tasks.bdd.md +593 -0
- package/tests/cypress/e2e/_utils/selectors/tasks.cy.ts +864 -0
- package/tests/cypress/e2e/_utils/selectors/taxonomies.cy.ts +126 -0
- package/tests/cypress/e2e/_utils/selectors/teams.bdd.md +278 -0
- package/tests/cypress/e2e/_utils/selectors/teams.cy.ts +195 -0
- package/tests/cypress/e2e/_utils/superadmin/all-teams.bdd.md +261 -0
- package/tests/cypress/e2e/_utils/superadmin/all-teams.cy.ts +177 -0
- package/tests/cypress/e2e/_utils/superadmin/all-users.bdd.md +406 -0
- package/tests/cypress/e2e/_utils/superadmin/all-users.cy.ts +294 -0
- package/tests/cypress/e2e/_utils/superadmin/dashboard.bdd.md +235 -0
- package/tests/cypress/e2e/_utils/superadmin/dashboard.cy.ts +149 -0
- package/tests/cypress/e2e/_utils/superadmin/subscriptions-overview.bdd.md +290 -0
- package/tests/cypress/e2e/_utils/superadmin/subscriptions-overview.cy.ts +194 -0
- package/tests/cypress/e2e/ai/ai-usage.cy.ts +209 -0
- package/tests/cypress/e2e/ai/chat-api.cy.ts +119 -0
- package/tests/cypress/e2e/ai/guardrails.cy.ts +332 -0
- package/tests/cypress/e2e/api/_core/billing/BillingAPIController.js +319 -0
- package/tests/cypress/e2e/api/_core/billing/check-action.cy.ts +326 -0
- package/tests/cypress/e2e/api/_core/billing/checkout.cy.ts +358 -0
- package/tests/cypress/e2e/api/_core/billing/lifecycle.cy.ts +423 -0
- package/tests/cypress/e2e/api/_core/billing/plans/README.md +345 -0
- package/tests/cypress/e2e/api/_core/billing/plans/business.cy.ts +412 -0
- package/tests/cypress/e2e/api/_core/billing/plans/downgrade.cy.ts +510 -0
- package/tests/cypress/e2e/api/_core/billing/plans/fixtures/billing-plans.json +163 -0
- package/tests/cypress/e2e/api/_core/billing/plans/free.cy.ts +500 -0
- package/tests/cypress/e2e/api/_core/billing/plans/pro.cy.ts +497 -0
- package/tests/cypress/e2e/api/_core/billing/plans/starter.cy.ts +342 -0
- package/tests/cypress/e2e/api/_core/billing/portal.cy.ts +313 -0
- package/tests/cypress/e2e/api/_core/devtools/registries.bdd.md +300 -0
- package/tests/cypress/e2e/api/_core/devtools/registries.cy.ts +368 -0
- package/tests/cypress/e2e/api/_core/scheduled-actions/cron-endpoint.bdd.md +375 -0
- package/tests/cypress/e2e/api/_core/scheduled-actions/cron-endpoint.cy.ts +346 -0
- package/tests/cypress/e2e/api/_core/scheduled-actions/devtools-endpoint.bdd.md +451 -0
- package/tests/cypress/e2e/api/_core/scheduled-actions/devtools-endpoint.cy.ts +447 -0
- package/tests/cypress/e2e/api/_core/scheduled-actions/scheduling.bdd.md +649 -0
- package/tests/cypress/e2e/api/_core/scheduled-actions/scheduling.cy.ts +333 -0
- package/tests/cypress/e2e/api/_core/security/security-headers.cy.ts +601 -0
- package/tests/cypress/e2e/api/_core/settings/api-keys.crud.cy.ts +923 -0
- package/tests/cypress/e2e/api/_core/teams/teams-security.cy.ts +415 -0
- package/tests/cypress/e2e/api/_core/users/users-crud.cy.ts +469 -0
- package/tests/cypress/e2e/api/_core/users/users-metas.cy.ts +913 -0
- package/tests/cypress/e2e/api/_core/users/users-security.cy.ts +375 -0
- package/tests/cypress/e2e/api/entities/customers/customers-crud.cy.ts +648 -0
- package/tests/cypress/e2e/api/entities/customers/customers-metas.cy.ts +839 -0
- package/tests/cypress/e2e/api/entities/media/media-crud.cy.ts +600 -0
- package/tests/cypress/e2e/api/entities/media/media-role-permissions.cy.ts +617 -0
- package/tests/cypress/e2e/api/entities/media/media-team-isolation.cy.ts +464 -0
- package/tests/cypress/e2e/api/entities/pages/blocks-scope.cy.ts +396 -0
- package/tests/cypress/e2e/api/entities/pages/pages-crud.cy.ts +425 -0
- package/tests/cypress/e2e/api/entities/pages/pages-status.cy.ts +335 -0
- package/tests/cypress/e2e/api/entities/posts/post-categories-crud.cy.ts +610 -0
- package/tests/cypress/e2e/api/entities/posts/posts-crud.cy.ts +709 -0
- package/tests/cypress/e2e/api/entities/posts/posts-status.cy.ts +396 -0
- package/tests/cypress/e2e/api/entities/tasks/tasks-crud.cy.ts +602 -0
- package/tests/cypress/e2e/api/entities/tasks/tasks-metas.cy.ts +878 -0
- package/tests/cypress/e2e/patterns/patterns-in-pages.cy.ts +367 -0
- package/tests/cypress/e2e/uat/_core/auth/app-roles/developer-login.bdd.md +231 -0
- package/tests/cypress/e2e/uat/_core/auth/app-roles/developer-login.cy.ts +144 -0
- package/tests/cypress/e2e/uat/_core/auth/app-roles/superadmin-login.bdd.md +118 -0
- package/tests/cypress/e2e/uat/_core/auth/app-roles/superadmin-login.cy.ts +84 -0
- package/tests/cypress/e2e/uat/_core/auth/custom-roles/editor-login.bdd.md +288 -0
- package/tests/cypress/e2e/uat/_core/auth/custom-roles/editor-login.cy.ts +188 -0
- package/tests/cypress/e2e/uat/_core/auth/login-logout.bdd.md +160 -0
- package/tests/cypress/e2e/uat/_core/auth/login-logout.cy.ts +116 -0
- package/tests/cypress/e2e/uat/_core/auth/password-reset.bdd.md +289 -0
- package/tests/cypress/e2e/uat/_core/auth/password-reset.cy.ts +200 -0
- package/tests/cypress/e2e/uat/_core/auth/registration-control-invitation.cy.ts +176 -0
- package/tests/cypress/e2e/uat/_core/auth/registration-control-open.cy.ts +131 -0
- package/tests/cypress/e2e/uat/_core/auth/registration-control.cy.ts +140 -0
- package/tests/cypress/e2e/uat/_core/auth/team-roles/admin-login.bdd.md +225 -0
- package/tests/cypress/e2e/uat/_core/auth/team-roles/admin-login.cy.ts +148 -0
- package/tests/cypress/e2e/uat/_core/auth/team-roles/member-login.bdd.md +251 -0
- package/tests/cypress/e2e/uat/_core/auth/team-roles/member-login.cy.ts +163 -0
- package/tests/cypress/e2e/uat/_core/auth/team-roles/owner-login.bdd.md +231 -0
- package/tests/cypress/e2e/uat/_core/auth/team-roles/owner-login.cy.ts +141 -0
- package/tests/cypress/e2e/uat/_core/billing/extended.bdd.md +273 -0
- package/tests/cypress/e2e/uat/_core/billing/extended.cy.ts +209 -0
- package/tests/cypress/e2e/uat/_core/billing/feature-gates.bdd.md +407 -0
- package/tests/cypress/e2e/uat/_core/billing/feature-gates.cy.ts +307 -0
- package/tests/cypress/e2e/uat/_core/billing/page.bdd.md +329 -0
- package/tests/cypress/e2e/uat/_core/billing/page.cy.ts +250 -0
- package/tests/cypress/e2e/uat/_core/billing/status.bdd.md +190 -0
- package/tests/cypress/e2e/uat/_core/billing/status.cy.ts +145 -0
- package/tests/cypress/e2e/uat/_core/billing/team-switch.bdd.md +156 -0
- package/tests/cypress/e2e/uat/_core/billing/team-switch.cy.ts +122 -0
- package/tests/cypress/e2e/uat/_core/billing/usage.bdd.md +218 -0
- package/tests/cypress/e2e/uat/_core/billing/usage.cy.ts +176 -0
- package/tests/cypress/e2e/uat/_core/blocks/hero.bdd.md +124 -0
- package/tests/cypress/e2e/uat/_core/blocks/hero.cy.ts +56 -0
- package/tests/cypress/e2e/uat/_core/devtools/api-tester.cy.ts +390 -0
- package/tests/cypress/e2e/uat/_core/performance/suspense-loading.cy.ts +134 -0
- package/tests/cypress/e2e/uat/_core/scheduled-actions/devtools-ui.bdd.md +736 -0
- package/tests/cypress/e2e/uat/_core/scheduled-actions/devtools-ui.cy.ts +740 -0
- package/tests/cypress/e2e/uat/_core/teams/inline-edit.cy.ts +278 -0
- package/tests/cypress/e2e/uat/_core/teams/roles-matrix.bdd.md +553 -0
- package/tests/cypress/e2e/uat/_core/teams/roles-matrix.cy.ts +185 -0
- package/tests/cypress/e2e/uat/_core/teams/switcher.bdd.md +1151 -0
- package/tests/cypress/e2e/uat/_core/teams/switcher.cy.ts +497 -0
- package/tests/cypress/e2e/uat/_core/teams/team-switcher.md +198 -0
- package/tests/cypress/e2e/uat/entities/customers/member.bdd.md +275 -0
- package/tests/cypress/e2e/uat/entities/customers/member.cy.ts +122 -0
- package/tests/cypress/e2e/uat/entities/customers/owner.bdd.md +243 -0
- package/tests/cypress/e2e/uat/entities/customers/owner.cy.ts +165 -0
- package/tests/cypress/e2e/uat/entities/pages/block-crud.bdd.md +476 -0
- package/tests/cypress/e2e/uat/entities/pages/block-crud.cy.ts +486 -0
- package/tests/cypress/e2e/uat/entities/pages/block-editor.bdd.md +460 -0
- package/tests/cypress/e2e/uat/entities/pages/block-editor.cy.ts +301 -0
- package/tests/cypress/e2e/uat/entities/pages/list.bdd.md +432 -0
- package/tests/cypress/e2e/uat/entities/pages/list.cy.ts +273 -0
- package/tests/cypress/e2e/uat/entities/pages/public-rendering.bdd.md +696 -0
- package/tests/cypress/e2e/uat/entities/pages/public-rendering.cy.ts +340 -0
- package/tests/cypress/e2e/uat/entities/posts/categories-api-aware.bdd.md +161 -0
- package/tests/cypress/e2e/uat/entities/posts/categories-api-aware.cy.ts +104 -0
- package/tests/cypress/e2e/uat/entities/posts/categories.bdd.md +375 -0
- package/tests/cypress/e2e/uat/entities/posts/categories.cy.ts +241 -0
- package/tests/cypress/e2e/uat/entities/posts/editor.bdd.md +429 -0
- package/tests/cypress/e2e/uat/entities/posts/editor.cy.ts +257 -0
- package/tests/cypress/e2e/uat/entities/posts/list.bdd.md +340 -0
- package/tests/cypress/e2e/uat/entities/posts/list.cy.ts +177 -0
- package/tests/cypress/e2e/uat/entities/posts/public.bdd.md +614 -0
- package/tests/cypress/e2e/uat/entities/posts/public.cy.ts +249 -0
- package/tests/cypress/e2e/uat/entities/tasks/member.bdd.md +222 -0
- package/tests/cypress/e2e/uat/entities/tasks/member.cy.ts +165 -0
- package/tests/cypress/e2e/uat/entities/tasks/owner.bdd.md +419 -0
- package/tests/cypress/e2e/uat/entities/tasks/owner.cy.ts +191 -0
- package/tests/cypress/e2e/uat/features/roles/editor-role.bdd.md +552 -0
- package/tests/cypress/e2e/uat/features/roles/editor-role.cy.ts +210 -0
- package/tests/cypress/e2e/uat/features/roles/member-restrictions.bdd.md +450 -0
- package/tests/cypress/e2e/uat/features/roles/member-restrictions.cy.ts +189 -0
- package/tests/cypress/e2e/uat/features/roles/owner-full-crud.bdd.md +530 -0
- package/tests/cypress/e2e/uat/features/roles/owner-full-crud.cy.ts +247 -0
- package/tests/cypress/fixtures/blocks.json +218 -0
- package/tests/cypress/fixtures/entities.json +87 -0
- package/tests/cypress/fixtures/page-builder.json +21 -0
- package/tests/cypress/src/components/CategoriesPOM.ts +382 -0
- package/tests/cypress/src/components/CustomersPOM.ts +439 -0
- package/tests/cypress/src/components/DevKeyringPOM.ts +160 -0
- package/tests/cypress/src/components/EntityForm.ts +375 -0
- package/tests/cypress/src/components/EntityList.ts +389 -0
- package/tests/cypress/src/components/PageBuilderPOM.ts +710 -0
- package/tests/cypress/src/components/PostEditorPOM.ts +370 -0
- package/tests/cypress/src/components/PostsListPOM.ts +223 -0
- package/tests/cypress/src/components/PublicPagePOM.ts +447 -0
- package/tests/cypress/src/components/PublicPostPOM.ts +146 -0
- package/tests/cypress/src/components/TasksPOM.ts +272 -0
- package/tests/cypress/src/components/TeamSwitcherPOM.ts +450 -0
- package/tests/cypress/src/components/index.ts +21 -0
- package/tests/cypress/src/controllers/ApiKeysAPIController.js +178 -0
- package/tests/cypress/src/controllers/BaseAPIController.js +317 -0
- package/tests/cypress/src/controllers/CustomerAPIController.js +251 -0
- package/tests/cypress/src/controllers/MediaAPIController.js +231 -0
- package/tests/cypress/src/controllers/PagesAPIController.js +226 -0
- package/tests/cypress/src/controllers/PostsAPIController.js +250 -0
- package/tests/cypress/src/controllers/TaskAPIController.js +240 -0
- package/tests/cypress/src/controllers/UsersAPIController.js +242 -0
- package/tests/cypress/src/controllers/index.js +25 -0
- package/tests/cypress/src/core/AuthPOM.ts +450 -0
- package/tests/cypress/src/core/BasePOM.ts +33 -0
- package/tests/cypress/src/core/BlockEditorBasePOM.ts +874 -0
- package/tests/cypress/src/core/DashboardEntityPOM.ts +41 -0
- package/tests/cypress/src/core/index.ts +14 -0
- package/tests/cypress/src/entities/CustomersPOM.ts +172 -0
- package/tests/cypress/src/entities/PagesPOM.ts +137 -0
- package/tests/cypress/src/entities/PatternsPOM.ts +329 -0
- package/tests/cypress/src/entities/PostsPOM.ts +137 -0
- package/tests/cypress/src/entities/TasksPOM.ts +246 -0
- package/tests/cypress/src/entities/index.ts +16 -0
- package/tests/cypress/src/features/BillingPOM.ts +385 -0
- package/tests/cypress/src/features/DashboardPOM.ts +271 -0
- package/tests/cypress/src/features/DevtoolsPOM.ts +750 -0
- package/tests/cypress/src/features/PageBuilderPOM.ts +283 -0
- package/tests/cypress/src/features/PostEditorPOM.ts +313 -0
- package/tests/cypress/src/features/ScheduledActionsPOM.ts +463 -0
- package/tests/cypress/src/features/SettingsPOM.ts +707 -0
- package/tests/cypress/src/features/SuperadminPOM.ts +851 -0
- package/tests/cypress/src/features/SuperadminTeamRolesPOM.ts +285 -0
- package/tests/cypress/src/features/index.ts +28 -0
- package/tests/cypress/src/helpers/ApiInterceptor.ts +20 -0
- package/tests/cypress/src/index.ts +101 -0
- package/tests/cypress/src/pages/dashboard/Dashboard.js +677 -0
- package/tests/cypress/src/pages/dashboard/DashboardPage.js +43 -0
- package/tests/cypress/src/pages/dashboard/DashboardStats.js +546 -0
- package/tests/cypress/src/pages/dashboard/index.js +6 -0
- package/tests/cypress/src/pages/index.js +5 -0
- package/tests/cypress/src/pages/public/FeaturesPage.js +28 -0
- package/tests/cypress/src/pages/public/LandingPage.js +69 -0
- package/tests/cypress/src/pages/public/PricingPage.js +33 -0
- package/tests/cypress/src/pages/public/index.js +6 -0
- package/tests/cypress/src/selectors.ts +46 -0
- package/tests/cypress/src/session-helpers.ts +518 -0
- package/tests/cypress/support/doc-commands.ts +260 -0
- package/tests/cypress/support/e2e.ts +90 -0
- package/tests/cypress.config.ts +178 -0
- package/tests/jest/__mocks__/@nextsparkjs/core/components/ui/badge.js +16 -0
- package/tests/jest/__mocks__/@nextsparkjs/core/lib/db.js +11 -0
- package/tests/jest/__mocks__/@nextsparkjs/registries/permissions-registry.ts +160 -0
- package/tests/jest/__mocks__/@nextsparkjs/registries/theme-registry.ts +68 -0
- package/tests/jest/__mocks__/jose.js +22 -0
- package/tests/jest/__mocks__/next/image.js +15 -0
- package/tests/jest/__mocks__/next-server.js +56 -0
- package/tests/jest/components/post-header.test.tsx +377 -0
- package/tests/jest/jest.config.cjs +154 -0
- package/tests/jest/langchain/COVERAGE.md +372 -0
- package/tests/jest/langchain/guardrails.test.ts +465 -0
- package/tests/jest/langchain/streaming.test.ts +370 -0
- package/tests/jest/langchain/token-tracker.test.ts +455 -0
- package/tests/jest/langchain/tracer-callbacks.test.ts +881 -0
- package/tests/jest/langchain/tracer.test.ts +823 -0
- package/tests/jest/services/tasks.service.test.ts +707 -0
- package/tests/jest/setup.ts +170 -0
- package/tests/jest/tsconfig.jest.json +6 -0
- package/tests/jest/validation/categories.test.ts +429 -0
- package/tests/jest/validation/posts.test.ts +546 -0
- package/tests/tsconfig.json +21 -0
- /package/docs/{02-features → public/02-features}/01-components.md +0 -0
- /package/docs/{02-features → public/02-features}/02-styling.md +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Permissions Registry for Jest tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type Permission = string
|
|
6
|
+
|
|
7
|
+
export interface ResolvedPermission {
|
|
8
|
+
id: string
|
|
9
|
+
label: string
|
|
10
|
+
description: string
|
|
11
|
+
category: string
|
|
12
|
+
roles: string[]
|
|
13
|
+
dangerous: boolean
|
|
14
|
+
source: 'core' | 'theme'
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PermissionUISection {
|
|
19
|
+
id: string
|
|
20
|
+
label: string
|
|
21
|
+
description: string
|
|
22
|
+
categories: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RolesConfig {
|
|
26
|
+
additionalRoles?: string[]
|
|
27
|
+
hierarchy?: Record<string, number>
|
|
28
|
+
displayNames?: Record<string, string>
|
|
29
|
+
descriptions?: Record<string, string>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Mock permissions data
|
|
33
|
+
export const ALL_RESOLVED_PERMISSIONS: ResolvedPermission[] = [
|
|
34
|
+
{
|
|
35
|
+
id: 'tasks.create',
|
|
36
|
+
label: 'Create tasks',
|
|
37
|
+
description: 'Can create new tasks',
|
|
38
|
+
category: 'Tasks',
|
|
39
|
+
roles: ['owner', 'admin', 'member'],
|
|
40
|
+
dangerous: false,
|
|
41
|
+
source: 'theme',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'tasks.read',
|
|
45
|
+
label: 'View tasks',
|
|
46
|
+
description: 'Can view task details',
|
|
47
|
+
category: 'Tasks',
|
|
48
|
+
roles: ['owner', 'admin', 'member'],
|
|
49
|
+
dangerous: false,
|
|
50
|
+
source: 'theme',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'tasks.update',
|
|
54
|
+
label: 'Edit tasks',
|
|
55
|
+
description: 'Can modify task information',
|
|
56
|
+
category: 'Tasks',
|
|
57
|
+
roles: ['owner', 'admin', 'member'],
|
|
58
|
+
dangerous: false,
|
|
59
|
+
source: 'theme',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'tasks.delete',
|
|
63
|
+
label: 'Delete tasks',
|
|
64
|
+
description: 'Can delete tasks',
|
|
65
|
+
category: 'Tasks',
|
|
66
|
+
roles: ['owner', 'admin'],
|
|
67
|
+
dangerous: true,
|
|
68
|
+
source: 'theme',
|
|
69
|
+
},
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
export const ALL_PERMISSIONS: Permission[] = ALL_RESOLVED_PERMISSIONS.map(p => p.id)
|
|
73
|
+
|
|
74
|
+
export const ALL_PERMISSIONS_SET = new Set(ALL_PERMISSIONS)
|
|
75
|
+
|
|
76
|
+
export const AVAILABLE_ROLES: readonly string[] = ['owner', 'admin', 'editor', 'member', 'viewer']
|
|
77
|
+
|
|
78
|
+
export const ROLE_HIERARCHY: Record<string, number> = {
|
|
79
|
+
owner: 100,
|
|
80
|
+
admin: 50,
|
|
81
|
+
editor: 30,
|
|
82
|
+
member: 10,
|
|
83
|
+
viewer: 1,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const ROLE_DISPLAY_NAMES: Record<string, string> = {
|
|
87
|
+
owner: 'common.teamRoles.owner',
|
|
88
|
+
admin: 'common.teamRoles.admin',
|
|
89
|
+
editor: 'common.teamRoles.editor',
|
|
90
|
+
member: 'common.teamRoles.member',
|
|
91
|
+
viewer: 'common.teamRoles.viewer',
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const ROLE_DESCRIPTIONS: Record<string, string> = {
|
|
95
|
+
owner: 'Full team control, cannot be removed',
|
|
96
|
+
admin: 'Manage team members and settings',
|
|
97
|
+
editor: 'Can edit content but not manage team',
|
|
98
|
+
member: 'Standard team access',
|
|
99
|
+
viewer: 'Read-only access to team resources',
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const CUSTOM_ROLES: RolesConfig = {
|
|
103
|
+
additionalRoles: ['editor'],
|
|
104
|
+
hierarchy: { editor: 30 },
|
|
105
|
+
displayNames: { editor: 'common.teamRoles.editor' },
|
|
106
|
+
descriptions: { editor: 'Can edit content but not manage team' },
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const PERMISSIONS_BY_ROLE: Record<string, Set<Permission>> = {
|
|
110
|
+
owner: new Set(ALL_PERMISSIONS),
|
|
111
|
+
admin: new Set(['tasks.create', 'tasks.read', 'tasks.update', 'tasks.delete']),
|
|
112
|
+
editor: new Set(['tasks.create', 'tasks.read', 'tasks.update']),
|
|
113
|
+
member: new Set(['tasks.create', 'tasks.read', 'tasks.update']),
|
|
114
|
+
viewer: new Set(['tasks.read']),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const ROLE_PERMISSIONS_ARRAY: Record<string, Permission[]> = {
|
|
118
|
+
owner: [...ALL_PERMISSIONS],
|
|
119
|
+
admin: ['tasks.create', 'tasks.read', 'tasks.update', 'tasks.delete'],
|
|
120
|
+
editor: ['tasks.create', 'tasks.read', 'tasks.update'],
|
|
121
|
+
member: ['tasks.create', 'tasks.read', 'tasks.update'],
|
|
122
|
+
viewer: ['tasks.read'],
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const PERMISSIONS_BY_CATEGORY: Record<string, ResolvedPermission[]> = {
|
|
126
|
+
Tasks: ALL_RESOLVED_PERMISSIONS,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const FULL_MATRIX: Record<Permission, Record<string, boolean>> = {}
|
|
130
|
+
for (const perm of ALL_RESOLVED_PERMISSIONS) {
|
|
131
|
+
FULL_MATRIX[perm.id] = {}
|
|
132
|
+
for (const role of AVAILABLE_ROLES) {
|
|
133
|
+
FULL_MATRIX[perm.id][role] = PERMISSIONS_BY_ROLE[role]?.has(perm.id) ?? false
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const UI_SECTIONS: PermissionUISection[] = [
|
|
138
|
+
{
|
|
139
|
+
id: 'entities',
|
|
140
|
+
label: 'Entities',
|
|
141
|
+
description: 'Entity-specific permissions',
|
|
142
|
+
categories: ['Tasks'],
|
|
143
|
+
},
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
export const TEAM_PERMISSIONS_RAW = []
|
|
147
|
+
export const TEAM_PERMISSIONS_BY_ROLE: Record<string, string[]> = {}
|
|
148
|
+
|
|
149
|
+
export const PERMISSIONS_METADATA = {
|
|
150
|
+
totalPermissions: ALL_PERMISSIONS.length,
|
|
151
|
+
corePermissions: 0,
|
|
152
|
+
teamPermissions: 0,
|
|
153
|
+
featurePermissions: 0,
|
|
154
|
+
entityPermissions: ALL_RESOLVED_PERMISSIONS.length,
|
|
155
|
+
customRoles: 0,
|
|
156
|
+
availableRoles: AVAILABLE_ROLES.length,
|
|
157
|
+
categories: Object.keys(PERMISSIONS_BY_CATEGORY).length,
|
|
158
|
+
generatedAt: new Date().toISOString(),
|
|
159
|
+
theme: 'default',
|
|
160
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Theme Registry for Jest tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ThemeEntity {
|
|
6
|
+
name: string
|
|
7
|
+
source: string
|
|
8
|
+
config: any
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ThemeRouteFile {
|
|
12
|
+
path: string
|
|
13
|
+
type: 'api' | 'page'
|
|
14
|
+
methods?: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ThemeRegistryEntry {
|
|
18
|
+
name: string
|
|
19
|
+
slug: string
|
|
20
|
+
displayName: string
|
|
21
|
+
description: string
|
|
22
|
+
version: string
|
|
23
|
+
entities: ThemeEntity[]
|
|
24
|
+
routes: ThemeRouteFile[]
|
|
25
|
+
middlewares: any[]
|
|
26
|
+
plugins: string[]
|
|
27
|
+
path: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const THEME_REGISTRY: Record<string, ThemeRegistryEntry> = {
|
|
31
|
+
default: {
|
|
32
|
+
name: 'default',
|
|
33
|
+
slug: 'default',
|
|
34
|
+
displayName: 'Default Theme',
|
|
35
|
+
description: 'Default theme for NextSpark',
|
|
36
|
+
version: '1.0.0',
|
|
37
|
+
entities: [
|
|
38
|
+
{ name: 'posts', source: 'default', config: { name: 'posts', label: 'Posts' } }
|
|
39
|
+
],
|
|
40
|
+
routes: [
|
|
41
|
+
{ path: '/api/v1/posts', type: 'api', methods: ['GET', 'POST'] }
|
|
42
|
+
],
|
|
43
|
+
middlewares: [],
|
|
44
|
+
plugins: [],
|
|
45
|
+
path: 'contents/themes/default',
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ThemeName = keyof typeof THEME_REGISTRY | string
|
|
50
|
+
|
|
51
|
+
export const THEME_METADATA = {
|
|
52
|
+
generated: new Date().toISOString(),
|
|
53
|
+
activeTheme: 'default',
|
|
54
|
+
totalThemes: Object.keys(THEME_REGISTRY).length,
|
|
55
|
+
totalThemeEntities: 1,
|
|
56
|
+
totalThemeRoutes: 1,
|
|
57
|
+
themes: Object.keys(THEME_REGISTRY),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Helper function for getting active theme
|
|
61
|
+
export function getActiveTheme(): ThemeRegistryEntry | undefined {
|
|
62
|
+
return THEME_REGISTRY[THEME_METADATA.activeTheme]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Helper function for getting theme by name
|
|
66
|
+
export function getTheme(name: string): ThemeRegistryEntry | undefined {
|
|
67
|
+
return THEME_REGISTRY[name]
|
|
68
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock for jose package
|
|
3
|
+
* Resolves ES module import issues in Jest tests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
jwtVerify: jest.fn(),
|
|
8
|
+
SignJWT: jest.fn().mockImplementation(() => ({
|
|
9
|
+
setProtectedHeader: jest.fn().mockReturnThis(),
|
|
10
|
+
setIssuedAt: jest.fn().mockReturnThis(),
|
|
11
|
+
setExpirationTime: jest.fn().mockReturnThis(),
|
|
12
|
+
sign: jest.fn().mockResolvedValue('mock-jwt-token')
|
|
13
|
+
})),
|
|
14
|
+
importJWK: jest.fn(),
|
|
15
|
+
generateSecret: jest.fn(),
|
|
16
|
+
createRemoteJWKSet: jest.fn(),
|
|
17
|
+
errors: {
|
|
18
|
+
JWTExpired: class JWTExpired extends Error {},
|
|
19
|
+
JWTInvalid: class JWTInvalid extends Error {},
|
|
20
|
+
JWTClaimValidationFailed: class JWTClaimValidationFailed extends Error {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock for next/image
|
|
3
|
+
*/
|
|
4
|
+
const React = require('react')
|
|
5
|
+
|
|
6
|
+
const Image = ({ src, alt, ...props }) => {
|
|
7
|
+
return React.createElement('img', {
|
|
8
|
+
src: typeof src === 'object' ? src.src : src,
|
|
9
|
+
alt: alt || '',
|
|
10
|
+
...props,
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = Image
|
|
15
|
+
module.exports.default = Image
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock for next/server
|
|
3
|
+
* Provides NextRequest and NextResponse mocks for API testing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class MockNextRequest {
|
|
7
|
+
constructor(url, options = {}) {
|
|
8
|
+
this.url = url
|
|
9
|
+
this.method = options.method || 'GET'
|
|
10
|
+
this.headers = new Map(Object.entries(options.headers || {}))
|
|
11
|
+
this._body = options.body
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async json() {
|
|
15
|
+
if (!this._body) return {}
|
|
16
|
+
try {
|
|
17
|
+
return typeof this._body === 'string' ? JSON.parse(this._body) : this._body
|
|
18
|
+
} catch {
|
|
19
|
+
throw new SyntaxError('Invalid JSON')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async text() {
|
|
24
|
+
return this._body || ''
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class MockNextResponse {
|
|
29
|
+
constructor(body, options = {}) {
|
|
30
|
+
this.body = body
|
|
31
|
+
this.status = options.status || 200
|
|
32
|
+
this.statusText = options.statusText || 'OK'
|
|
33
|
+
this.headers = new Map(Object.entries(options.headers || {}))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async json() {
|
|
37
|
+
if (!this.body) return {}
|
|
38
|
+
try {
|
|
39
|
+
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body
|
|
40
|
+
} catch {
|
|
41
|
+
throw new SyntaxError('Invalid JSON')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static json(data, options = {}) {
|
|
46
|
+
return new MockNextResponse(data, {
|
|
47
|
+
...options,
|
|
48
|
+
headers: { 'Content-Type': 'application/json', ...options.headers }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
NextRequest: MockNextRequest,
|
|
55
|
+
NextResponse: MockNextResponse
|
|
56
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests - PostHeader Component
|
|
3
|
+
*
|
|
4
|
+
* Tests PostHeader component logic and rendering:
|
|
5
|
+
* - Renders title, excerpt, and metadata
|
|
6
|
+
* - Handles optional fields (featuredImage, excerpt, categories)
|
|
7
|
+
* - Displays category badges with colors
|
|
8
|
+
* - Formats dates correctly
|
|
9
|
+
* - data-cy attribute presence
|
|
10
|
+
*
|
|
11
|
+
* Focus on component behavior WITHOUT full E2E interaction.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React from 'react'
|
|
15
|
+
import { render, screen } from '@testing-library/react'
|
|
16
|
+
import { PostHeader } from '@/themes/default/entities/posts/components/post-header'
|
|
17
|
+
|
|
18
|
+
describe('PostHeader Component', () => {
|
|
19
|
+
const mockPost = {
|
|
20
|
+
title: 'Test Post Title',
|
|
21
|
+
excerpt: 'This is a test excerpt for the post',
|
|
22
|
+
featuredImage: 'https://example.com/image.jpg',
|
|
23
|
+
createdAt: '2024-12-16T10:00:00.000Z',
|
|
24
|
+
categories: [
|
|
25
|
+
{ id: 'cat-1', name: 'Technology', color: '#3B82F6' },
|
|
26
|
+
{ id: 'cat-2', name: 'Tutorials', color: '#10B981' }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('Rendering - Complete Post', () => {
|
|
31
|
+
it('should render post title', () => {
|
|
32
|
+
render(<PostHeader post={mockPost} />)
|
|
33
|
+
|
|
34
|
+
const title = screen.getByText('Test Post Title')
|
|
35
|
+
expect(title).toBeInTheDocument()
|
|
36
|
+
expect(title).toHaveAttribute('data-cy', 'post-title')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should render post excerpt', () => {
|
|
40
|
+
render(<PostHeader post={mockPost} />)
|
|
41
|
+
|
|
42
|
+
const excerpt = screen.getByText('This is a test excerpt for the post')
|
|
43
|
+
expect(excerpt).toBeInTheDocument()
|
|
44
|
+
expect(excerpt).toHaveAttribute('data-cy', 'post-excerpt')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should render featured image', () => {
|
|
48
|
+
render(<PostHeader post={mockPost} />)
|
|
49
|
+
|
|
50
|
+
const image = screen.getByAltText('Test Post Title')
|
|
51
|
+
expect(image).toBeInTheDocument()
|
|
52
|
+
expect(image).toHaveAttribute('src', expect.stringContaining('image.jpg'))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should render all categories', () => {
|
|
56
|
+
render(<PostHeader post={mockPost} />)
|
|
57
|
+
|
|
58
|
+
expect(screen.getByText('Technology')).toBeInTheDocument()
|
|
59
|
+
expect(screen.getByText('Tutorials')).toBeInTheDocument()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should render formatted date', () => {
|
|
63
|
+
render(<PostHeader post={mockPost} />)
|
|
64
|
+
|
|
65
|
+
// Date should be formatted as "December 16, 2024"
|
|
66
|
+
const dateElement = screen.getByText(/December 16, 2024/i)
|
|
67
|
+
expect(dateElement).toBeInTheDocument()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should have main container with data-cy', () => {
|
|
71
|
+
const { container } = render(<PostHeader post={mockPost} />)
|
|
72
|
+
|
|
73
|
+
const header = container.querySelector('[data-cy="post-header"]')
|
|
74
|
+
expect(header).toBeInTheDocument()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('Rendering - Optional Fields', () => {
|
|
79
|
+
it('should not render featured image when missing', () => {
|
|
80
|
+
const postWithoutImage = {
|
|
81
|
+
...mockPost,
|
|
82
|
+
featuredImage: undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { container } = render(<PostHeader post={postWithoutImage} />)
|
|
86
|
+
|
|
87
|
+
const imageContainer = container.querySelector('[data-cy="post-featured-image-display"]')
|
|
88
|
+
expect(imageContainer).not.toBeInTheDocument()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should not render excerpt when missing', () => {
|
|
92
|
+
const postWithoutExcerpt = {
|
|
93
|
+
...mockPost,
|
|
94
|
+
excerpt: undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render(<PostHeader post={postWithoutExcerpt} />)
|
|
98
|
+
|
|
99
|
+
const excerpt = screen.queryByTestId('post-excerpt')
|
|
100
|
+
expect(excerpt).not.toBeInTheDocument()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should not render categories section when empty', () => {
|
|
104
|
+
const postWithoutCategories = {
|
|
105
|
+
...mockPost,
|
|
106
|
+
categories: []
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { container } = render(<PostHeader post={postWithoutCategories} />)
|
|
110
|
+
|
|
111
|
+
const categoriesContainer = container.querySelector('[data-cy="post-categories-display"]')
|
|
112
|
+
expect(categoriesContainer).not.toBeInTheDocument()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should not render categories section when undefined', () => {
|
|
116
|
+
const postWithoutCategories = {
|
|
117
|
+
...mockPost,
|
|
118
|
+
categories: undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { container } = render(<PostHeader post={postWithoutCategories} />)
|
|
122
|
+
|
|
123
|
+
const categoriesContainer = container.querySelector('[data-cy="post-categories-display"]')
|
|
124
|
+
expect(categoriesContainer).not.toBeInTheDocument()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('Category Badges', () => {
|
|
129
|
+
it('should render category badge with correct color', () => {
|
|
130
|
+
render(<PostHeader post={mockPost} />)
|
|
131
|
+
|
|
132
|
+
const techBadge = screen.getByText('Technology')
|
|
133
|
+
|
|
134
|
+
// Badge should have inline styles with the color
|
|
135
|
+
const parentBadge = techBadge.closest('[style]')
|
|
136
|
+
expect(parentBadge).toHaveStyle({
|
|
137
|
+
borderColor: '#3B82F6',
|
|
138
|
+
color: '#3B82F6'
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should render category badge with background color opacity', () => {
|
|
143
|
+
render(<PostHeader post={mockPost} />)
|
|
144
|
+
|
|
145
|
+
const techBadge = screen.getByText('Technology')
|
|
146
|
+
const parentBadge = techBadge.closest('[style]')
|
|
147
|
+
|
|
148
|
+
// backgroundColor should be color + 20 (hex opacity)
|
|
149
|
+
expect(parentBadge).toHaveStyle({
|
|
150
|
+
backgroundColor: '#3B82F620'
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should handle categories without color', () => {
|
|
155
|
+
const postWithNoColorCategory = {
|
|
156
|
+
...mockPost,
|
|
157
|
+
categories: [
|
|
158
|
+
{ id: 'cat-1', name: 'No Color Category' }
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
render(<PostHeader post={postWithNoColorCategory} />)
|
|
163
|
+
|
|
164
|
+
const badge = screen.getByText('No Color Category')
|
|
165
|
+
expect(badge).toBeInTheDocument()
|
|
166
|
+
|
|
167
|
+
// Badge should still render without inline color styles
|
|
168
|
+
const parentBadge = badge.closest('[style]')
|
|
169
|
+
// If no color, styles will be undefined or not include custom colors
|
|
170
|
+
if (parentBadge) {
|
|
171
|
+
const style = parentBadge.getAttribute('style')
|
|
172
|
+
expect(style).not.toContain('borderColor')
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should render multiple category badges', () => {
|
|
177
|
+
const postWithManyCategories = {
|
|
178
|
+
...mockPost,
|
|
179
|
+
categories: [
|
|
180
|
+
{ id: 'cat-1', name: 'Category 1', color: '#FF0000' },
|
|
181
|
+
{ id: 'cat-2', name: 'Category 2', color: '#00FF00' },
|
|
182
|
+
{ id: 'cat-3', name: 'Category 3', color: '#0000FF' }
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
render(<PostHeader post={postWithManyCategories} />)
|
|
187
|
+
|
|
188
|
+
expect(screen.getByText('Category 1')).toBeInTheDocument()
|
|
189
|
+
expect(screen.getByText('Category 2')).toBeInTheDocument()
|
|
190
|
+
expect(screen.getByText('Category 3')).toBeInTheDocument()
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe('Date Formatting', () => {
|
|
195
|
+
it('should format date correctly for different dates', () => {
|
|
196
|
+
const testDates = [
|
|
197
|
+
{ input: '2024-01-01T00:00:00.000Z', expected: 'January 1, 2024' },
|
|
198
|
+
{ input: '2024-06-15T12:30:00.000Z', expected: 'June 15, 2024' },
|
|
199
|
+
{ input: '2024-12-31T23:59:59.000Z', expected: 'December 31, 2024' }
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
testDates.forEach(({ input, expected }) => {
|
|
203
|
+
const post = { ...mockPost, createdAt: input }
|
|
204
|
+
const { unmount } = render(<PostHeader post={post} />)
|
|
205
|
+
|
|
206
|
+
const dateElement = screen.getByText(new RegExp(expected, 'i'))
|
|
207
|
+
expect(dateElement).toBeInTheDocument()
|
|
208
|
+
|
|
209
|
+
unmount()
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should include dateTime attribute on time element', () => {
|
|
214
|
+
render(<PostHeader post={mockPost} />)
|
|
215
|
+
|
|
216
|
+
const timeElement = screen.getByText(/December 16, 2024/i).closest('time')
|
|
217
|
+
expect(timeElement).toHaveAttribute('dateTime', '2024-12-16T10:00:00.000Z')
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe('Data-cy Attributes', () => {
|
|
222
|
+
it('should have data-cy on post header container', () => {
|
|
223
|
+
const { container } = render(<PostHeader post={mockPost} />)
|
|
224
|
+
|
|
225
|
+
const header = container.querySelector('[data-cy="post-header"]')
|
|
226
|
+
expect(header).toBeInTheDocument()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should have data-cy on featured image container', () => {
|
|
230
|
+
const { container } = render(<PostHeader post={mockPost} />)
|
|
231
|
+
|
|
232
|
+
const imageContainer = container.querySelector('[data-cy="post-featured-image-display"]')
|
|
233
|
+
expect(imageContainer).toBeInTheDocument()
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should have data-cy on categories display', () => {
|
|
237
|
+
const { container } = render(<PostHeader post={mockPost} />)
|
|
238
|
+
|
|
239
|
+
const categoriesDisplay = container.querySelector('[data-cy="post-categories-display"]')
|
|
240
|
+
expect(categoriesDisplay).toBeInTheDocument()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('should have data-cy on post title', () => {
|
|
244
|
+
const { container } = render(<PostHeader post={mockPost} />)
|
|
245
|
+
|
|
246
|
+
const title = container.querySelector('[data-cy="post-title"]')
|
|
247
|
+
expect(title).toBeInTheDocument()
|
|
248
|
+
expect(title?.textContent).toBe('Test Post Title')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should have data-cy on post excerpt', () => {
|
|
252
|
+
const { container } = render(<PostHeader post={mockPost} />)
|
|
253
|
+
|
|
254
|
+
const excerpt = container.querySelector('[data-cy="post-excerpt"]')
|
|
255
|
+
expect(excerpt).toBeInTheDocument()
|
|
256
|
+
expect(excerpt?.textContent).toBe('This is a test excerpt for the post')
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe('Edge Cases', () => {
|
|
261
|
+
it('should handle empty title', () => {
|
|
262
|
+
const postWithEmptyTitle = {
|
|
263
|
+
...mockPost,
|
|
264
|
+
title: ''
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { container } = render(<PostHeader post={postWithEmptyTitle} />)
|
|
268
|
+
|
|
269
|
+
// Component should still render without crashing
|
|
270
|
+
const header = container.querySelector('[data-cy="post-header"]')
|
|
271
|
+
expect(header).toBeInTheDocument()
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should handle very long title', () => {
|
|
275
|
+
const longTitle = 'A'.repeat(500)
|
|
276
|
+
const postWithLongTitle = {
|
|
277
|
+
...mockPost,
|
|
278
|
+
title: longTitle
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
render(<PostHeader post={postWithLongTitle} />)
|
|
282
|
+
|
|
283
|
+
const title = screen.getByText(longTitle)
|
|
284
|
+
expect(title).toBeInTheDocument()
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('should handle very long excerpt', () => {
|
|
288
|
+
const longExcerpt = 'B'.repeat(1000)
|
|
289
|
+
const postWithLongExcerpt = {
|
|
290
|
+
...mockPost,
|
|
291
|
+
excerpt: longExcerpt
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
render(<PostHeader post={postWithLongExcerpt} />)
|
|
295
|
+
|
|
296
|
+
const excerpt = screen.getByText(longExcerpt)
|
|
297
|
+
expect(excerpt).toBeInTheDocument()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('should throw error for invalid date', () => {
|
|
301
|
+
const postWithInvalidDate = {
|
|
302
|
+
...mockPost,
|
|
303
|
+
createdAt: 'invalid-date'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Component throws RangeError when date is invalid (toISOString fails)
|
|
307
|
+
expect(() => render(<PostHeader post={postWithInvalidDate} />)).toThrow(RangeError)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('should handle categories with very long names', () => {
|
|
311
|
+
const postWithLongCategoryName = {
|
|
312
|
+
...mockPost,
|
|
313
|
+
categories: [
|
|
314
|
+
{ id: 'cat-1', name: 'C'.repeat(100), color: '#FF0000' }
|
|
315
|
+
]
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
render(<PostHeader post={postWithLongCategoryName} />)
|
|
319
|
+
|
|
320
|
+
const categoryBadge = screen.getByText('C'.repeat(100))
|
|
321
|
+
expect(categoryBadge).toBeInTheDocument()
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('should handle special characters in title', () => {
|
|
325
|
+
const postWithSpecialChars = {
|
|
326
|
+
...mockPost,
|
|
327
|
+
title: 'Test <Title> & "Quotes" & Special ©haracters'
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
render(<PostHeader post={postWithSpecialChars} />)
|
|
331
|
+
|
|
332
|
+
const title = screen.getByText(/Test <Title> & "Quotes" & Special ©haracters/)
|
|
333
|
+
expect(title).toBeInTheDocument()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('should handle missing category id', () => {
|
|
337
|
+
const postWithInvalidCategory = {
|
|
338
|
+
...mockPost,
|
|
339
|
+
categories: [
|
|
340
|
+
{ id: '', name: 'Invalid Category', color: '#FF0000' }
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
render(<PostHeader post={postWithInvalidCategory} />)
|
|
345
|
+
|
|
346
|
+
// Should still render category name
|
|
347
|
+
expect(screen.getByText('Invalid Category')).toBeInTheDocument()
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
describe('Component Structure', () => {
|
|
352
|
+
it('should render in correct order: image -> categories -> title -> excerpt -> date', () => {
|
|
353
|
+
const { container } = render(<PostHeader post={mockPost} />)
|
|
354
|
+
|
|
355
|
+
const elements = container.querySelectorAll('[data-cy]')
|
|
356
|
+
const order = Array.from(elements).map(el => el.getAttribute('data-cy'))
|
|
357
|
+
|
|
358
|
+
// Expected order based on component structure
|
|
359
|
+
expect(order).toContain('post-featured-image-display')
|
|
360
|
+
expect(order).toContain('post-categories-display')
|
|
361
|
+
expect(order).toContain('post-title')
|
|
362
|
+
expect(order).toContain('post-excerpt')
|
|
363
|
+
|
|
364
|
+
// Featured image should come before title
|
|
365
|
+
const imageIndex = order.indexOf('post-featured-image-display')
|
|
366
|
+
const titleIndex = order.indexOf('post-title')
|
|
367
|
+
expect(imageIndex).toBeLessThan(titleIndex)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('should render separator (hr) at the bottom', () => {
|
|
371
|
+
const { container } = render(<PostHeader post={mockPost} />)
|
|
372
|
+
|
|
373
|
+
const separator = container.querySelector('hr')
|
|
374
|
+
expect(separator).toBeInTheDocument()
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
})
|