@wopr-network/platform-ui-core 1.0.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/.env.paperclip +18 -0
- package/.env.wopr +18 -0
- package/README.md +36 -0
- package/biome.json +52 -0
- package/next.config.ts +45 -0
- package/package.json +84 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/window.svg +1 -0
- package/src/__tests__/__snapshots__/layout-snapshots.test.tsx.snap +741 -0
- package/src/__tests__/account-page-redirect.test.tsx +73 -0
- package/src/__tests__/account-switcher.test.tsx +85 -0
- package/src/__tests__/activity-page.test.tsx +176 -0
- package/src/__tests__/add-payment-method-dialog.test.tsx +160 -0
- package/src/__tests__/admin-api.test.ts +244 -0
- package/src/__tests__/admin-gpu-api.test.ts +188 -0
- package/src/__tests__/admin-guard.test.tsx +79 -0
- package/src/__tests__/admin-marketplace-api.test.ts +179 -0
- package/src/__tests__/admin-middleware.test.ts +157 -0
- package/src/__tests__/admin-tenant-table.test.tsx +95 -0
- package/src/__tests__/affiliate-dashboard.test.tsx +178 -0
- package/src/__tests__/api-401-redirect.test.ts +78 -0
- package/src/__tests__/api-client.test.ts +316 -0
- package/src/__tests__/api-config.test.ts +89 -0
- package/src/__tests__/api-control-instance.test.ts +69 -0
- package/src/__tests__/api-fleet-resources.test.ts +52 -0
- package/src/__tests__/api-fleet-trpc.test.ts +252 -0
- package/src/__tests__/api-get-instance-config.test.ts +41 -0
- package/src/__tests__/api-null-guards.test.ts +244 -0
- package/src/__tests__/api-rename-instance.test.ts +60 -0
- package/src/__tests__/api-update-instance-config.test.ts +60 -0
- package/src/__tests__/audit-log-table-pagination.test.tsx +136 -0
- package/src/__tests__/auth-client.test.ts +87 -0
- package/src/__tests__/auth-password-reset.test.tsx +435 -0
- package/src/__tests__/auth-redirect.test.tsx +60 -0
- package/src/__tests__/auth.test.tsx +269 -0
- package/src/__tests__/auto-topup-card.test.tsx +257 -0
- package/src/__tests__/backups-tab.test.tsx +221 -0
- package/src/__tests__/billing-byok-callout.test.tsx +76 -0
- package/src/__tests__/billing-layout-nav-hidden.test.tsx +47 -0
- package/src/__tests__/billing-payment-org-invoices.test.tsx +123 -0
- package/src/__tests__/billing.test.tsx +509 -0
- package/src/__tests__/bot-settings/resources-tab.test.tsx +119 -0
- package/src/__tests__/bot-settings/storage-tab.test.tsx +80 -0
- package/src/__tests__/bot-settings/vps-info-panel.test.tsx +108 -0
- package/src/__tests__/bot-settings/vps-upgrade-card.test.tsx +52 -0
- package/src/__tests__/bot-settings-data-control.test.ts +49 -0
- package/src/__tests__/bot-settings-restart.test.tsx +149 -0
- package/src/__tests__/bot-settings.test.tsx +678 -0
- package/src/__tests__/brand.test.ts +335 -0
- package/src/__tests__/buy-credits-panel.test.tsx +249 -0
- package/src/__tests__/buy-crypto-credits-panel.test.tsx +178 -0
- package/src/__tests__/capability-conflicts.test.ts +88 -0
- package/src/__tests__/capability-resolver.test.tsx +173 -0
- package/src/__tests__/changeset-detail.test.tsx +156 -0
- package/src/__tests__/channel-setup-logger.test.ts +22 -0
- package/src/__tests__/channel-setup-toast.test.tsx +60 -0
- package/src/__tests__/channel-wizard.test.tsx +505 -0
- package/src/__tests__/chat/ambient-dot.test.tsx +35 -0
- package/src/__tests__/chat/chat-input.test.tsx +78 -0
- package/src/__tests__/chat/chat-message.test.tsx +45 -0
- package/src/__tests__/chat/chat-panel.test.tsx +111 -0
- package/src/__tests__/chat/chat-widget.test.tsx +82 -0
- package/src/__tests__/chat-store.test.ts +87 -0
- package/src/__tests__/command-center.test.tsx +246 -0
- package/src/__tests__/compliance-retention-edit.test.tsx +134 -0
- package/src/__tests__/coupon-input.test.tsx +119 -0
- package/src/__tests__/create-instance.test.tsx +96 -0
- package/src/__tests__/create-org-wizard.test.tsx +200 -0
- package/src/__tests__/credit-balance.test.tsx +103 -0
- package/src/__tests__/credits.test.tsx +376 -0
- package/src/__tests__/csrf-middleware.test.ts +198 -0
- package/src/__tests__/degraded-state-banner.test.tsx +130 -0
- package/src/__tests__/dividend-calculator.test.tsx +20 -0
- package/src/__tests__/dividend-stats.test.tsx +64 -0
- package/src/__tests__/dividend.test.tsx +169 -0
- package/src/__tests__/dockerfile.test.ts +110 -0
- package/src/__tests__/email-verification-banner.test.tsx +64 -0
- package/src/__tests__/env-example.test.ts +25 -0
- package/src/__tests__/error-boundaries.test.tsx +64 -0
- package/src/__tests__/fetch-pricing.test.ts +121 -0
- package/src/__tests__/field-oauth.test.tsx +302 -0
- package/src/__tests__/fixtures/mock-manifests-data.js +372 -0
- package/src/__tests__/fixtures/mock-manifests.ts +24 -0
- package/src/__tests__/fleet-health-timestamp.test.tsx +101 -0
- package/src/__tests__/fleet-health-update.test.tsx +83 -0
- package/src/__tests__/format-credit.test.ts +58 -0
- package/src/__tests__/gpu-dashboard.test.tsx +236 -0
- package/src/__tests__/hosted-usage-date-range.test.tsx +54 -0
- package/src/__tests__/instance-detail.test.tsx +571 -0
- package/src/__tests__/instance-list.test.tsx +230 -0
- package/src/__tests__/landing-hero.test.tsx +27 -0
- package/src/__tests__/landing-nav.test.tsx +24 -0
- package/src/__tests__/layout-snapshots.test.tsx +167 -0
- package/src/__tests__/logger.test.ts +54 -0
- package/src/__tests__/login-page-redirect.test.tsx +142 -0
- package/src/__tests__/manifest-validation.test.ts +126 -0
- package/src/__tests__/marketplace-admin.test.tsx +151 -0
- package/src/__tests__/marketplace.test.tsx +609 -0
- package/src/__tests__/merge-api-rates.test.ts +70 -0
- package/src/__tests__/middleware.test.ts +690 -0
- package/src/__tests__/network-page.test.tsx +100 -0
- package/src/__tests__/next-config-headers.test.ts +28 -0
- package/src/__tests__/not-found.test.tsx +26 -0
- package/src/__tests__/notifications.test.tsx +128 -0
- package/src/__tests__/oauth-buttons.test.tsx +101 -0
- package/src/__tests__/oauth-error-mapping.test.tsx +97 -0
- package/src/__tests__/observability.test.tsx +541 -0
- package/src/__tests__/onboarding-data.test.ts +363 -0
- package/src/__tests__/onboarding-page.test.tsx +113 -0
- package/src/__tests__/onboarding-store.test.ts +121 -0
- package/src/__tests__/org-billing-api.test.tsx +70 -0
- package/src/__tests__/org-billing-null-guards.test.ts +64 -0
- package/src/__tests__/org-billing-page.test.tsx +124 -0
- package/src/__tests__/plugin-definition.test.ts +43 -0
- package/src/__tests__/plugin-install-flow.test.tsx +535 -0
- package/src/__tests__/plugin-registry.test.tsx +475 -0
- package/src/__tests__/plugin-setup/setup-chat-panel.test.ts +142 -0
- package/src/__tests__/plugin-setup/use-plugin-setup-chat.test.ts +49 -0
- package/src/__tests__/plugin-tool-definitions.test.ts +51 -0
- package/src/__tests__/plugin-tool-sync.test.ts +59 -0
- package/src/__tests__/portfolio-chart.test.tsx +24 -0
- package/src/__tests__/pricing.test.tsx +107 -0
- package/src/__tests__/promotion-form.test.tsx +180 -0
- package/src/__tests__/promotions-list.test.tsx +194 -0
- package/src/__tests__/provider-key-api.test.ts +134 -0
- package/src/__tests__/resend-verification-button.test.tsx +104 -0
- package/src/__tests__/sanitize-redirect-url.test.ts +47 -0
- package/src/__tests__/secrets-audit-pagination.test.tsx +139 -0
- package/src/__tests__/settings.test.tsx +937 -0
- package/src/__tests__/setup-checklist.test.tsx +274 -0
- package/src/__tests__/setup.ts +82 -0
- package/src/__tests__/smoke.test.tsx +10 -0
- package/src/__tests__/snapshot-api.test.ts +104 -0
- package/src/__tests__/status-api.test.ts +46 -0
- package/src/__tests__/status-badge.test.tsx +33 -0
- package/src/__tests__/status-colors.test.ts +83 -0
- package/src/__tests__/status-page.test.tsx +86 -0
- package/src/__tests__/step-superpowers.test.tsx +218 -0
- package/src/__tests__/story-sections.test.tsx +24 -0
- package/src/__tests__/superpower-content-sanitize.test.tsx +87 -0
- package/src/__tests__/superpower-content.test.tsx +44 -0
- package/src/__tests__/suspension-banner.test.tsx +140 -0
- package/src/__tests__/tenant-context.test.tsx +146 -0
- package/src/__tests__/tenant-keys-api.test.ts +114 -0
- package/src/__tests__/tenant-table-pagination.test.tsx +124 -0
- package/src/__tests__/terminal-log-cleanup.test.tsx +51 -0
- package/src/__tests__/terminal-sequence.test.tsx +28 -0
- package/src/__tests__/transaction-history.test.tsx +325 -0
- package/src/__tests__/trpc-types.test.ts +102 -0
- package/src/__tests__/use-capability-meta.test.ts +161 -0
- package/src/__tests__/use-chat.test.ts +616 -0
- package/src/__tests__/use-has-org.test.ts +44 -0
- package/src/__tests__/use-image-status.test.ts +77 -0
- package/src/__tests__/use-pagination-params.test.ts +88 -0
- package/src/__tests__/use-plugin-setup-chat-stale-closure.test.ts +53 -0
- package/src/__tests__/use-webmcp.test.ts +119 -0
- package/src/__tests__/validate-elevenlabs-key.test.ts +95 -0
- package/src/__tests__/validate-redirect-url.test.ts +61 -0
- package/src/__tests__/verify-page.test.tsx +140 -0
- package/src/__tests__/verify-redirect.test.tsx +41 -0
- package/src/__tests__/verify-result-banner.test.tsx +66 -0
- package/src/__tests__/webmcp-feature-detect.test.ts +54 -0
- package/src/__tests__/webmcp-hook.test.tsx +72 -0
- package/src/__tests__/webmcp-marketplace-onboarding-tools.test.ts +185 -0
- package/src/__tests__/webmcp-register.test.ts +103 -0
- package/src/__tests__/webmcp-set-provider.test.ts +47 -0
- package/src/__tests__/webmcp-tools.test.ts +348 -0
- package/src/app/(auth)/error.tsx +72 -0
- package/src/app/(auth)/forgot-password/page.tsx +137 -0
- package/src/app/(auth)/layout.tsx +14 -0
- package/src/app/(auth)/loading.tsx +26 -0
- package/src/app/(auth)/login/page.tsx +188 -0
- package/src/app/(auth)/reset-password/page.tsx +169 -0
- package/src/app/(auth)/signup/page.tsx +309 -0
- package/src/app/(dashboard)/billing/credits/page.tsx +209 -0
- package/src/app/(dashboard)/billing/error.tsx +72 -0
- package/src/app/(dashboard)/billing/layout.tsx +73 -0
- package/src/app/(dashboard)/billing/loading.tsx +41 -0
- package/src/app/(dashboard)/billing/payment/page.tsx +639 -0
- package/src/app/(dashboard)/billing/plans/page.tsx +58 -0
- package/src/app/(dashboard)/billing/referrals/page.tsx +7 -0
- package/src/app/(dashboard)/billing/usage/hosted/page.tsx +348 -0
- package/src/app/(dashboard)/billing/usage/page.tsx +663 -0
- package/src/app/(dashboard)/changesets/[id]/changeset-detail-client.tsx +400 -0
- package/src/app/(dashboard)/changesets/[id]/error.tsx +57 -0
- package/src/app/(dashboard)/changesets/[id]/loading.tsx +23 -0
- package/src/app/(dashboard)/changesets/[id]/page.tsx +10 -0
- package/src/app/(dashboard)/changesets/error.tsx +72 -0
- package/src/app/(dashboard)/changesets/page.tsx +10 -0
- package/src/app/(dashboard)/chat/page.tsx +74 -0
- package/src/app/(dashboard)/dashboard/bots/[id]/settings/page.tsx +10 -0
- package/src/app/(dashboard)/dashboard/network/page.tsx +97 -0
- package/src/app/(dashboard)/dashboard/page.tsx +13 -0
- package/src/app/(dashboard)/error.tsx +72 -0
- package/src/app/(dashboard)/layout.tsx +113 -0
- package/src/app/(dashboard)/loading.tsx +27 -0
- package/src/app/(dashboard)/marketplace/[plugin]/page.tsx +548 -0
- package/src/app/(dashboard)/marketplace/error.tsx +72 -0
- package/src/app/(dashboard)/marketplace/loading.tsx +27 -0
- package/src/app/(dashboard)/marketplace/page.tsx +268 -0
- package/src/app/(dashboard)/not-found.tsx +46 -0
- package/src/app/(dashboard)/onboarding/page.tsx +267 -0
- package/src/app/(dashboard)/settings/account/page.tsx +132 -0
- package/src/app/(dashboard)/settings/activity/page.tsx +280 -0
- package/src/app/(dashboard)/settings/api-keys/page.tsx +530 -0
- package/src/app/(dashboard)/settings/brain/page.tsx +412 -0
- package/src/app/(dashboard)/settings/error.tsx +72 -0
- package/src/app/(dashboard)/settings/layout.tsx +114 -0
- package/src/app/(dashboard)/settings/loading.tsx +31 -0
- package/src/app/(dashboard)/settings/notifications/page.tsx +216 -0
- package/src/app/(dashboard)/settings/org/page.tsx +617 -0
- package/src/app/(dashboard)/settings/profile/page.tsx +510 -0
- package/src/app/(dashboard)/settings/providers/page.tsx +842 -0
- package/src/app/(dashboard)/settings/secrets/page.tsx +658 -0
- package/src/app/(dashboard)/settings/security/page.tsx +1133 -0
- package/src/app/admin/accounting/loading.tsx +32 -0
- package/src/app/admin/accounting/page.tsx +5 -0
- package/src/app/admin/affiliates/loading.tsx +32 -0
- package/src/app/admin/affiliates/page.tsx +5 -0
- package/src/app/admin/audit/loading.tsx +32 -0
- package/src/app/admin/audit/page.tsx +5 -0
- package/src/app/admin/billing-health/loading.tsx +17 -0
- package/src/app/admin/billing-health/page.tsx +10 -0
- package/src/app/admin/compliance/page.tsx +5 -0
- package/src/app/admin/error.tsx +72 -0
- package/src/app/admin/gpu/loading.tsx +38 -0
- package/src/app/admin/gpu/page.tsx +5 -0
- package/src/app/admin/incidents/page.tsx +10 -0
- package/src/app/admin/inference/loading.tsx +32 -0
- package/src/app/admin/inference/page.tsx +5 -0
- package/src/app/admin/layout.tsx +44 -0
- package/src/app/admin/loading.tsx +32 -0
- package/src/app/admin/marketplace/loading.tsx +32 -0
- package/src/app/admin/marketplace/page.tsx +5 -0
- package/src/app/admin/migrations/loading.tsx +22 -0
- package/src/app/admin/migrations/page.tsx +5 -0
- package/src/app/admin/onboarding/loading.tsx +18 -0
- package/src/app/admin/onboarding/page.tsx +5 -0
- package/src/app/admin/promotions/[id]/edit/loading.tsx +16 -0
- package/src/app/admin/promotions/[id]/edit/page.tsx +56 -0
- package/src/app/admin/promotions/[id]/loading.tsx +15 -0
- package/src/app/admin/promotions/[id]/page.tsx +311 -0
- package/src/app/admin/promotions/loading.tsx +21 -0
- package/src/app/admin/promotions/new/loading.tsx +16 -0
- package/src/app/admin/promotions/new/page.tsx +12 -0
- package/src/app/admin/promotions/page.tsx +266 -0
- package/src/app/admin/rate-overrides/loading.tsx +17 -0
- package/src/app/admin/rate-overrides/page.tsx +290 -0
- package/src/app/admin/roles/loading.tsx +27 -0
- package/src/app/admin/roles/page.tsx +5 -0
- package/src/app/admin/tenants/loading.tsx +32 -0
- package/src/app/admin/tenants/page.tsx +5 -0
- package/src/app/apple-icon.tsx +32 -0
- package/src/app/auth/callback/[provider]/page.tsx +104 -0
- package/src/app/auth/verify/page.tsx +224 -0
- package/src/app/channels/error.tsx +72 -0
- package/src/app/channels/loading.tsx +29 -0
- package/src/app/channels/page.tsx +262 -0
- package/src/app/channels/setup/[plugin]/page.tsx +136 -0
- package/src/app/error.tsx +72 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/fleet/error.tsx +72 -0
- package/src/app/fleet/health/page.tsx +9 -0
- package/src/app/fleet/layout.tsx +14 -0
- package/src/app/fleet/loading.tsx +33 -0
- package/src/app/fleet/page.tsx +5 -0
- package/src/app/global-error.tsx +96 -0
- package/src/app/globals.css +251 -0
- package/src/app/icon.svg +4 -0
- package/src/app/instances/[id]/instance-detail-client.tsx +1298 -0
- package/src/app/instances/[id]/page.tsx +10 -0
- package/src/app/instances/error.tsx +72 -0
- package/src/app/instances/instance-list-client.tsx +540 -0
- package/src/app/instances/loading.tsx +33 -0
- package/src/app/instances/new/create-instance-client.tsx +377 -0
- package/src/app/instances/new/page.tsx +9 -0
- package/src/app/instances/page.tsx +9 -0
- package/src/app/layout.tsx +83 -0
- package/src/app/not-found.tsx +38 -0
- package/src/app/og/route.tsx +50 -0
- package/src/app/page.tsx +39 -0
- package/src/app/plugins/error.tsx +72 -0
- package/src/app/plugins/layout.tsx +14 -0
- package/src/app/plugins/loading.tsx +30 -0
- package/src/app/plugins/page.tsx +555 -0
- package/src/app/pricing/error.tsx +72 -0
- package/src/app/pricing/loading.tsx +25 -0
- package/src/app/pricing/page.tsx +20 -0
- package/src/app/privacy/page.tsx +406 -0
- package/src/app/robots.ts +9 -0
- package/src/app/sitemap.ts +11 -0
- package/src/app/status/error.tsx +72 -0
- package/src/app/status/loading.tsx +21 -0
- package/src/app/status/page.tsx +20 -0
- package/src/app/terms/page.tsx +414 -0
- package/src/components/account-switcher.tsx +82 -0
- package/src/components/admin/accounting-dashboard.tsx +190 -0
- package/src/components/admin/admin-guard.tsx +36 -0
- package/src/components/admin/admin-nav.tsx +71 -0
- package/src/components/admin/affiliate-dashboard.tsx +564 -0
- package/src/components/admin/audit-log-table.tsx +336 -0
- package/src/components/admin/billing-health-dashboard.test.tsx +40 -0
- package/src/components/admin/billing-health-dashboard.tsx +416 -0
- package/src/components/admin/bulk-actions-bar.test.tsx +92 -0
- package/src/components/admin/bulk-actions-bar.tsx +80 -0
- package/src/components/admin/bulk-export-dialog.test.tsx +75 -0
- package/src/components/admin/bulk-export-dialog.tsx +189 -0
- package/src/components/admin/bulk-grant-dialog.test.tsx +81 -0
- package/src/components/admin/bulk-grant-dialog.tsx +147 -0
- package/src/components/admin/bulk-preview-dialog.test.tsx +72 -0
- package/src/components/admin/bulk-preview-dialog.tsx +106 -0
- package/src/components/admin/bulk-reactivate-dialog.test.tsx +51 -0
- package/src/components/admin/bulk-reactivate-dialog.tsx +55 -0
- package/src/components/admin/bulk-select-all-banner.test.tsx +36 -0
- package/src/components/admin/bulk-select-all-banner.tsx +44 -0
- package/src/components/admin/bulk-suspend-dialog.test.tsx +77 -0
- package/src/components/admin/bulk-suspend-dialog.tsx +129 -0
- package/src/components/admin/bulk-undo-toast.test.tsx +66 -0
- package/src/components/admin/bulk-undo-toast.tsx +121 -0
- package/src/components/admin/compliance-dashboard.tsx +1341 -0
- package/src/components/admin/gpu-dashboard.tsx +552 -0
- package/src/components/admin/grant-credits-dialog.tsx +121 -0
- package/src/components/admin/incident-dashboard.test.tsx +44 -0
- package/src/components/admin/incident-dashboard.tsx +717 -0
- package/src/components/admin/inference-dashboard.tsx +415 -0
- package/src/components/admin/marketplace-admin.tsx +765 -0
- package/src/components/admin/migrations-dashboard.tsx +404 -0
- package/src/components/admin/onboarding-dashboard.tsx +422 -0
- package/src/components/admin/promotions/promotion-form.tsx +440 -0
- package/src/components/admin/roles-dashboard.tsx +278 -0
- package/src/components/admin/suspend-dialog.tsx +98 -0
- package/src/components/admin/tenant-notes-panel.tsx +134 -0
- package/src/components/admin/tenant-row-actions.tsx +78 -0
- package/src/components/admin/tenant-table.tsx +339 -0
- package/src/components/auth/auth-error.tsx +22 -0
- package/src/components/auth/auth-redirect.tsx +18 -0
- package/src/components/auth/auth-shell.tsx +25 -0
- package/src/components/auth/email-verification-banner.tsx +25 -0
- package/src/components/auth/email-verification-result-banner.tsx +70 -0
- package/src/components/auth/resend-verification-button.tsx +94 -0
- package/src/components/auth/wopr-wordmark.tsx +19 -0
- package/src/components/billing/add-payment-method-dialog.tsx +267 -0
- package/src/components/billing/affiliate-dashboard.tsx +300 -0
- package/src/components/billing/auto-topup-card.tsx +432 -0
- package/src/components/billing/buy-credits-panel.tsx +180 -0
- package/src/components/billing/buy-crypto-credits-panel.tsx +96 -0
- package/src/components/billing/byok-callout.tsx +87 -0
- package/src/components/billing/coupon-input.tsx +86 -0
- package/src/components/billing/credit-balance.tsx +79 -0
- package/src/components/billing/degraded-state-banner.tsx +95 -0
- package/src/components/billing/dividend-banner.tsx +97 -0
- package/src/components/billing/dividend-eligibility.tsx +86 -0
- package/src/components/billing/dividend-pool-stats.tsx +86 -0
- package/src/components/billing/first-dividend-dialog.tsx +109 -0
- package/src/components/billing/low-balance-banner.tsx +50 -0
- package/src/components/billing/org-billing-page.tsx +360 -0
- package/src/components/billing/suspension-banner.tsx +53 -0
- package/src/components/billing/transaction-history.tsx +239 -0
- package/src/components/bot-settings/__tests__/bot-settings-client.test.tsx +205 -0
- package/src/components/bot-settings/backups-tab.tsx +377 -0
- package/src/components/bot-settings/bot-settings-client.tsx +1712 -0
- package/src/components/bot-settings/resources-tab.tsx +203 -0
- package/src/components/bot-settings/storage-tab.tsx +248 -0
- package/src/components/bot-settings/vps-info-panel.tsx +132 -0
- package/src/components/bot-settings/vps-upgrade-card.tsx +110 -0
- package/src/components/capability/CapabilityResolver.tsx +113 -0
- package/src/components/channel-wizard/field-interactive.tsx +48 -0
- package/src/components/channel-wizard/field-oauth.tsx +181 -0
- package/src/components/channel-wizard/field-paste.tsx +47 -0
- package/src/components/channel-wizard/field-qr.tsx +302 -0
- package/src/components/channel-wizard/index.ts +6 -0
- package/src/components/channel-wizard/step-renderer.tsx +103 -0
- package/src/components/channel-wizard/wizard.tsx +200 -0
- package/src/components/chat/ambient-dot.tsx +32 -0
- package/src/components/chat/chat-input.tsx +56 -0
- package/src/components/chat/chat-message.tsx +36 -0
- package/src/components/chat/chat-panel.tsx +138 -0
- package/src/components/chat/chat-widget.tsx +41 -0
- package/src/components/chat/index.ts +5 -0
- package/src/components/dashboard/command-center.tsx +614 -0
- package/src/components/instances/friends-tab.test.tsx +265 -0
- package/src/components/instances/friends-tab.tsx +721 -0
- package/src/components/landing/hero.tsx +53 -0
- package/src/components/landing/landing-nav.tsx +21 -0
- package/src/components/landing/landing-page.tsx +71 -0
- package/src/components/landing/portfolio-chart.tsx +349 -0
- package/src/components/landing/story-sections.tsx +50 -0
- package/src/components/landing/terminal-lines.ts +99 -0
- package/src/components/landing/terminal-sequence.tsx +453 -0
- package/src/components/landing/typing-effect.tsx +43 -0
- package/src/components/marketplace/category-filter.tsx +61 -0
- package/src/components/marketplace/empty-state.tsx +61 -0
- package/src/components/marketplace/featured-heroes.tsx +84 -0
- package/src/components/marketplace/first-visit-hero.tsx +110 -0
- package/src/components/marketplace/index.ts +9 -0
- package/src/components/marketplace/install-wizard.tsx +782 -0
- package/src/components/marketplace/marketplace-tabs.tsx +54 -0
- package/src/components/marketplace/plugin-card.tsx +129 -0
- package/src/components/marketplace/superpower-card.tsx +104 -0
- package/src/components/marketplace/superpower-content.tsx +117 -0
- package/src/components/marketplace/terminal-search.tsx +67 -0
- package/src/components/oauth-buttons.tsx +75 -0
- package/src/components/observability/fleet-health.tsx +370 -0
- package/src/components/observability/health-overview.tsx +246 -0
- package/src/components/observability/logs-viewer.tsx +215 -0
- package/src/components/observability/metrics-dashboard.tsx +288 -0
- package/src/components/onboarding/fallback-setup.tsx +137 -0
- package/src/components/onboarding/index.ts +3 -0
- package/src/components/onboarding/setup-checklist.tsx +333 -0
- package/src/components/onboarding/step-superpowers.tsx +122 -0
- package/src/components/plugin-setup/index.ts +1 -0
- package/src/components/plugin-setup/setup-chat-panel.tsx +188 -0
- package/src/components/pricing/dividend-calculator.tsx +47 -0
- package/src/components/pricing/dividend-stats.tsx +117 -0
- package/src/components/pricing/pricing-page.tsx +229 -0
- package/src/components/settings/create-org-wizard.tsx +225 -0
- package/src/components/sidebar.tsx +202 -0
- package/src/components/status/status-page.tsx +209 -0
- package/src/components/status-badge.tsx +28 -0
- package/src/components/theme-provider.tsx +8 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +47 -0
- package/src/components/ui/banner.tsx +36 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/checkbox.tsx +52 -0
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/credit-detailed.tsx +33 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +228 -0
- package/src/components/ui/form.tsx +151 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/popover.tsx +74 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/select.tsx +175 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +125 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/table.tsx +92 -0
- package/src/components/ui/tabs.tsx +81 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +44 -0
- package/src/config/provider-docs.ts +17 -0
- package/src/hooks/__tests__/use-async.test.ts +127 -0
- package/src/hooks/__tests__/use-count-up.test.ts +129 -0
- package/src/hooks/__tests__/use-debounce.test.ts +105 -0
- package/src/hooks/__tests__/use-fleet-sse.test.ts +216 -0
- package/src/hooks/__tests__/use-local-storage.test.ts +74 -0
- package/src/hooks/__tests__/use-mobile.test.ts +86 -0
- package/src/hooks/__tests__/use-save-queue.test.ts +159 -0
- package/src/hooks/use-async.ts +54 -0
- package/src/hooks/use-capability-meta.ts +99 -0
- package/src/hooks/use-count-up.ts +23 -0
- package/src/hooks/use-debounce.ts +12 -0
- package/src/hooks/use-fleet-sse.ts +47 -0
- package/src/hooks/use-has-org.ts +18 -0
- package/src/hooks/use-image-status.ts +36 -0
- package/src/hooks/use-local-storage.ts +36 -0
- package/src/hooks/use-mobile.ts +17 -0
- package/src/hooks/use-page-context.ts +24 -0
- package/src/hooks/use-pagination-params.ts +30 -0
- package/src/hooks/use-plugin-registry.ts +247 -0
- package/src/hooks/use-plugin-setup-chat.ts +211 -0
- package/src/hooks/use-save-queue.ts +54 -0
- package/src/hooks/use-webmcp.ts +40 -0
- package/src/lib/__tests__/__snapshots__/pricing-data.test.ts.snap +112 -0
- package/src/lib/__tests__/admin-api.test.ts +487 -0
- package/src/lib/__tests__/api-bot-crud.test.ts +391 -0
- package/src/lib/__tests__/api-fetch.test.ts +196 -0
- package/src/lib/__tests__/bot-settings-data.test.ts +352 -0
- package/src/lib/__tests__/org-api.test.ts +281 -0
- package/src/lib/__tests__/org-billing-api.test.ts +242 -0
- package/src/lib/__tests__/pricing-data.test.ts +32 -0
- package/src/lib/__tests__/settings-api.test.ts +272 -0
- package/src/lib/admin-affiliate-api.ts +51 -0
- package/src/lib/admin-api.ts +325 -0
- package/src/lib/admin-compliance-api.ts +127 -0
- package/src/lib/admin-gpu-api.ts +82 -0
- package/src/lib/admin-incident-api.ts +121 -0
- package/src/lib/admin-inference-api.ts +47 -0
- package/src/lib/admin-marketplace-api.ts +97 -0
- package/src/lib/api-config.test.ts +111 -0
- package/src/lib/api-config.ts +65 -0
- package/src/lib/api-errors.test.ts +43 -0
- package/src/lib/api.ts +2011 -0
- package/src/lib/auth-client.ts +11 -0
- package/src/lib/bot-settings-data.ts +342 -0
- package/src/lib/brand-config.ts +145 -0
- package/src/lib/brand.ts +669 -0
- package/src/lib/changeset-api.ts +29 -0
- package/src/lib/changeset-types.ts +56 -0
- package/src/lib/channel-manifests.ts +50 -0
- package/src/lib/chat/chat-context.tsx +70 -0
- package/src/lib/chat/chat-store.ts +62 -0
- package/src/lib/chat/types.ts +35 -0
- package/src/lib/chat/use-chat.ts +255 -0
- package/src/lib/cost-comparison-data.test.ts +95 -0
- package/src/lib/cost-comparison-data.ts +54 -0
- package/src/lib/errors.test.ts +64 -0
- package/src/lib/errors.ts +52 -0
- package/src/lib/fetch-utils.test.ts +57 -0
- package/src/lib/fetch-utils.ts +25 -0
- package/src/lib/format-credit.test.ts +66 -0
- package/src/lib/format-credit.ts +24 -0
- package/src/lib/format.test.ts +62 -0
- package/src/lib/format.ts +17 -0
- package/src/lib/logger.ts +28 -0
- package/src/lib/marketplace-data.ts +346 -0
- package/src/lib/oauth-errors.ts +19 -0
- package/src/lib/onboarding-data.ts +1265 -0
- package/src/lib/onboarding-store.ts +233 -0
- package/src/lib/org-api.ts +74 -0
- package/src/lib/org-billing-api.ts +81 -0
- package/src/lib/page-prompts.test.ts +32 -0
- package/src/lib/page-prompts.ts +23 -0
- package/src/lib/plugin/index.ts +32 -0
- package/src/lib/plugin/tool-definitions.ts +306 -0
- package/src/lib/pricing-data.ts +115 -0
- package/src/lib/promotions-types.ts +58 -0
- package/src/lib/settings-api.ts +63 -0
- package/src/lib/status-colors.ts +38 -0
- package/src/lib/tenant-context.tsx +134 -0
- package/src/lib/trpc-types.ts +173 -0
- package/src/lib/trpc.tsx +86 -0
- package/src/lib/utils.test.ts +55 -0
- package/src/lib/utils.ts +18 -0
- package/src/lib/validate-redirect-url.ts +39 -0
- package/src/lib/webmcp/feature-detect.ts +13 -0
- package/src/lib/webmcp/marketplace-onboarding-tools.ts +202 -0
- package/src/lib/webmcp/register.ts +44 -0
- package/src/lib/webmcp/tools.ts +422 -0
- package/src/proxy.ts +258 -0
- package/src/types/missing-deps.d.ts +160 -0
- package/src/types/motion-dom.d.ts +162 -0
- package/src/types/vitest-matchers.d.ts +40 -0
- package/src/types/web-mcp.d.ts +22 -0
- package/tsconfig.json +34 -0
- package/vitest.config.ts +26 -0
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type {
|
|
5
|
+
BillingInfo,
|
|
6
|
+
BillingUsage,
|
|
7
|
+
CapabilitySetting,
|
|
8
|
+
CreditBalance,
|
|
9
|
+
Organization,
|
|
10
|
+
PlatformApiKey,
|
|
11
|
+
ProviderKey,
|
|
12
|
+
UserProfile,
|
|
13
|
+
} from "@/lib/api";
|
|
14
|
+
|
|
15
|
+
// Mock next/navigation
|
|
16
|
+
vi.mock("next/navigation", () => ({
|
|
17
|
+
useRouter: () => ({ push: vi.fn() }),
|
|
18
|
+
useSearchParams: () => new URLSearchParams(),
|
|
19
|
+
usePathname: () => "/settings/profile",
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Mock better-auth/react
|
|
23
|
+
vi.mock("better-auth/react", () => ({
|
|
24
|
+
createAuthClient: () => ({
|
|
25
|
+
useSession: () => ({ data: null, isPending: false, error: null }),
|
|
26
|
+
signIn: { email: vi.fn(), social: vi.fn() },
|
|
27
|
+
signUp: { email: vi.fn() },
|
|
28
|
+
signOut: vi.fn(),
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const MOCK_PROFILE: UserProfile = {
|
|
33
|
+
id: "user-001",
|
|
34
|
+
name: "Alice Johnson",
|
|
35
|
+
email: "alice@example.com",
|
|
36
|
+
avatarUrl: null,
|
|
37
|
+
oauthConnections: [
|
|
38
|
+
{ provider: "github", connected: true },
|
|
39
|
+
{ provider: "discord", connected: false },
|
|
40
|
+
{ provider: "google", connected: true },
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const MOCK_PROVIDERS: ProviderKey[] = [
|
|
45
|
+
{
|
|
46
|
+
id: "pk-1",
|
|
47
|
+
provider: "Anthropic",
|
|
48
|
+
maskedKey: "sk-ant-...a1b2",
|
|
49
|
+
status: "valid",
|
|
50
|
+
lastChecked: "2026-02-13T14:00:00Z",
|
|
51
|
+
defaultModel: "claude-sonnet-4-5-20250514",
|
|
52
|
+
models: ["claude-sonnet-4-5-20250514", "claude-opus-4-5-20250514", "claude-haiku-4-5-20250514"],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "pk-2",
|
|
56
|
+
provider: "OpenAI",
|
|
57
|
+
maskedKey: "sk-...x9y8",
|
|
58
|
+
status: "valid",
|
|
59
|
+
lastChecked: "2026-02-13T13:55:00Z",
|
|
60
|
+
defaultModel: "gpt-4o",
|
|
61
|
+
models: ["gpt-4o", "gpt-4o-mini", "o1"],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "pk-3",
|
|
65
|
+
provider: "xAI",
|
|
66
|
+
maskedKey: "",
|
|
67
|
+
status: "unchecked",
|
|
68
|
+
lastChecked: null,
|
|
69
|
+
defaultModel: null,
|
|
70
|
+
models: ["grok-2", "grok-3"],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const MOCK_API_KEYS: PlatformApiKey[] = [
|
|
75
|
+
{
|
|
76
|
+
id: "ak-1",
|
|
77
|
+
name: "CI Pipeline",
|
|
78
|
+
prefix: "platform_ci_",
|
|
79
|
+
scope: "full",
|
|
80
|
+
createdAt: "2026-01-20T10:00:00Z",
|
|
81
|
+
lastUsedAt: "2026-02-13T08:00:00Z",
|
|
82
|
+
expiresAt: "2026-04-20T10:00:00Z",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "ak-2",
|
|
86
|
+
name: "Monitoring Dashboard",
|
|
87
|
+
prefix: "platform_mon_",
|
|
88
|
+
scope: "read-only",
|
|
89
|
+
createdAt: "2026-02-01T12:00:00Z",
|
|
90
|
+
lastUsedAt: "2026-02-12T22:00:00Z",
|
|
91
|
+
expiresAt: null,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "ak-3",
|
|
95
|
+
name: "Mobile App",
|
|
96
|
+
prefix: "platform_mob_",
|
|
97
|
+
scope: "instances",
|
|
98
|
+
createdAt: "2026-02-10T09:00:00Z",
|
|
99
|
+
lastUsedAt: null,
|
|
100
|
+
expiresAt: "2026-05-10T09:00:00Z",
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const MOCK_ORG: Organization = {
|
|
105
|
+
id: "org-001",
|
|
106
|
+
name: "Acme Corp",
|
|
107
|
+
slug: "acme-corp",
|
|
108
|
+
billingEmail: "billing@acme.com",
|
|
109
|
+
members: [
|
|
110
|
+
{
|
|
111
|
+
id: "user-001",
|
|
112
|
+
userId: "user-001",
|
|
113
|
+
name: "Alice Johnson",
|
|
114
|
+
email: "alice@example.com",
|
|
115
|
+
role: "owner",
|
|
116
|
+
joinedAt: "2025-12-01T00:00:00Z",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "user-002",
|
|
120
|
+
userId: "user-002",
|
|
121
|
+
name: "Bob Smith",
|
|
122
|
+
email: "bob@example.com",
|
|
123
|
+
role: "admin",
|
|
124
|
+
joinedAt: "2026-01-15T00:00:00Z",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "user-003",
|
|
128
|
+
userId: "user-003",
|
|
129
|
+
name: "Carol Davis",
|
|
130
|
+
email: "carol@example.com",
|
|
131
|
+
role: "member",
|
|
132
|
+
joinedAt: "2026-02-01T00:00:00Z",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
invites: [],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const MOCK_CAPABILITIES: CapabilitySetting[] = [
|
|
139
|
+
{
|
|
140
|
+
capability: "transcription",
|
|
141
|
+
mode: "hosted",
|
|
142
|
+
maskedKey: null,
|
|
143
|
+
keyStatus: null,
|
|
144
|
+
provider: null,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
capability: "image-gen",
|
|
148
|
+
mode: "hosted",
|
|
149
|
+
maskedKey: null,
|
|
150
|
+
keyStatus: null,
|
|
151
|
+
provider: null,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
capability: "text-gen",
|
|
155
|
+
mode: "byok",
|
|
156
|
+
maskedKey: "sk-ant-...a1b2",
|
|
157
|
+
keyStatus: "valid",
|
|
158
|
+
provider: "Anthropic",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
capability: "embeddings",
|
|
162
|
+
mode: "hosted",
|
|
163
|
+
maskedKey: null,
|
|
164
|
+
keyStatus: null,
|
|
165
|
+
provider: null,
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const MOCK_BILLING_USAGE: BillingUsage = {
|
|
170
|
+
plan: "pro",
|
|
171
|
+
planName: "Pro",
|
|
172
|
+
billingPeriodStart: "2026-02-01T00:00:00Z",
|
|
173
|
+
billingPeriodEnd: "2026-03-01T00:00:00Z",
|
|
174
|
+
instancesRunning: 2,
|
|
175
|
+
instanceCap: 5,
|
|
176
|
+
storageUsedGb: 3.2,
|
|
177
|
+
storageCapGb: 10,
|
|
178
|
+
apiCalls: 12500,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Mock @/lib/api with test fixtures
|
|
182
|
+
vi.mock("@/lib/api", async (importOriginal) => {
|
|
183
|
+
const actual = await importOriginal<typeof import("@/lib/api")>();
|
|
184
|
+
return {
|
|
185
|
+
...actual,
|
|
186
|
+
getProfile: vi.fn().mockResolvedValue(MOCK_PROFILE),
|
|
187
|
+
updateProfile: vi.fn().mockResolvedValue(MOCK_PROFILE),
|
|
188
|
+
changePassword: vi.fn().mockResolvedValue(undefined),
|
|
189
|
+
deleteAccount: vi.fn().mockResolvedValue(undefined),
|
|
190
|
+
listProviderKeys: vi.fn().mockResolvedValue(MOCK_PROVIDERS),
|
|
191
|
+
testProviderKey: vi.fn().mockResolvedValue({ valid: true }),
|
|
192
|
+
removeProviderKey: vi.fn().mockResolvedValue(undefined),
|
|
193
|
+
saveProviderKey: vi.fn().mockResolvedValue({ ok: true, id: "test-id", provider: "openai" }),
|
|
194
|
+
updateProviderModel: vi.fn().mockResolvedValue(undefined),
|
|
195
|
+
listApiKeys: vi.fn().mockResolvedValue(MOCK_API_KEYS),
|
|
196
|
+
createApiKey: vi
|
|
197
|
+
.fn()
|
|
198
|
+
.mockResolvedValue({ key: MOCK_API_KEYS[0], secret: "platform_test_secret" }),
|
|
199
|
+
revokeApiKey: vi.fn().mockResolvedValue(undefined),
|
|
200
|
+
getBillingUsage: vi.fn().mockResolvedValue(MOCK_BILLING_USAGE),
|
|
201
|
+
createBillingPortalSession: vi
|
|
202
|
+
.fn()
|
|
203
|
+
.mockResolvedValue({ url: "https://billing.stripe.com/session/test" }),
|
|
204
|
+
storeTenantKey: vi.fn().mockResolvedValue({
|
|
205
|
+
provider: "anthropic",
|
|
206
|
+
hasKey: true,
|
|
207
|
+
maskedKey: "sk-ant-...xy",
|
|
208
|
+
createdAt: null,
|
|
209
|
+
updatedAt: null,
|
|
210
|
+
}),
|
|
211
|
+
deleteTenantKey: vi.fn().mockResolvedValue(undefined),
|
|
212
|
+
getBillingInfo: vi.fn().mockResolvedValue({
|
|
213
|
+
email: "billing@acme.com",
|
|
214
|
+
paymentMethods: [
|
|
215
|
+
{
|
|
216
|
+
id: "pm-1",
|
|
217
|
+
brand: "Visa",
|
|
218
|
+
last4: "4242",
|
|
219
|
+
expiryMonth: 12,
|
|
220
|
+
expiryYear: 2027,
|
|
221
|
+
isDefault: true,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
invoices: [],
|
|
225
|
+
} satisfies BillingInfo),
|
|
226
|
+
getCreditBalance: vi.fn().mockResolvedValue({
|
|
227
|
+
balance: 5.0,
|
|
228
|
+
dailyBurn: 0.33,
|
|
229
|
+
runway: 15,
|
|
230
|
+
} satisfies CreditBalance),
|
|
231
|
+
uploadAvatar: vi.fn().mockResolvedValue(MOCK_PROFILE),
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Mock @/lib/settings-api for capability tRPC wrappers
|
|
236
|
+
vi.mock("@/lib/settings-api", () => ({
|
|
237
|
+
getNotificationPreferences: vi.fn(),
|
|
238
|
+
updateNotificationPreferences: vi.fn(),
|
|
239
|
+
saveProviderKey: vi.fn(),
|
|
240
|
+
testProviderKey: vi.fn().mockResolvedValue({ valid: true }),
|
|
241
|
+
listCapabilities: vi.fn().mockResolvedValue(MOCK_CAPABILITIES),
|
|
242
|
+
updateCapability: vi.fn().mockResolvedValue(MOCK_CAPABILITIES[0]),
|
|
243
|
+
}));
|
|
244
|
+
|
|
245
|
+
// Mock @/hooks/use-has-org — default to hasOrg=true so existing org tests pass
|
|
246
|
+
vi.mock("@/hooks/use-has-org", () => ({
|
|
247
|
+
useHasOrg: vi.fn().mockReturnValue({ hasOrg: true, loading: false }),
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
// Mock @/lib/org-api with test fixtures
|
|
251
|
+
vi.mock("@/lib/org-api", () => ({
|
|
252
|
+
getOrganization: vi.fn().mockResolvedValue(MOCK_ORG),
|
|
253
|
+
updateOrganization: vi.fn().mockResolvedValue(MOCK_ORG),
|
|
254
|
+
inviteMember: vi.fn().mockResolvedValue({
|
|
255
|
+
id: "inv-001",
|
|
256
|
+
email: "carol@example.com",
|
|
257
|
+
role: "member",
|
|
258
|
+
invitedBy: "user-001",
|
|
259
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
260
|
+
createdAt: new Date().toISOString(),
|
|
261
|
+
}),
|
|
262
|
+
changeRole: vi.fn().mockResolvedValue(undefined),
|
|
263
|
+
revokeInvite: vi.fn().mockResolvedValue(undefined),
|
|
264
|
+
removeMember: vi.fn().mockResolvedValue(undefined),
|
|
265
|
+
transferOwnership: vi.fn().mockResolvedValue(undefined),
|
|
266
|
+
createOrganization: vi
|
|
267
|
+
.fn()
|
|
268
|
+
.mockResolvedValue({ id: "org-new", name: "Test Org", slug: "test-org" }),
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
// Mock @/lib/auth-client for OAuth account linking
|
|
272
|
+
vi.mock("@/lib/auth-client", async (importOriginal) => {
|
|
273
|
+
const actual = await importOriginal<typeof import("@/lib/auth-client")>();
|
|
274
|
+
return {
|
|
275
|
+
...actual,
|
|
276
|
+
linkSocial: vi.fn(),
|
|
277
|
+
unlinkAccount: vi.fn().mockResolvedValue({}),
|
|
278
|
+
listAccounts: vi.fn().mockResolvedValue({
|
|
279
|
+
data: [
|
|
280
|
+
{ providerId: "github", accountId: "gh-123" },
|
|
281
|
+
{ providerId: "google", accountId: "goog-456" },
|
|
282
|
+
],
|
|
283
|
+
}),
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("Profile page", () => {
|
|
288
|
+
it("renders profile heading and form fields", async () => {
|
|
289
|
+
const { default: ProfilePage } = await import("../app/(dashboard)/settings/profile/page");
|
|
290
|
+
render(<ProfilePage />);
|
|
291
|
+
|
|
292
|
+
// Initially shows skeleton loading state
|
|
293
|
+
expect(document.querySelector('[data-slot="skeleton"]')).toBeInTheDocument();
|
|
294
|
+
|
|
295
|
+
// Wait for mock data to load
|
|
296
|
+
expect(await screen.findByText("Profile")).toBeInTheDocument();
|
|
297
|
+
expect(screen.getByLabelText("Display name")).toBeInTheDocument();
|
|
298
|
+
expect(screen.getByLabelText("Email")).toBeInTheDocument();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("renders change password section", async () => {
|
|
302
|
+
const { default: ProfilePage } = await import("../app/(dashboard)/settings/profile/page");
|
|
303
|
+
render(<ProfilePage />);
|
|
304
|
+
|
|
305
|
+
expect(await screen.findByText("Change Password")).toBeInTheDocument();
|
|
306
|
+
expect(screen.getByLabelText("Current password")).toBeInTheDocument();
|
|
307
|
+
expect(screen.getByLabelText("New password")).toBeInTheDocument();
|
|
308
|
+
expect(screen.getByLabelText("Confirm new password")).toBeInTheDocument();
|
|
309
|
+
expect(screen.getByRole("button", { name: "Change password" })).toBeInTheDocument();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("renders connected accounts section", async () => {
|
|
313
|
+
const { default: ProfilePage } = await import("../app/(dashboard)/settings/profile/page");
|
|
314
|
+
render(<ProfilePage />);
|
|
315
|
+
|
|
316
|
+
expect(await screen.findByText("Connected Accounts")).toBeInTheDocument();
|
|
317
|
+
expect(screen.getByText("github")).toBeInTheDocument();
|
|
318
|
+
expect(screen.getByText("discord")).toBeInTheDocument();
|
|
319
|
+
expect(screen.getByText("google")).toBeInTheDocument();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("renders delete account section", async () => {
|
|
323
|
+
const { default: ProfilePage } = await import("../app/(dashboard)/settings/profile/page");
|
|
324
|
+
render(<ProfilePage />);
|
|
325
|
+
|
|
326
|
+
expect(await screen.findByText("Delete Account")).toBeInTheDocument();
|
|
327
|
+
expect(screen.getByRole("button", { name: "Delete account" })).toBeInTheDocument();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("renders save button", async () => {
|
|
331
|
+
const { default: ProfilePage } = await import("../app/(dashboard)/settings/profile/page");
|
|
332
|
+
render(<ProfilePage />);
|
|
333
|
+
|
|
334
|
+
expect(await screen.findByRole("button", { name: "Save changes" })).toBeInTheDocument();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe("Providers page", () => {
|
|
339
|
+
it("renders provider settings heading", async () => {
|
|
340
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
341
|
+
render(<ProvidersPage />);
|
|
342
|
+
|
|
343
|
+
// Initially shows skeleton loading state
|
|
344
|
+
expect(document.querySelector('[data-slot="skeleton"]')).toBeInTheDocument();
|
|
345
|
+
expect(await screen.findByText("Provider Settings")).toBeInTheDocument();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("renders capability toggle cards", async () => {
|
|
349
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
350
|
+
render(<ProvidersPage />);
|
|
351
|
+
|
|
352
|
+
expect(await screen.findByText("Transcription")).toBeInTheDocument();
|
|
353
|
+
expect(screen.getByText("Image Generation")).toBeInTheDocument();
|
|
354
|
+
expect(screen.getByText("Text Generation")).toBeInTheDocument();
|
|
355
|
+
expect(screen.getByText("Embeddings")).toBeInTheDocument();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("renders hosted pricing for each capability", async () => {
|
|
359
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
360
|
+
render(<ProvidersPage />);
|
|
361
|
+
|
|
362
|
+
expect(await screen.findByText("$0.006/min")).toBeInTheDocument();
|
|
363
|
+
expect(screen.getByText("$0.05/image")).toBeInTheDocument();
|
|
364
|
+
expect(screen.getByText("$0.002/1K tokens")).toBeInTheDocument();
|
|
365
|
+
expect(screen.getByText("$0.0001/1K tokens")).toBeInTheDocument();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("renders Platform Hosted and Bring Your Own Key options", async () => {
|
|
369
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
370
|
+
render(<ProvidersPage />);
|
|
371
|
+
|
|
372
|
+
const hostedLabels = await screen.findAllByText("Platform Hosted");
|
|
373
|
+
expect(hostedLabels.length).toBe(4);
|
|
374
|
+
|
|
375
|
+
const byokLabels = screen.getAllByText("Bring Your Own Key");
|
|
376
|
+
expect(byokLabels.length).toBe(4);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("shows BYOK key input for capability in byok mode", async () => {
|
|
380
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
381
|
+
render(<ProvidersPage />);
|
|
382
|
+
|
|
383
|
+
// text-gen is in BYOK mode with a masked key -- appears in both capability and provider sections
|
|
384
|
+
const maskedKeys = await screen.findAllByText("sk-ant-...a1b2");
|
|
385
|
+
expect(maskedKeys.length).toBeGreaterThanOrEqual(1);
|
|
386
|
+
// "valid" badge appears in both sections too
|
|
387
|
+
const validBadges = screen.getAllByText("valid");
|
|
388
|
+
expect(validBadges.length).toBeGreaterThanOrEqual(1);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("renders Test Key button for BYOK capability with key", async () => {
|
|
392
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
393
|
+
render(<ProvidersPage />);
|
|
394
|
+
|
|
395
|
+
expect(await screen.findByRole("button", { name: "Test Key" })).toBeInTheDocument();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("renders provider keys section", async () => {
|
|
399
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
400
|
+
render(<ProvidersPage />);
|
|
401
|
+
|
|
402
|
+
expect(await screen.findByText("Provider Keys")).toBeInTheDocument();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("renders configured providers with status", async () => {
|
|
406
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
407
|
+
render(<ProvidersPage />);
|
|
408
|
+
|
|
409
|
+
// Provider names from MOCK_PROVIDERS rendered in provider keys section
|
|
410
|
+
const anthropicElements = await screen.findAllByText("Anthropic");
|
|
411
|
+
expect(anthropicElements.length).toBeGreaterThanOrEqual(1);
|
|
412
|
+
expect(screen.getByText("OpenAI")).toBeInTheDocument();
|
|
413
|
+
expect(screen.getByText("xAI")).toBeInTheDocument();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("renders test connection and rotate buttons for configured providers", async () => {
|
|
417
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
418
|
+
render(<ProvidersPage />);
|
|
419
|
+
|
|
420
|
+
const testButtons = await screen.findAllByRole("button", { name: "Test connection" });
|
|
421
|
+
expect(testButtons.length).toBeGreaterThanOrEqual(1);
|
|
422
|
+
|
|
423
|
+
const rotateButtons = screen.getAllByRole("button", { name: "Rotate key" });
|
|
424
|
+
expect(rotateButtons.length).toBeGreaterThanOrEqual(1);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("renders add key button for unconfigured providers", async () => {
|
|
428
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
429
|
+
render(<ProvidersPage />);
|
|
430
|
+
|
|
431
|
+
expect(await screen.findByRole("button", { name: "Add key" })).toBeInTheDocument();
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe("Providers page - billing gate", () => {
|
|
436
|
+
it("shows Enable Hosted button when user has payment method", async () => {
|
|
437
|
+
const user = userEvent.setup();
|
|
438
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
439
|
+
render(<ProvidersPage />);
|
|
440
|
+
|
|
441
|
+
// Wait for capabilities to load
|
|
442
|
+
await screen.findByText("Transcription");
|
|
443
|
+
|
|
444
|
+
// text-gen is in byok mode, click hosted radio to trigger billing gate
|
|
445
|
+
const textGenCard = screen.getByTestId("capability-text-gen");
|
|
446
|
+
const hostedRadio = within(textGenCard).getByRole("radio", { name: /platform hosted/i });
|
|
447
|
+
await user.click(hostedRadio);
|
|
448
|
+
|
|
449
|
+
// Dialog opens — wait for billing check to complete
|
|
450
|
+
expect(
|
|
451
|
+
await screen.findByText(/Enable Platform Hosted for Text Generation/),
|
|
452
|
+
).toBeInTheDocument();
|
|
453
|
+
// With a payment method on file, the Enable Hosted button should appear
|
|
454
|
+
expect(await screen.findByRole("button", { name: "Enable Hosted" })).toBeInTheDocument();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("blocks activation and shows add payment prompt when no payment method or credits", async () => {
|
|
458
|
+
const api = await import("@/lib/api");
|
|
459
|
+
vi.mocked(api.getBillingInfo).mockResolvedValueOnce({
|
|
460
|
+
email: "billing@acme.com",
|
|
461
|
+
paymentMethods: [],
|
|
462
|
+
invoices: [],
|
|
463
|
+
});
|
|
464
|
+
vi.mocked(api.getCreditBalance).mockResolvedValueOnce({
|
|
465
|
+
balance: 0,
|
|
466
|
+
dailyBurn: 0,
|
|
467
|
+
runway: null,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const user = userEvent.setup();
|
|
471
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
472
|
+
render(<ProvidersPage />);
|
|
473
|
+
|
|
474
|
+
await screen.findByText("Transcription");
|
|
475
|
+
|
|
476
|
+
const textGenCard = screen.getByTestId("capability-text-gen");
|
|
477
|
+
const hostedRadio = within(textGenCard).getByRole("radio", { name: /platform hosted/i });
|
|
478
|
+
await user.click(hostedRadio);
|
|
479
|
+
|
|
480
|
+
// Dialog opens — should show payment required message instead of Enable Hosted button
|
|
481
|
+
expect(await screen.findByText(/payment method required/i)).toBeInTheDocument();
|
|
482
|
+
expect(screen.queryByRole("button", { name: "Enable Hosted" })).not.toBeInTheDocument();
|
|
483
|
+
expect(screen.getByRole("button", { name: "Add payment method" })).toBeInTheDocument();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("allows activation when user has credit balance but no payment method", async () => {
|
|
487
|
+
const api = await import("@/lib/api");
|
|
488
|
+
vi.mocked(api.getBillingInfo).mockResolvedValueOnce({
|
|
489
|
+
email: "billing@acme.com",
|
|
490
|
+
paymentMethods: [],
|
|
491
|
+
invoices: [],
|
|
492
|
+
});
|
|
493
|
+
vi.mocked(api.getCreditBalance).mockResolvedValueOnce({
|
|
494
|
+
balance: 5.0,
|
|
495
|
+
dailyBurn: 0.33,
|
|
496
|
+
runway: 15,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const user = userEvent.setup();
|
|
500
|
+
const { default: ProvidersPage } = await import("../app/(dashboard)/settings/providers/page");
|
|
501
|
+
render(<ProvidersPage />);
|
|
502
|
+
|
|
503
|
+
await screen.findByText("Transcription");
|
|
504
|
+
|
|
505
|
+
const textGenCard = screen.getByTestId("capability-text-gen");
|
|
506
|
+
const hostedRadio = within(textGenCard).getByRole("radio", { name: /platform hosted/i });
|
|
507
|
+
await user.click(hostedRadio);
|
|
508
|
+
|
|
509
|
+
// Has credit balance, so Enable Hosted should be available
|
|
510
|
+
expect(await screen.findByRole("button", { name: "Enable Hosted" })).toBeInTheDocument();
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe("API Keys page", () => {
|
|
515
|
+
it("renders API keys heading and generate button", async () => {
|
|
516
|
+
const { default: ApiKeysPage } = await import("../app/(dashboard)/settings/api-keys/page");
|
|
517
|
+
render(<ApiKeysPage />);
|
|
518
|
+
|
|
519
|
+
// Initially shows skeleton loading state (table with skeleton rows)
|
|
520
|
+
expect(document.querySelector('[data-slot="skeleton"]')).toBeInTheDocument();
|
|
521
|
+
expect(await screen.findByText("API Keys")).toBeInTheDocument();
|
|
522
|
+
expect(screen.getByRole("button", { name: "Generate new key" })).toBeInTheDocument();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("renders existing API keys in table", async () => {
|
|
526
|
+
const { default: ApiKeysPage } = await import("../app/(dashboard)/settings/api-keys/page");
|
|
527
|
+
render(<ApiKeysPage />);
|
|
528
|
+
|
|
529
|
+
expect(await screen.findByText("CI Pipeline")).toBeInTheDocument();
|
|
530
|
+
expect(screen.getByText("Monitoring Dashboard")).toBeInTheDocument();
|
|
531
|
+
expect(screen.getByText("Mobile App")).toBeInTheDocument();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("renders revoke buttons for each key", async () => {
|
|
535
|
+
const { default: ApiKeysPage } = await import("../app/(dashboard)/settings/api-keys/page");
|
|
536
|
+
render(<ApiKeysPage />);
|
|
537
|
+
|
|
538
|
+
const revokeButtons = await screen.findAllByRole("button", { name: "Revoke" });
|
|
539
|
+
expect(revokeButtons).toHaveLength(3);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("renders scope badges", async () => {
|
|
543
|
+
const { default: ApiKeysPage } = await import("../app/(dashboard)/settings/api-keys/page");
|
|
544
|
+
render(<ApiKeysPage />);
|
|
545
|
+
|
|
546
|
+
expect(await screen.findByText("full")).toBeInTheDocument();
|
|
547
|
+
expect(screen.getByText("read-only")).toBeInTheDocument();
|
|
548
|
+
expect(screen.getByText("instances")).toBeInTheDocument();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("re-fetches keys from server on revoke failure instead of restoring stale snapshot", async () => {
|
|
552
|
+
const api = await import("@/lib/api");
|
|
553
|
+
const user = userEvent.setup();
|
|
554
|
+
|
|
555
|
+
const key1: PlatformApiKey = {
|
|
556
|
+
id: "k1",
|
|
557
|
+
name: "Key One",
|
|
558
|
+
prefix: "platform_k1",
|
|
559
|
+
scope: "full",
|
|
560
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
561
|
+
lastUsedAt: null,
|
|
562
|
+
expiresAt: null,
|
|
563
|
+
};
|
|
564
|
+
const key2: PlatformApiKey = {
|
|
565
|
+
id: "k2",
|
|
566
|
+
name: "Key Two",
|
|
567
|
+
prefix: "platform_k2",
|
|
568
|
+
scope: "full",
|
|
569
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
570
|
+
lastUsedAt: null,
|
|
571
|
+
expiresAt: null,
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// First load returns both keys; after failed revoke, server returns only key2
|
|
575
|
+
vi.mocked(api.listApiKeys).mockResolvedValueOnce([key1, key2]).mockResolvedValueOnce([key2]);
|
|
576
|
+
|
|
577
|
+
// Revoke fails
|
|
578
|
+
vi.mocked(api.revokeApiKey).mockRejectedValueOnce(new Error("Server error"));
|
|
579
|
+
|
|
580
|
+
const { default: ApiKeysPage } = await import("../app/(dashboard)/settings/api-keys/page");
|
|
581
|
+
render(<ApiKeysPage />);
|
|
582
|
+
|
|
583
|
+
// Wait for initial render with both keys
|
|
584
|
+
await waitFor(() => {
|
|
585
|
+
expect(screen.getByText("Key One")).toBeInTheDocument();
|
|
586
|
+
expect(screen.getByText("Key Two")).toBeInTheDocument();
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Click revoke for Key Two (second button)
|
|
590
|
+
const revokeButtons = screen.getAllByText("Revoke");
|
|
591
|
+
await user.click(revokeButtons[1]);
|
|
592
|
+
const confirmButton = screen.getByRole("button", { name: "Revoke key" });
|
|
593
|
+
await user.click(confirmButton);
|
|
594
|
+
|
|
595
|
+
// After failure, load() re-fetches — server returns only key2
|
|
596
|
+
// Old behavior: would restore [key1, key2] (stale snapshot)
|
|
597
|
+
// New behavior: calls load() which returns [key2]
|
|
598
|
+
await waitFor(() => {
|
|
599
|
+
expect(screen.queryByText("Key One")).not.toBeInTheDocument();
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
expect(screen.getByText("Key Two")).toBeInTheDocument();
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
describe("Organization page", () => {
|
|
607
|
+
it("renders organization heading and form", async () => {
|
|
608
|
+
const { default: OrgPage } = await import("../app/(dashboard)/settings/org/page");
|
|
609
|
+
render(<OrgPage />);
|
|
610
|
+
|
|
611
|
+
// Initially shows skeleton loading state
|
|
612
|
+
expect(document.querySelector('[data-slot="skeleton"]')).toBeInTheDocument();
|
|
613
|
+
expect(await screen.findByText("Organization")).toBeInTheDocument();
|
|
614
|
+
expect(screen.getByLabelText("Organization name")).toBeInTheDocument();
|
|
615
|
+
expect(screen.getByLabelText("Billing email")).toBeInTheDocument();
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("renders members table", async () => {
|
|
619
|
+
const { default: OrgPage } = await import("../app/(dashboard)/settings/org/page");
|
|
620
|
+
render(<OrgPage />);
|
|
621
|
+
|
|
622
|
+
expect(await screen.findByText("Alice Johnson")).toBeInTheDocument();
|
|
623
|
+
expect(screen.getByText("Bob Smith")).toBeInTheDocument();
|
|
624
|
+
expect(screen.getByText("Carol Davis")).toBeInTheDocument();
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("renders member roles", async () => {
|
|
628
|
+
const { default: OrgPage } = await import("../app/(dashboard)/settings/org/page");
|
|
629
|
+
render(<OrgPage />);
|
|
630
|
+
|
|
631
|
+
expect(await screen.findByText("owner")).toBeInTheDocument();
|
|
632
|
+
expect(screen.getByText("admin")).toBeInTheDocument();
|
|
633
|
+
expect(screen.getByText("member")).toBeInTheDocument();
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it("renders invite member button", async () => {
|
|
637
|
+
const { default: OrgPage } = await import("../app/(dashboard)/settings/org/page");
|
|
638
|
+
render(<OrgPage />);
|
|
639
|
+
|
|
640
|
+
expect(await screen.findByRole("button", { name: "Invite member" })).toBeInTheDocument();
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("renders remove buttons for non-owner members", async () => {
|
|
644
|
+
const { default: OrgPage } = await import("../app/(dashboard)/settings/org/page");
|
|
645
|
+
render(<OrgPage />);
|
|
646
|
+
|
|
647
|
+
const removeButtons = await screen.findAllByRole("button", { name: "Remove" });
|
|
648
|
+
expect(removeButtons).toHaveLength(2); // Bob and Carol, not Alice (owner)
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it("renders transfer buttons for non-owner members", async () => {
|
|
652
|
+
const { default: OrgPage } = await import("../app/(dashboard)/settings/org/page");
|
|
653
|
+
render(<OrgPage />);
|
|
654
|
+
|
|
655
|
+
const transferButtons = await screen.findAllByRole("button", { name: "Transfer" });
|
|
656
|
+
expect(transferButtons).toHaveLength(2);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("renders save button for org details", async () => {
|
|
660
|
+
const { default: OrgPage } = await import("../app/(dashboard)/settings/org/page");
|
|
661
|
+
render(<OrgPage />);
|
|
662
|
+
|
|
663
|
+
expect(await screen.findByRole("button", { name: "Save changes" })).toBeInTheDocument();
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
describe("Account page", () => {
|
|
668
|
+
it("renders account heading", async () => {
|
|
669
|
+
const { default: AccountPage } = await import("../app/(dashboard)/settings/account/page");
|
|
670
|
+
render(<AccountPage />);
|
|
671
|
+
|
|
672
|
+
// Initially shows skeleton loading state
|
|
673
|
+
expect(document.querySelector('[data-slot="skeleton"]')).toBeInTheDocument();
|
|
674
|
+
expect(await screen.findByText("Account")).toBeInTheDocument();
|
|
675
|
+
expect(screen.getByText("Manage your billing settings and team")).toBeInTheDocument();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("renders current plan tier", async () => {
|
|
679
|
+
const { default: AccountPage } = await import("../app/(dashboard)/settings/account/page");
|
|
680
|
+
render(<AccountPage />);
|
|
681
|
+
|
|
682
|
+
expect(await screen.findByText("Current Plan")).toBeInTheDocument();
|
|
683
|
+
expect(screen.getByText("Pro")).toBeInTheDocument();
|
|
684
|
+
expect(screen.getByText("2 of 5 instances used")).toBeInTheDocument();
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("renders manage billing button", async () => {
|
|
688
|
+
const { default: AccountPage } = await import("../app/(dashboard)/settings/account/page");
|
|
689
|
+
render(<AccountPage />);
|
|
690
|
+
|
|
691
|
+
expect(await screen.findByRole("button", { name: "Manage Billing" })).toBeInTheDocument();
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it("does not render password change form (consolidated to profile)", async () => {
|
|
695
|
+
const { default: AccountPage } = await import("../app/(dashboard)/settings/account/page");
|
|
696
|
+
render(<AccountPage />);
|
|
697
|
+
|
|
698
|
+
await screen.findByText("Account");
|
|
699
|
+
expect(screen.queryByText("Change Password")).not.toBeInTheDocument();
|
|
700
|
+
expect(screen.queryByLabelText("Current password")).not.toBeInTheDocument();
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
describe("Settings layout", () => {
|
|
705
|
+
it("renders settings navigation links", async () => {
|
|
706
|
+
const { default: SettingsLayout } = await import("../app/(dashboard)/settings/layout");
|
|
707
|
+
render(
|
|
708
|
+
<SettingsLayout>
|
|
709
|
+
<div>child content</div>
|
|
710
|
+
</SettingsLayout>,
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
expect(screen.getByText("Profile")).toBeInTheDocument();
|
|
714
|
+
expect(screen.getByText("Account")).toBeInTheDocument();
|
|
715
|
+
expect(screen.getByText("Provider Keys")).toBeInTheDocument();
|
|
716
|
+
expect(screen.getByText("API Keys")).toBeInTheDocument();
|
|
717
|
+
expect(screen.getByText("Organization")).toBeInTheDocument();
|
|
718
|
+
expect(screen.getByText("child content")).toBeInTheDocument();
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
describe("Settings layout - org nav visibility", () => {
|
|
723
|
+
it("hides Organization nav when user has no org", async () => {
|
|
724
|
+
const { useHasOrg } = await import("@/hooks/use-has-org");
|
|
725
|
+
vi.mocked(useHasOrg).mockReturnValue({ hasOrg: false, loading: false });
|
|
726
|
+
|
|
727
|
+
const { default: SettingsLayout } = await import("../app/(dashboard)/settings/layout");
|
|
728
|
+
render(
|
|
729
|
+
<SettingsLayout>
|
|
730
|
+
<div>child</div>
|
|
731
|
+
</SettingsLayout>,
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
expect(screen.getByText("Profile")).toBeInTheDocument();
|
|
735
|
+
expect(screen.queryByText("Organization")).not.toBeInTheDocument();
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("shows Organization nav when user has an org", async () => {
|
|
739
|
+
const { useHasOrg } = await import("@/hooks/use-has-org");
|
|
740
|
+
vi.mocked(useHasOrg).mockReturnValue({ hasOrg: true, loading: false });
|
|
741
|
+
|
|
742
|
+
const { default: SettingsLayout } = await import("../app/(dashboard)/settings/layout");
|
|
743
|
+
render(
|
|
744
|
+
<SettingsLayout>
|
|
745
|
+
<div>child</div>
|
|
746
|
+
</SettingsLayout>,
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
expect(screen.getByText("Organization")).toBeInTheDocument();
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
describe("Organization page - no org redirect", () => {
|
|
754
|
+
it("redirects to /settings/profile when user has no org", async () => {
|
|
755
|
+
const orgApi = await import("@/lib/org-api");
|
|
756
|
+
vi.mocked(orgApi.getOrganization).mockRejectedValueOnce(new Error("Not found"));
|
|
757
|
+
|
|
758
|
+
// Capture the push mock from the existing useRouter mock
|
|
759
|
+
const nav = await import("next/navigation");
|
|
760
|
+
const mockPush = vi.fn();
|
|
761
|
+
const originalUseRouter = nav.useRouter;
|
|
762
|
+
vi.spyOn(nav, "useRouter").mockReturnValue({
|
|
763
|
+
...originalUseRouter(),
|
|
764
|
+
push: mockPush,
|
|
765
|
+
} as ReturnType<typeof nav.useRouter>);
|
|
766
|
+
|
|
767
|
+
const { default: OrgPage } = await import("../app/(dashboard)/settings/org/page");
|
|
768
|
+
render(<OrgPage />);
|
|
769
|
+
|
|
770
|
+
await waitFor(() => {
|
|
771
|
+
expect(mockPush).toHaveBeenCalledWith("/settings/profile");
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
vi.spyOn(nav, "useRouter").mockRestore();
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
describe("Account page — Teams & Organizations section", () => {
|
|
779
|
+
it("renders Teams & Organizations heading", async () => {
|
|
780
|
+
const { default: AccountPage } = await import("../app/(dashboard)/settings/account/page");
|
|
781
|
+
render(<AccountPage />);
|
|
782
|
+
expect(await screen.findByText("Teams & Organizations")).toBeInTheDocument();
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it("renders Create organization button", async () => {
|
|
786
|
+
const { default: AccountPage } = await import("../app/(dashboard)/settings/account/page");
|
|
787
|
+
render(<AccountPage />);
|
|
788
|
+
expect(await screen.findByRole("button", { name: /create organization/i })).toBeInTheDocument();
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
describe("CreateOrgWizard", () => {
|
|
793
|
+
beforeEach(() => {
|
|
794
|
+
vi.clearAllMocks();
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it("opens dialog and shows name input on click", async () => {
|
|
798
|
+
const { default: CreateOrgWizard } = await import("../components/settings/create-org-wizard");
|
|
799
|
+
const user = userEvent.setup();
|
|
800
|
+
render(<CreateOrgWizard />);
|
|
801
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
802
|
+
expect(screen.getByLabelText(/organization name/i)).toBeInTheDocument();
|
|
803
|
+
expect(screen.getByLabelText(/slug/i)).toBeInTheDocument();
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it("auto-generates slug from name", async () => {
|
|
807
|
+
const { default: CreateOrgWizard } = await import("../components/settings/create-org-wizard");
|
|
808
|
+
const user = userEvent.setup();
|
|
809
|
+
render(<CreateOrgWizard />);
|
|
810
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
811
|
+
await user.type(screen.getByLabelText(/organization name/i), "My Cool Team");
|
|
812
|
+
expect(screen.getByLabelText(/slug/i)).toHaveValue("my-cool-team");
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it("proceeds to confirm step and shows summary", async () => {
|
|
816
|
+
const { default: CreateOrgWizard } = await import("../components/settings/create-org-wizard");
|
|
817
|
+
const user = userEvent.setup();
|
|
818
|
+
render(<CreateOrgWizard />);
|
|
819
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
820
|
+
await user.type(screen.getByLabelText(/organization name/i), "Acme Corp");
|
|
821
|
+
await user.click(screen.getByRole("button", { name: /next/i }));
|
|
822
|
+
expect(await screen.findByText(/you.?ll be the admin/i)).toBeInTheDocument();
|
|
823
|
+
expect(screen.getByText("Acme Corp")).toBeInTheDocument();
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it("calls createOrganization on confirm and shows success", async () => {
|
|
827
|
+
const { createOrganization } = await import("@/lib/org-api");
|
|
828
|
+
vi.mocked(createOrganization).mockResolvedValueOnce({
|
|
829
|
+
id: "org-1",
|
|
830
|
+
name: "Acme Corp",
|
|
831
|
+
slug: "acme-corp",
|
|
832
|
+
});
|
|
833
|
+
const { default: CreateOrgWizard } = await import("../components/settings/create-org-wizard");
|
|
834
|
+
const user = userEvent.setup();
|
|
835
|
+
render(<CreateOrgWizard />);
|
|
836
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
837
|
+
await user.type(screen.getByLabelText(/organization name/i), "Acme Corp");
|
|
838
|
+
await user.click(screen.getByRole("button", { name: /next/i }));
|
|
839
|
+
const createBtn = await screen.findByRole("button", { name: /^create$/i });
|
|
840
|
+
await user.click(createBtn);
|
|
841
|
+
expect(await screen.findByText(/organization created/i)).toBeInTheDocument();
|
|
842
|
+
expect(createOrganization).toHaveBeenCalledWith({ name: "Acme Corp", slug: "acme-corp" });
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it("shows inline error on API failure", async () => {
|
|
846
|
+
const { createOrganization } = await import("@/lib/org-api");
|
|
847
|
+
vi.mocked(createOrganization).mockRejectedValueOnce(new Error("API error: 409 Conflict"));
|
|
848
|
+
const { default: CreateOrgWizard } = await import("../components/settings/create-org-wizard");
|
|
849
|
+
const user = userEvent.setup();
|
|
850
|
+
render(<CreateOrgWizard />);
|
|
851
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
852
|
+
await user.type(screen.getByLabelText(/organization name/i), "Acme Corp");
|
|
853
|
+
await user.click(screen.getByRole("button", { name: /next/i }));
|
|
854
|
+
const createBtn = await screen.findByRole("button", { name: /^create$/i });
|
|
855
|
+
await user.click(createBtn);
|
|
856
|
+
expect(await screen.findByText(/already taken/i)).toBeInTheDocument();
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
describe("Notifications page - no team language", () => {
|
|
861
|
+
it("does not show 'Team invitations' label in notification preferences", async () => {
|
|
862
|
+
const settingsApi = await import("@/lib/settings-api");
|
|
863
|
+
vi.mocked(settingsApi.getNotificationPreferences).mockResolvedValueOnce({
|
|
864
|
+
billing_low_balance: true,
|
|
865
|
+
billing_receipts: true,
|
|
866
|
+
billing_auto_topup: true,
|
|
867
|
+
agent_channel_disconnect: true,
|
|
868
|
+
agent_status_changes: true,
|
|
869
|
+
account_role_changes: true,
|
|
870
|
+
account_team_invites: true,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
const { default: NotificationsPage } = await import(
|
|
874
|
+
"../app/(dashboard)/settings/notifications/page"
|
|
875
|
+
);
|
|
876
|
+
render(<NotificationsPage />);
|
|
877
|
+
|
|
878
|
+
await screen.findByText("Notifications");
|
|
879
|
+
|
|
880
|
+
expect(screen.queryByText("Team invitations")).not.toBeInTheDocument();
|
|
881
|
+
expect(screen.getByText("Invitations")).toBeInTheDocument();
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
describe("Avatar upload", () => {
|
|
886
|
+
it("renders avatar upload area on profile page", async () => {
|
|
887
|
+
const { default: ProfilePage } = await import("../app/(dashboard)/settings/profile/page");
|
|
888
|
+
render(<ProfilePage />);
|
|
889
|
+
|
|
890
|
+
expect(await screen.findByText("Profile")).toBeInTheDocument();
|
|
891
|
+
expect(screen.getByLabelText("Change avatar")).toBeInTheDocument();
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it("shows initials when no avatar URL", async () => {
|
|
895
|
+
const { default: ProfilePage } = await import("../app/(dashboard)/settings/profile/page");
|
|
896
|
+
render(<ProfilePage />);
|
|
897
|
+
|
|
898
|
+
await screen.findByText("Profile");
|
|
899
|
+
expect(screen.getByText("A")).toBeInTheDocument();
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it("calls uploadAvatar and updates profile on file select", async () => {
|
|
903
|
+
const api = await import("@/lib/api");
|
|
904
|
+
const updatedProfile = { ...MOCK_PROFILE, avatarUrl: "https://cdn.example.com/avatar.png" };
|
|
905
|
+
vi.mocked(api.uploadAvatar).mockResolvedValueOnce(updatedProfile);
|
|
906
|
+
|
|
907
|
+
const user = userEvent.setup();
|
|
908
|
+
const { default: ProfilePage } = await import("../app/(dashboard)/settings/profile/page");
|
|
909
|
+
render(<ProfilePage />);
|
|
910
|
+
|
|
911
|
+
await screen.findByText("Profile");
|
|
912
|
+
|
|
913
|
+
const file = new File(["fake-image"], "avatar.png", { type: "image/png" });
|
|
914
|
+
const input = screen.getByLabelText("Change avatar") as HTMLInputElement;
|
|
915
|
+
await user.upload(input, file);
|
|
916
|
+
|
|
917
|
+
await waitFor(() => {
|
|
918
|
+
expect(api.uploadAvatar).toHaveBeenCalledWith(file);
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it("shows error when file exceeds 2MB", async () => {
|
|
923
|
+
const user = userEvent.setup();
|
|
924
|
+
const { default: ProfilePage } = await import("../app/(dashboard)/settings/profile/page");
|
|
925
|
+
render(<ProfilePage />);
|
|
926
|
+
|
|
927
|
+
await screen.findByText("Profile");
|
|
928
|
+
|
|
929
|
+
const largeFile = new File([new ArrayBuffer(3 * 1024 * 1024)], "big.png", {
|
|
930
|
+
type: "image/png",
|
|
931
|
+
});
|
|
932
|
+
const input = screen.getByLabelText("Change avatar") as HTMLInputElement;
|
|
933
|
+
await user.upload(input, largeFile);
|
|
934
|
+
|
|
935
|
+
expect(await screen.findByText(/file size must be under 2MB/i)).toBeInTheDocument();
|
|
936
|
+
});
|
|
937
|
+
});
|