@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,119 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const { mockApplyCoupon } = vi.hoisted(() => ({
|
|
6
|
+
mockApplyCoupon: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("@/lib/trpc", () => ({
|
|
10
|
+
trpcVanilla: {
|
|
11
|
+
billing: {
|
|
12
|
+
applyCoupon: {
|
|
13
|
+
mutate: (...args: unknown[]) => mockApplyCoupon(...args),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("framer-motion", () => ({
|
|
20
|
+
motion: {
|
|
21
|
+
div: ({ children, ...props }: React.PropsWithChildren<Record<string, unknown>>) => (
|
|
22
|
+
<div {...props}>{children}</div>
|
|
23
|
+
),
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("next/navigation", () => ({
|
|
28
|
+
useRouter: () => ({ push: vi.fn() }),
|
|
29
|
+
useSearchParams: () => new URLSearchParams(),
|
|
30
|
+
usePathname: () => "/billing/credits",
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock("better-auth/react", () => ({
|
|
34
|
+
createAuthClient: () => ({
|
|
35
|
+
useSession: () => ({ data: null, isPending: false, error: null }),
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
describe("CouponInput", () => {
|
|
40
|
+
it("renders idle state — shows input and Apply button, no success/error message", async () => {
|
|
41
|
+
const { CouponInput } = await import("../components/billing/coupon-input");
|
|
42
|
+
render(<CouponInput />);
|
|
43
|
+
|
|
44
|
+
expect(screen.getByPlaceholderText("Coupon code")).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByRole("button", { name: "Apply" })).toBeInTheDocument();
|
|
46
|
+
expect(screen.queryByText(/credits will be added/)).not.toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("shows loading state while applying — button shows Applying... and is disabled", async () => {
|
|
50
|
+
// Never resolves so we can inspect the loading state
|
|
51
|
+
mockApplyCoupon.mockImplementation(
|
|
52
|
+
() =>
|
|
53
|
+
new Promise(() => {
|
|
54
|
+
/* never resolves — keep loading state for test */
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
const user = userEvent.setup();
|
|
58
|
+
const { CouponInput } = await import("../components/billing/coupon-input");
|
|
59
|
+
render(<CouponInput />);
|
|
60
|
+
|
|
61
|
+
const input = screen.getByPlaceholderText("Coupon code");
|
|
62
|
+
await user.type(input, "SAVE20");
|
|
63
|
+
await user.click(screen.getByRole("button", { name: "Apply" }));
|
|
64
|
+
|
|
65
|
+
expect(screen.getByRole("button", { name: "Applying..." })).toBeDisabled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("shows success state after valid code — green message with credits amount visible", async () => {
|
|
69
|
+
mockApplyCoupon.mockResolvedValueOnce({
|
|
70
|
+
creditsGranted: 500,
|
|
71
|
+
message: "Coupon applied!",
|
|
72
|
+
});
|
|
73
|
+
const user = userEvent.setup();
|
|
74
|
+
const { CouponInput } = await import("../components/billing/coupon-input");
|
|
75
|
+
render(<CouponInput />);
|
|
76
|
+
|
|
77
|
+
const input = screen.getByPlaceholderText("Coupon code");
|
|
78
|
+
await user.type(input, "VALID50");
|
|
79
|
+
await user.click(screen.getByRole("button", { name: "Apply" }));
|
|
80
|
+
|
|
81
|
+
expect(await screen.findByText(/\+500 credits added to your balance/)).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("shows error state after invalid code — red error message with server message shown", async () => {
|
|
85
|
+
mockApplyCoupon.mockRejectedValueOnce(new Error("Coupon not found"));
|
|
86
|
+
const user = userEvent.setup();
|
|
87
|
+
const { CouponInput } = await import("../components/billing/coupon-input");
|
|
88
|
+
render(<CouponInput />);
|
|
89
|
+
|
|
90
|
+
const input = screen.getByPlaceholderText("Coupon code");
|
|
91
|
+
await user.type(input, "BADCODE");
|
|
92
|
+
await user.click(screen.getByRole("button", { name: "Apply" }));
|
|
93
|
+
|
|
94
|
+
expect(await screen.findByText("Coupon not found")).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("clears error when input changes — typing a new code clears the error state", async () => {
|
|
98
|
+
mockApplyCoupon.mockRejectedValueOnce(new Error("Invalid code"));
|
|
99
|
+
const user = userEvent.setup();
|
|
100
|
+
const { CouponInput } = await import("../components/billing/coupon-input");
|
|
101
|
+
render(<CouponInput />);
|
|
102
|
+
|
|
103
|
+
const input = screen.getByPlaceholderText("Coupon code");
|
|
104
|
+
await user.type(input, "BAD");
|
|
105
|
+
await user.click(screen.getByRole("button", { name: "Apply" }));
|
|
106
|
+
await screen.findByText("Invalid code");
|
|
107
|
+
|
|
108
|
+
await user.type(input, "X");
|
|
109
|
+
|
|
110
|
+
expect(screen.queryByText("Invalid code")).not.toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("Apply button disabled when input is empty", async () => {
|
|
114
|
+
const { CouponInput } = await import("../components/billing/coupon-input");
|
|
115
|
+
render(<CouponInput />);
|
|
116
|
+
|
|
117
|
+
expect(screen.getByRole("button", { name: "Apply" })).toBeDisabled();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { CreateInstanceClient } from "../app/instances/new/create-instance-client";
|
|
5
|
+
|
|
6
|
+
vi.mock("@/lib/marketplace-data", () => ({
|
|
7
|
+
listMarketplacePlugins: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@/lib/api", () => ({
|
|
11
|
+
createInstance: vi.fn().mockResolvedValue({
|
|
12
|
+
id: "inst-new",
|
|
13
|
+
name: "my-new-instance",
|
|
14
|
+
template: "Custom",
|
|
15
|
+
status: "stopped",
|
|
16
|
+
provider: "anthropic",
|
|
17
|
+
channels: [],
|
|
18
|
+
plugins: [],
|
|
19
|
+
uptime: null,
|
|
20
|
+
createdAt: new Date().toISOString(),
|
|
21
|
+
}),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { listMarketplacePlugins } from "@/lib/marketplace-data";
|
|
25
|
+
|
|
26
|
+
const mockListMarketplacePlugins = vi.mocked(listMarketplacePlugins);
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
// Default: reject so hook falls back to static data quickly (loading resolves)
|
|
30
|
+
mockListMarketplacePlugins.mockRejectedValue(new Error("no marketplace in tests"));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("CreateInstanceClient", () => {
|
|
34
|
+
it("shows loading state before plugin data is ready", () => {
|
|
35
|
+
// Override to never resolve so loading stays true
|
|
36
|
+
const noop = (_resolve: unknown) => void _resolve;
|
|
37
|
+
mockListMarketplacePlugins.mockReturnValue(new Promise(noop));
|
|
38
|
+
render(<CreateInstanceClient />);
|
|
39
|
+
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders the create form heading", async () => {
|
|
43
|
+
render(<CreateInstanceClient />);
|
|
44
|
+
await waitFor(() =>
|
|
45
|
+
expect(screen.getByRole("heading", { name: "Create Instance" })).toBeInTheDocument(),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("renders preset cards", async () => {
|
|
50
|
+
render(<CreateInstanceClient />);
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
expect(screen.getByText("Discord AI Bot")).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByText("Slack AI Assistant")).toBeInTheDocument();
|
|
54
|
+
expect(screen.getByText("Multi-Channel")).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("disables submit button without name", async () => {
|
|
59
|
+
render(<CreateInstanceClient />);
|
|
60
|
+
await waitFor(() => screen.getByRole("button", { name: "Create Instance" }));
|
|
61
|
+
const submitBtn = screen.getByRole("button", { name: "Create Instance" });
|
|
62
|
+
expect(submitBtn).toBeDisabled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("enables submit button when name is entered without preset", async () => {
|
|
66
|
+
const user = userEvent.setup();
|
|
67
|
+
render(<CreateInstanceClient />);
|
|
68
|
+
await waitFor(() => screen.getByPlaceholderText("my-instance"));
|
|
69
|
+
const nameInput = screen.getByPlaceholderText("my-instance");
|
|
70
|
+
await user.type(nameInput, "my-bot");
|
|
71
|
+
const submitBtn = screen.getByRole("button", { name: "Create Instance" });
|
|
72
|
+
expect(submitBtn).toBeEnabled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("shows confirmation after creating", async () => {
|
|
76
|
+
const user = userEvent.setup();
|
|
77
|
+
render(<CreateInstanceClient />);
|
|
78
|
+
|
|
79
|
+
// Wait for loading to complete
|
|
80
|
+
await waitFor(() => screen.getByText("Discord AI Bot"));
|
|
81
|
+
|
|
82
|
+
// Select preset
|
|
83
|
+
await user.click(screen.getByText("Discord AI Bot"));
|
|
84
|
+
|
|
85
|
+
// Enter name
|
|
86
|
+
const nameInput = screen.getByPlaceholderText("my-instance");
|
|
87
|
+
await user.type(nameInput, "my-new-instance");
|
|
88
|
+
|
|
89
|
+
// Submit
|
|
90
|
+
await user.click(screen.getByRole("button", { name: "Create Instance" }));
|
|
91
|
+
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
expect(screen.getByText("Instance created")).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const mockPush = vi.fn();
|
|
6
|
+
vi.mock("next/navigation", () => ({
|
|
7
|
+
useRouter: () => ({ push: mockPush }),
|
|
8
|
+
useSearchParams: () => new URLSearchParams(),
|
|
9
|
+
usePathname: () => "/settings",
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const mockCreateOrganization = vi.fn();
|
|
13
|
+
vi.mock("@/lib/org-api", () => ({
|
|
14
|
+
createOrganization: (...args: unknown[]) => mockCreateOrganization(...args),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("framer-motion", () => {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
|
+
const React = require("react");
|
|
20
|
+
return {
|
|
21
|
+
motion: new Proxy(
|
|
22
|
+
{},
|
|
23
|
+
{
|
|
24
|
+
get:
|
|
25
|
+
(_target: unknown, tag: string) =>
|
|
26
|
+
({ children, ...props }: { children?: unknown; [key: string]: unknown }) =>
|
|
27
|
+
React.createElement(tag, props, children),
|
|
28
|
+
},
|
|
29
|
+
),
|
|
30
|
+
AnimatePresence: ({ children }: { children?: unknown }) => children,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
import CreateOrgWizard from "@/components/settings/create-org-wizard";
|
|
35
|
+
|
|
36
|
+
describe("CreateOrgWizard", () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
mockPush.mockClear();
|
|
39
|
+
mockCreateOrganization.mockClear();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders the trigger button", () => {
|
|
43
|
+
render(<CreateOrgWizard />);
|
|
44
|
+
expect(screen.getByRole("button", { name: /create organization/i })).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("opens the dialog when trigger is clicked", async () => {
|
|
48
|
+
const user = userEvent.setup();
|
|
49
|
+
render(<CreateOrgWizard />);
|
|
50
|
+
|
|
51
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
52
|
+
expect(screen.getByText("Name your organization")).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByLabelText("Organization name")).toBeInTheDocument();
|
|
54
|
+
expect(screen.getByLabelText("Slug")).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("auto-generates slug from name", async () => {
|
|
58
|
+
const user = userEvent.setup();
|
|
59
|
+
render(<CreateOrgWizard />);
|
|
60
|
+
|
|
61
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
62
|
+
await user.type(screen.getByLabelText("Organization name"), "Acme Corp");
|
|
63
|
+
|
|
64
|
+
expect(screen.getByLabelText("Slug")).toHaveValue("acme-corp");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("stops auto-generating slug once slug is manually edited", async () => {
|
|
68
|
+
const user = userEvent.setup();
|
|
69
|
+
render(<CreateOrgWizard />);
|
|
70
|
+
|
|
71
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
72
|
+
await user.type(screen.getByLabelText("Organization name"), "Acme");
|
|
73
|
+
expect(screen.getByLabelText("Slug")).toHaveValue("acme");
|
|
74
|
+
|
|
75
|
+
// Use fireEvent.change to set custom slug (userEvent treats hyphens as special keys)
|
|
76
|
+
fireEvent.change(screen.getByLabelText("Slug"), { target: { value: "customslug" } });
|
|
77
|
+
expect(screen.getByLabelText("Slug")).toHaveValue("customslug");
|
|
78
|
+
|
|
79
|
+
// Type more in name — slug should NOT change since slug was manually edited
|
|
80
|
+
await user.type(screen.getByLabelText("Organization name"), " Corp");
|
|
81
|
+
expect(screen.getByLabelText("Slug")).toHaveValue("customslug");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("sanitizes slug to lowercase, alphanumeric, hyphens only", async () => {
|
|
85
|
+
const user = userEvent.setup();
|
|
86
|
+
render(<CreateOrgWizard />);
|
|
87
|
+
|
|
88
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
89
|
+
await user.type(screen.getByLabelText("Organization name"), "Hello World! @#$ Test");
|
|
90
|
+
|
|
91
|
+
expect(screen.getByLabelText("Slug")).toHaveValue("hello-world-test");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("disables Next button when name or slug is empty", async () => {
|
|
95
|
+
const user = userEvent.setup();
|
|
96
|
+
render(<CreateOrgWizard />);
|
|
97
|
+
|
|
98
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
99
|
+
expect(screen.getByRole("button", { name: "Next" })).toBeDisabled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("advances to confirm step on Next", async () => {
|
|
103
|
+
const user = userEvent.setup();
|
|
104
|
+
render(<CreateOrgWizard />);
|
|
105
|
+
|
|
106
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
107
|
+
await user.type(screen.getByLabelText("Organization name"), "Acme Corp");
|
|
108
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
109
|
+
|
|
110
|
+
expect(screen.getByText("Confirm")).toBeInTheDocument();
|
|
111
|
+
expect(screen.getByText("Acme Corp")).toBeInTheDocument();
|
|
112
|
+
expect(screen.getByText("acme-corp")).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("goes back from confirm to name step", async () => {
|
|
116
|
+
const user = userEvent.setup();
|
|
117
|
+
render(<CreateOrgWizard />);
|
|
118
|
+
|
|
119
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
120
|
+
await user.type(screen.getByLabelText("Organization name"), "Acme Corp");
|
|
121
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
122
|
+
await user.click(screen.getByRole("button", { name: "Back" }));
|
|
123
|
+
|
|
124
|
+
expect(screen.getByText("Name your organization")).toBeInTheDocument();
|
|
125
|
+
expect(screen.getByLabelText("Organization name")).toHaveValue("Acme Corp");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("calls createOrganization and shows done step on success", async () => {
|
|
129
|
+
mockCreateOrganization.mockResolvedValue({ id: "org-1", name: "Acme Corp", slug: "acme-corp" });
|
|
130
|
+
const user = userEvent.setup();
|
|
131
|
+
render(<CreateOrgWizard />);
|
|
132
|
+
|
|
133
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
134
|
+
await user.type(screen.getByLabelText("Organization name"), "Acme Corp");
|
|
135
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
136
|
+
await user.click(screen.getByRole("button", { name: "Create" }));
|
|
137
|
+
|
|
138
|
+
expect(mockCreateOrganization).toHaveBeenCalledWith({ name: "Acme Corp", slug: "acme-corp" });
|
|
139
|
+
expect(await screen.findByText("Organization created")).toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("shows 409 conflict error message", async () => {
|
|
143
|
+
mockCreateOrganization.mockRejectedValue(new Error("409 Conflict"));
|
|
144
|
+
const user = userEvent.setup();
|
|
145
|
+
render(<CreateOrgWizard />);
|
|
146
|
+
|
|
147
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
148
|
+
await user.type(screen.getByLabelText("Organization name"), "Acme Corp");
|
|
149
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
150
|
+
await user.click(screen.getByRole("button", { name: "Create" }));
|
|
151
|
+
|
|
152
|
+
expect(await screen.findByText(/slug is already taken/)).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("shows generic error for non-409 failures", async () => {
|
|
156
|
+
mockCreateOrganization.mockRejectedValue(new Error("500 Server Error"));
|
|
157
|
+
const user = userEvent.setup();
|
|
158
|
+
render(<CreateOrgWizard />);
|
|
159
|
+
|
|
160
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
161
|
+
await user.type(screen.getByLabelText("Organization name"), "Acme Corp");
|
|
162
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
163
|
+
await user.click(screen.getByRole("button", { name: "Create" }));
|
|
164
|
+
|
|
165
|
+
expect(await screen.findByText(/Failed to create organization/)).toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("navigates to /settings/org on done step button click", async () => {
|
|
169
|
+
mockCreateOrganization.mockResolvedValue({ id: "org-1", name: "Acme Corp", slug: "acme-corp" });
|
|
170
|
+
const user = userEvent.setup();
|
|
171
|
+
render(<CreateOrgWizard />);
|
|
172
|
+
|
|
173
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
174
|
+
await user.type(screen.getByLabelText("Organization name"), "Acme Corp");
|
|
175
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
176
|
+
await user.click(screen.getByRole("button", { name: "Create" }));
|
|
177
|
+
|
|
178
|
+
const goBtn = await screen.findByRole("button", { name: /go to organization settings/i });
|
|
179
|
+
await user.click(goBtn);
|
|
180
|
+
expect(mockPush).toHaveBeenCalledWith("/settings/org");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("disables Create button while creating", async () => {
|
|
184
|
+
mockCreateOrganization.mockReturnValue(
|
|
185
|
+
new Promise(() => {
|
|
186
|
+
/* never resolves */
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
const user = userEvent.setup();
|
|
190
|
+
render(<CreateOrgWizard />);
|
|
191
|
+
|
|
192
|
+
await user.click(screen.getByRole("button", { name: /create organization/i }));
|
|
193
|
+
await user.type(screen.getByLabelText("Organization name"), "Acme Corp");
|
|
194
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
195
|
+
await user.click(screen.getByRole("button", { name: "Create" }));
|
|
196
|
+
|
|
197
|
+
expect(await screen.findByText("Creating...")).toBeInTheDocument();
|
|
198
|
+
expect(screen.getByRole("button", { name: "Creating..." })).toBeDisabled();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { CreditBalance } from "@/components/billing/credit-balance";
|
|
4
|
+
|
|
5
|
+
describe("CreditBalance", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// Make requestAnimationFrame fire synchronously so the count-up animation completes instantly
|
|
8
|
+
vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
|
|
9
|
+
cb(performance.now() + 2000); // advance time past animation duration
|
|
10
|
+
return 0;
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("renders the formatted balance", async () => {
|
|
19
|
+
render(<CreditBalance data={{ balance: 42.5, dailyBurn: 3.25, runway: 13 }} />);
|
|
20
|
+
// The count-up animation targets 42.5 → "$42.50"
|
|
21
|
+
await waitFor(() => {
|
|
22
|
+
expect(screen.getByText("$42.50")).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("renders daily burn rate", () => {
|
|
27
|
+
render(<CreditBalance data={{ balance: 100, dailyBurn: 5.0, runway: 20 }} />);
|
|
28
|
+
expect(screen.getByText("$5.00/day")).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders runway in days (plural)", () => {
|
|
32
|
+
render(<CreditBalance data={{ balance: 100, dailyBurn: 5.0, runway: 20 }} />);
|
|
33
|
+
expect(screen.getByText("~20 days")).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("renders runway singular for 1 day", () => {
|
|
37
|
+
render(<CreditBalance data={{ balance: 5, dailyBurn: 5.0, runway: 1 }} />);
|
|
38
|
+
expect(screen.getByText("~1 day")).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders 'Suspended' when runway is 0", () => {
|
|
42
|
+
render(<CreditBalance data={{ balance: 0, dailyBurn: 5.0, runway: 0 }} />);
|
|
43
|
+
expect(screen.getByText("Suspended")).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders 'N/A' when runway is null", () => {
|
|
47
|
+
render(<CreditBalance data={{ balance: 50, dailyBurn: 0, runway: null }} />);
|
|
48
|
+
expect(screen.getByText("N/A")).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("applies destructive color when balance <= 2", async () => {
|
|
52
|
+
const { container } = render(
|
|
53
|
+
<CreditBalance data={{ balance: 1.5, dailyBurn: 1.5, runway: 1 }} />,
|
|
54
|
+
);
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
const balanceEl = container.querySelector(".text-4xl");
|
|
57
|
+
expect(balanceEl?.className).toContain("text-destructive");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("applies destructive color when runway <= 1", async () => {
|
|
62
|
+
const { container } = render(
|
|
63
|
+
<CreditBalance data={{ balance: 50, dailyBurn: 50, runway: 1 }} />,
|
|
64
|
+
);
|
|
65
|
+
await waitFor(() => {
|
|
66
|
+
const balanceEl = container.querySelector(".text-4xl");
|
|
67
|
+
expect(balanceEl?.className).toContain("text-destructive");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("applies amber color when balance <= 10", async () => {
|
|
72
|
+
const { container } = render(<CreditBalance data={{ balance: 8, dailyBurn: 2, runway: 4 }} />);
|
|
73
|
+
await waitFor(() => {
|
|
74
|
+
const balanceEl = container.querySelector(".text-4xl");
|
|
75
|
+
expect(balanceEl?.className).toContain("text-amber-500");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("applies amber color when runway <= 7", async () => {
|
|
80
|
+
const { container } = render(
|
|
81
|
+
<CreditBalance data={{ balance: 50, dailyBurn: 10, runway: 5 }} />,
|
|
82
|
+
);
|
|
83
|
+
await waitFor(() => {
|
|
84
|
+
const balanceEl = container.querySelector(".text-4xl");
|
|
85
|
+
expect(balanceEl?.className).toContain("text-amber-500");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("applies emerald color for healthy balance", async () => {
|
|
90
|
+
const { container } = render(
|
|
91
|
+
<CreditBalance data={{ balance: 100, dailyBurn: 5, runway: 20 }} />,
|
|
92
|
+
);
|
|
93
|
+
await waitFor(() => {
|
|
94
|
+
const balanceEl = container.querySelector(".text-4xl");
|
|
95
|
+
expect(balanceEl?.className).toContain("text-emerald-500");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("renders the card title", () => {
|
|
100
|
+
render(<CreditBalance data={{ balance: 50, dailyBurn: 2, runway: 25 }} />);
|
|
101
|
+
expect(screen.getByText("Credit Balance")).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
});
|