@libredb/studio 0.9.7
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/.claude/settings.local.json +127 -0
- package/.cursorrules +426 -0
- package/.devin/wiki.json +143 -0
- package/.dockerignore +80 -0
- package/.env.example +159 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +57 -0
- package/.github/workflows/ci.yml +185 -0
- package/.github/workflows/codeql.yml +57 -0
- package/.github/workflows/docker-build-push.yml +118 -0
- package/.github/workflows/helm-release.yml +113 -0
- package/CLAUDE.md +265 -0
- package/CODE_OF_CONDUCT.md +124 -0
- package/CONTRIBUTING.md +154 -0
- package/Dockerfile +73 -0
- package/LICENSE +21 -0
- package/README.md +614 -0
- package/SECURITY.md +107 -0
- package/artifacthub-repo.yml +4 -0
- package/bun.lock +1714 -0
- package/bunfig.toml +3 -0
- package/charts/libredb-studio/.helmignore +11 -0
- package/charts/libredb-studio/Chart.lock +6 -0
- package/charts/libredb-studio/Chart.yaml +50 -0
- package/charts/libredb-studio/README.md +206 -0
- package/charts/libredb-studio/templates/NOTES.txt +59 -0
- package/charts/libredb-studio/templates/_helpers.tpl +135 -0
- package/charts/libredb-studio/templates/configmap.yaml +37 -0
- package/charts/libredb-studio/templates/deployment.yaml +184 -0
- package/charts/libredb-studio/templates/hpa.yaml +32 -0
- package/charts/libredb-studio/templates/ingress.yaml +41 -0
- package/charts/libredb-studio/templates/networkpolicy.yaml +50 -0
- package/charts/libredb-studio/templates/pdb.yaml +18 -0
- package/charts/libredb-studio/templates/pvc.yaml +23 -0
- package/charts/libredb-studio/templates/secret.yaml +30 -0
- package/charts/libredb-studio/templates/seed-configmap.yaml +11 -0
- package/charts/libredb-studio/templates/service.yaml +22 -0
- package/charts/libredb-studio/templates/serviceaccount.yaml +13 -0
- package/charts/libredb-studio/values.schema.json +246 -0
- package/charts/libredb-studio/values.yaml +286 -0
- package/components.json +22 -0
- package/conductor/code_styleguides/typescript.md +43 -0
- package/conductor/product-guidelines.md +43 -0
- package/conductor/product.md +3 -0
- package/conductor/setup_state.json +1 -0
- package/conductor/tech-stack.md +39 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/metadata.json +8 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/plan.md +44 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/spec.md +31 -0
- package/conductor/tracks.md +8 -0
- package/conductor/workflow.md +333 -0
- package/database-compose.yml +55 -0
- package/docker/postgres-init/01-extensions.sql +10 -0
- package/docker/postgres-init/02-sample-data.sql +585 -0
- package/docker/postgres.yml +68 -0
- package/docker-compose.yml +38 -0
- package/docs/AI_PLAN.md +74 -0
- package/docs/API_DOCS.md +875 -0
- package/docs/ARCHITECTURE.md +218 -0
- package/docs/DATABASE_PROVIDERS.md +358 -0
- package/docs/FEATURES.md +116 -0
- package/docs/HELM_CHART.md +252 -0
- package/docs/LOGIN_PAGE.md +178 -0
- package/docs/MONACO_EDITOR_PERFORMANCE.md +315 -0
- package/docs/OIDC_ARCH.md +681 -0
- package/docs/OIDC_SETUP.md +322 -0
- package/docs/POSTGRES_METRICS.md +516 -0
- package/docs/QUERY_OPTIMIZATION.md +370 -0
- package/docs/SEED_CONNECTIONS.md +468 -0
- package/docs/SQL_ALIAS_COMPLETION.md +190 -0
- package/docs/STORAGE_ARCHITECTURE.md +565 -0
- package/docs/STORAGE_QUICK_SETUP.md +419 -0
- package/docs/TECHNICAL_PLAN.md +36 -0
- package/docs/THEMING.md +345 -0
- package/docs/adding-a-new-database-provider.md +642 -0
- package/docs/backlogs/000-PLATFORM_DATA_SYNC_DATABASE.md +360 -0
- package/docs/backlogs/001-INLINE_DATA_EDITING.md +118 -0
- package/docs/backlogs/002-DATA_IMPORT.md +215 -0
- package/docs/backlogs/003-QUERY_TIME_MACHINE.md +183 -0
- package/docs/backlogs/004-AI_DATA_STORYTELLER.md +292 -0
- package/docs/backlogs/005-QUERY_PLAYGROUND.md +352 -0
- package/docs/backlogs/006-DATA_MASKING.md +418 -0
- package/docs/enterprise-features.md +718 -0
- package/docs/kubernetes-helm-chart-artifacthub-plan.md +803 -0
- package/docs/medium-koyeb-article-en.md +215 -0
- package/docs/plans/test-plans.md +445 -0
- package/docs/releases/RELEASE.V0.3.0.md +22 -0
- package/docs/releases/RELEASE.V0.4.0.md +154 -0
- package/docs/releases/RELEASE.V0.5.0.md +252 -0
- package/docs/releases/RELEASE_v0.5.6.md +145 -0
- package/docs/releases/RELEASE_v0.6.1.md +303 -0
- package/docs/releases/RELEASE_v0.6.7.md +292 -0
- package/docs/releases/RELEASE_v0.7.0.md +332 -0
- package/docs/releases/RELEASE_v0.8.0.md +521 -0
- package/docs/sampledb/titanic.sql +1379 -0
- package/docs/superpowers/plans/2026-03-25-seed-connections.md +1362 -0
- package/docs/superpowers/specs/2026-03-25-seed-connections-design.md +590 -0
- package/e2e/admin-dashboard.spec.ts +64 -0
- package/e2e/connection-management.spec.ts +58 -0
- package/e2e/export.spec.ts +34 -0
- package/e2e/login.spec.ts +85 -0
- package/e2e/query-execution.spec.ts +35 -0
- package/e2e/tab-management.spec.ts +64 -0
- package/eslint.config.mjs +28 -0
- package/fly.toml +43 -0
- package/next.config.ts +32 -0
- package/package.json +130 -0
- package/playwright.config.ts +34 -0
- package/postcss.config.mjs +7 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.svg +32 -0
- package/public/next.svg +1 -0
- package/public/screenshots/code-generator.png +0 -0
- package/public/screenshots/connection-modal.png +0 -0
- package/public/screenshots/data-profiler.png +0 -0
- package/public/screenshots/erd-diagram.png +0 -0
- package/public/screenshots/hero-editor.png +0 -0
- package/public/screenshots/nl2sql.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/render.yaml +58 -0
- package/scripts/merge-lcov.mjs +239 -0
- package/sonar-project.properties +16 -0
- package/src/app/admin/error.tsx +46 -0
- package/src/app/admin/page.tsx +10 -0
- package/src/app/api/admin/audit/route.ts +52 -0
- package/src/app/api/admin/fleet-health/route.ts +81 -0
- package/src/app/api/ai/autopilot/route.ts +105 -0
- package/src/app/api/ai/chat/route.ts +132 -0
- package/src/app/api/ai/describe-schema/route.ts +52 -0
- package/src/app/api/ai/explain/route.ts +86 -0
- package/src/app/api/ai/impact/route.ts +97 -0
- package/src/app/api/ai/index-advisor/route.ts +98 -0
- package/src/app/api/ai/nl2sql/route.ts +87 -0
- package/src/app/api/ai/query-safety/route.ts +87 -0
- package/src/app/api/auth/login/route.ts +62 -0
- package/src/app/api/auth/logout/route.ts +25 -0
- package/src/app/api/auth/me/route.ts +10 -0
- package/src/app/api/auth/oidc/callback/route.ts +82 -0
- package/src/app/api/auth/oidc/login/route.ts +43 -0
- package/src/app/api/connections/managed/route.ts +35 -0
- package/src/app/api/db/cancel/route.ts +42 -0
- package/src/app/api/db/disconnect/route.ts +28 -0
- package/src/app/api/db/health/route.ts +49 -0
- package/src/app/api/db/maintenance/route.ts +72 -0
- package/src/app/api/db/monitoring/route.ts +62 -0
- package/src/app/api/db/multi-query/route.ts +116 -0
- package/src/app/api/db/pool-stats/route.ts +37 -0
- package/src/app/api/db/profile/route.ts +144 -0
- package/src/app/api/db/provider-meta/route.ts +49 -0
- package/src/app/api/db/query/route.ts +50 -0
- package/src/app/api/db/schema/route.ts +47 -0
- package/src/app/api/db/schema-snapshot/route.ts +42 -0
- package/src/app/api/db/test-connection/route.ts +55 -0
- package/src/app/api/db/transaction/route.ts +111 -0
- package/src/app/api/storage/[collection]/route.ts +67 -0
- package/src/app/api/storage/config/route.ts +17 -0
- package/src/app/api/storage/migrate/route.ts +45 -0
- package/src/app/api/storage/route.ts +32 -0
- package/src/app/error.tsx +49 -0
- package/src/app/global-error.tsx +55 -0
- package/src/app/globals.css +146 -0
- package/src/app/icon.svg +42 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/login/login-form.tsx +301 -0
- package/src/app/login/page.tsx +11 -0
- package/src/app/monitoring/page.tsx +8 -0
- package/src/app/not-found.tsx +29 -0
- package/src/app/page.tsx +5 -0
- package/src/components/AIAutopilotPanel.tsx +238 -0
- package/src/components/CodeGenerator.tsx +271 -0
- package/src/components/CommandPalette.tsx +227 -0
- package/src/components/ConnectionModal.tsx +759 -0
- package/src/components/CreateTableModal.tsx +281 -0
- package/src/components/DataCharts.tsx +962 -0
- package/src/components/DataImportModal.tsx +582 -0
- package/src/components/DataProfiler.tsx +335 -0
- package/src/components/DatabaseDocs.tsx +251 -0
- package/src/components/MaskingSettings.tsx +414 -0
- package/src/components/MobileNav.tsx +50 -0
- package/src/components/NL2SQLPanel.tsx +281 -0
- package/src/components/PivotTable.tsx +257 -0
- package/src/components/QueryEditor.tsx +760 -0
- package/src/components/QueryHistory.tsx +344 -0
- package/src/components/QuerySafetyDialog.tsx +290 -0
- package/src/components/ResultsGrid.tsx +644 -0
- package/src/components/SaveQueryModal.tsx +104 -0
- package/src/components/SavedQueries.tsx +128 -0
- package/src/components/SchemaDiagram.tsx +473 -0
- package/src/components/SchemaDiff.tsx +473 -0
- package/src/components/SnapshotTimeline.tsx +116 -0
- package/src/components/Studio.tsx +639 -0
- package/src/components/TestDataGenerator.tsx +261 -0
- package/src/components/VisualExplain.tsx +820 -0
- package/src/components/admin/AdminDashboard.tsx +163 -0
- package/src/components/admin/tabs/AuditTab.tsx +531 -0
- package/src/components/admin/tabs/MonitoringEmbed.tsx +11 -0
- package/src/components/admin/tabs/OperationsTab.tsx +646 -0
- package/src/components/admin/tabs/OverviewTab.tsx +1328 -0
- package/src/components/admin/tabs/SecurityTab.tsx +284 -0
- package/src/components/community-section.tsx +92 -0
- package/src/components/icons/db-icons.tsx +84 -0
- package/src/components/libredb-logo.tsx +61 -0
- package/src/components/monitoring/MonitoringDashboard.tsx +345 -0
- package/src/components/monitoring/tabs/MetricChart.tsx +82 -0
- package/src/components/monitoring/tabs/OverviewTab.tsx +263 -0
- package/src/components/monitoring/tabs/PerformanceTab.tsx +254 -0
- package/src/components/monitoring/tabs/PoolTab.tsx +174 -0
- package/src/components/monitoring/tabs/QueriesTab.tsx +287 -0
- package/src/components/monitoring/tabs/SessionsTab.tsx +316 -0
- package/src/components/monitoring/tabs/StorageTab.tsx +335 -0
- package/src/components/monitoring/tabs/TablesTab.tsx +300 -0
- package/src/components/results-grid/ResultCard.tsx +111 -0
- package/src/components/results-grid/RowDetailSheet.tsx +178 -0
- package/src/components/results-grid/StatsBar.tsx +201 -0
- package/src/components/results-grid/index.ts +1 -0
- package/src/components/results-grid/utils.ts +23 -0
- package/src/components/schema-explorer/ColumnList.tsx +53 -0
- package/src/components/schema-explorer/SchemaExplorer.tsx +182 -0
- package/src/components/schema-explorer/TableItem.tsx +210 -0
- package/src/components/schema-explorer/index.ts +1 -0
- package/src/components/sidebar/ConnectionItem.tsx +105 -0
- package/src/components/sidebar/ConnectionsList.tsx +62 -0
- package/src/components/sidebar/Sidebar.tsx +130 -0
- package/src/components/sidebar/index.ts +2 -0
- package/src/components/studio/BottomPanel.tsx +286 -0
- package/src/components/studio/QueryToolbar.tsx +180 -0
- package/src/components/studio/StudioDesktopHeader.tsx +114 -0
- package/src/components/studio/StudioMobileHeader.tsx +340 -0
- package/src/components/studio/StudioTabBar.tsx +82 -0
- package/src/components/studio/index.ts +5 -0
- package/src/components/ui/accordion.tsx +66 -0
- package/src/components/ui/alert-dialog.tsx +157 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/aspect-ratio.tsx +11 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/calendar.tsx +216 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/carousel.tsx +241 -0
- package/src/components/ui/chart.tsx +357 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/context-menu.tsx +252 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +248 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/hover-card.tsx +44 -0
- package/src/components/ui/input-group.tsx +170 -0
- package/src/components/ui/input-otp.tsx +77 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/item.tsx +193 -0
- package/src/components/ui/kbd.tsx +28 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/menubar.tsx +276 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/popover.tsx +48 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +187 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +139 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +63 -0
- package/src/components/ui/sonner.tsx +40 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/exports/components.ts +15 -0
- package/src/exports/index.ts +4 -0
- package/src/exports/providers.ts +4 -0
- package/src/exports/types.ts +26 -0
- package/src/hooks/use-ai-chat.ts +182 -0
- package/src/hooks/use-all-connections.ts +66 -0
- package/src/hooks/use-api-call.ts +71 -0
- package/src/hooks/use-auth.ts +51 -0
- package/src/hooks/use-connection-form.ts +349 -0
- package/src/hooks/use-connection-manager.ts +169 -0
- package/src/hooks/use-connection-payload.ts +15 -0
- package/src/hooks/use-inline-editing.ts +109 -0
- package/src/hooks/use-mobile.ts +20 -0
- package/src/hooks/use-monitoring-data.ts +270 -0
- package/src/hooks/use-provider-metadata.ts +62 -0
- package/src/hooks/use-query-execution.ts +478 -0
- package/src/hooks/use-storage-sync.ts +259 -0
- package/src/hooks/use-tab-manager.ts +231 -0
- package/src/hooks/use-toast.ts +20 -0
- package/src/hooks/use-transaction-control.ts +64 -0
- package/src/lib/api/error-codes.ts +30 -0
- package/src/lib/api/errors.ts +236 -0
- package/src/lib/api/with-error-handler.ts +41 -0
- package/src/lib/audit.ts +105 -0
- package/src/lib/auth.ts +87 -0
- package/src/lib/connection-string-parser.ts +172 -0
- package/src/lib/data-masking.ts +385 -0
- package/src/lib/db/base-provider.ts +325 -0
- package/src/lib/db/errors.ts +317 -0
- package/src/lib/db/factory.ts +324 -0
- package/src/lib/db/index.ts +123 -0
- package/src/lib/db/providers/document/index.ts +6 -0
- package/src/lib/db/providers/document/mongodb.ts +992 -0
- package/src/lib/db/providers/keyvalue/redis.ts +554 -0
- package/src/lib/db/providers/sql/index.ts +11 -0
- package/src/lib/db/providers/sql/mssql.ts +1065 -0
- package/src/lib/db/providers/sql/mysql.ts +978 -0
- package/src/lib/db/providers/sql/oracle.ts +1044 -0
- package/src/lib/db/providers/sql/postgres.ts +1179 -0
- package/src/lib/db/providers/sql/sql-base.ts +174 -0
- package/src/lib/db/providers/sql/sqlite.ts +721 -0
- package/src/lib/db/types.ts +437 -0
- package/src/lib/db/utils/pool-manager.ts +287 -0
- package/src/lib/db/utils/query-limiter.ts +239 -0
- package/src/lib/db-ui-config.ts +86 -0
- package/src/lib/editor/mongodb-completions.ts +172 -0
- package/src/lib/editor/sql-completions.ts +280 -0
- package/src/lib/llm/base-provider.ts +117 -0
- package/src/lib/llm/factory.ts +102 -0
- package/src/lib/llm/index.ts +90 -0
- package/src/lib/llm/providers/custom.ts +181 -0
- package/src/lib/llm/providers/gemini.ts +126 -0
- package/src/lib/llm/providers/ollama.ts +154 -0
- package/src/lib/llm/providers/openai.ts +146 -0
- package/src/lib/llm/types.ts +173 -0
- package/src/lib/llm/utils/config.ts +187 -0
- package/src/lib/llm/utils/retry.ts +119 -0
- package/src/lib/llm/utils/streaming.ts +202 -0
- package/src/lib/logger.ts +127 -0
- package/src/lib/monitoring-thresholds.ts +44 -0
- package/src/lib/oidc.ts +262 -0
- package/src/lib/query-generators.ts +61 -0
- package/src/lib/schema-diff/diff-engine.ts +273 -0
- package/src/lib/schema-diff/migration-generator.ts +208 -0
- package/src/lib/schema-diff/types.ts +55 -0
- package/src/lib/seed/config-loader.ts +79 -0
- package/src/lib/seed/connection-filter.ts +49 -0
- package/src/lib/seed/credential-resolver.ts +62 -0
- package/src/lib/seed/index.ts +40 -0
- package/src/lib/seed/resolve-connection.ts +57 -0
- package/src/lib/seed/types.ts +69 -0
- package/src/lib/sql/alias-extractor.ts +267 -0
- package/src/lib/sql/index.ts +8 -0
- package/src/lib/sql/statement-splitter.ts +167 -0
- package/src/lib/sql/types.ts +40 -0
- package/src/lib/ssh/tunnel.ts +142 -0
- package/src/lib/storage/factory.ts +84 -0
- package/src/lib/storage/index.ts +14 -0
- package/src/lib/storage/local-storage.ts +99 -0
- package/src/lib/storage/providers/postgres.ts +225 -0
- package/src/lib/storage/providers/sqlite.ts +153 -0
- package/src/lib/storage/storage-facade.ts +272 -0
- package/src/lib/storage/types.ts +75 -0
- package/src/lib/time-series-buffer.ts +58 -0
- package/src/lib/types.ts +173 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +104 -0
- package/src/types/db-drivers.d.ts +23 -0
- package/src/types/html2canvas.d.ts +9 -0
- package/tests/api/admin/audit.test.ts +178 -0
- package/tests/api/admin/fleet-health.test.ts +183 -0
- package/tests/api/ai/autopilot.test.ts +174 -0
- package/tests/api/ai/chat.test.ts +250 -0
- package/tests/api/ai/describe-schema.test.ts +266 -0
- package/tests/api/ai/explain.test.ts +199 -0
- package/tests/api/ai/impact.test.ts +168 -0
- package/tests/api/ai/index-advisor.test.ts +171 -0
- package/tests/api/ai/nl2sql.test.ts +202 -0
- package/tests/api/ai/query-safety.test.ts +196 -0
- package/tests/api/auth/login.test.ts +170 -0
- package/tests/api/auth/logout.test.ts +140 -0
- package/tests/api/auth/me.test.ts +73 -0
- package/tests/api/auth/oidc-callback.test.ts +215 -0
- package/tests/api/auth/oidc-login.test.ts +127 -0
- package/tests/api/db/cancel.test.ts +198 -0
- package/tests/api/db/disconnect.test.ts +124 -0
- package/tests/api/db/health.test.ts +222 -0
- package/tests/api/db/maintenance.test.ts +263 -0
- package/tests/api/db/monitoring.test.ts +221 -0
- package/tests/api/db/multi-query.test.ts +316 -0
- package/tests/api/db/pool-stats.test.ts +135 -0
- package/tests/api/db/profile.test.ts +330 -0
- package/tests/api/db/provider-meta.test.ts +193 -0
- package/tests/api/db/query.test.ts +314 -0
- package/tests/api/db/schema-snapshot.test.ts +170 -0
- package/tests/api/db/schema.test.ts +191 -0
- package/tests/api/db/test-connection.test.ts +185 -0
- package/tests/api/db/transaction.test.ts +314 -0
- package/tests/api/proxy.test.ts +191 -0
- package/tests/api/seed/managed-route.test.ts +113 -0
- package/tests/api/storage/config.test.ts +42 -0
- package/tests/api/storage/storage-routes.test.ts +309 -0
- package/tests/components/AIAutopilotPanel.test.tsx +756 -0
- package/tests/components/AdminPage.test.tsx +33 -0
- package/tests/components/CodeGenerator.test.tsx +182 -0
- package/tests/components/CommandPalette.test.tsx +428 -0
- package/tests/components/CommunitySection.test.tsx +91 -0
- package/tests/components/ConnectionModal.mobile.test.tsx +284 -0
- package/tests/components/ConnectionModal.test.tsx +570 -0
- package/tests/components/CreateTableModal.test.tsx +383 -0
- package/tests/components/DataCharts.test.tsx +739 -0
- package/tests/components/DataImportModal.test.tsx +751 -0
- package/tests/components/DataProfiler.test.tsx +589 -0
- package/tests/components/DatabaseDocs.test.tsx +353 -0
- package/tests/components/LoginPage.test.tsx +163 -0
- package/tests/components/LoginPageOIDC.test.tsx +92 -0
- package/tests/components/MaskingSettings.test.tsx +498 -0
- package/tests/components/MobileNav.test.tsx +30 -0
- package/tests/components/MonitoringPage.test.tsx +32 -0
- package/tests/components/NL2SQLPanel.test.tsx +621 -0
- package/tests/components/Page.test.tsx +33 -0
- package/tests/components/PivotTable.test.tsx +350 -0
- package/tests/components/QueryEditor.test.tsx +1730 -0
- package/tests/components/QueryHistory.test.tsx +572 -0
- package/tests/components/QuerySafetyDialog.test.tsx +586 -0
- package/tests/components/ResultsGrid.test.tsx +804 -0
- package/tests/components/RootLayout.test.tsx +83 -0
- package/tests/components/SaveQueryModal.test.tsx +25 -0
- package/tests/components/SavedQueries.test.tsx +43 -0
- package/tests/components/SchemaDiagram.test.tsx +1034 -0
- package/tests/components/SchemaDiff.test.tsx +906 -0
- package/tests/components/SnapshotTimeline.test.tsx +174 -0
- package/tests/components/Studio.test.tsx +1030 -0
- package/tests/components/TestDataGenerator.test.tsx +291 -0
- package/tests/components/VisualExplain.test.tsx +704 -0
- package/tests/components/admin/AdminDashboard.test.tsx +205 -0
- package/tests/components/admin/AuditTab.test.tsx +220 -0
- package/tests/components/admin/MonitoringEmbed.test.tsx +58 -0
- package/tests/components/admin/OperationsTab.test.tsx +975 -0
- package/tests/components/admin/OverviewTab.test.tsx +254 -0
- package/tests/components/admin/SecurityTab.test.tsx +467 -0
- package/tests/components/monitoring/MetricChart.test.tsx +111 -0
- package/tests/components/monitoring/MonitoringDashboard.test.tsx +259 -0
- package/tests/components/monitoring/OverviewTab.test.tsx +78 -0
- package/tests/components/monitoring/PerformanceTab.test.tsx +87 -0
- package/tests/components/monitoring/PoolTab.test.tsx +42 -0
- package/tests/components/monitoring/QueriesTab.test.tsx +80 -0
- package/tests/components/monitoring/SessionsTab.test.tsx +154 -0
- package/tests/components/monitoring/StorageTab.test.tsx +127 -0
- package/tests/components/monitoring/TablesTab.test.tsx +153 -0
- package/tests/components/results-grid/ResultCard.test.tsx +105 -0
- package/tests/components/results-grid/RowDetailSheet.test.tsx +308 -0
- package/tests/components/results-grid/StatsBar.test.tsx +162 -0
- package/tests/components/schema-explorer/ColumnList.test.tsx +151 -0
- package/tests/components/schema-explorer/SchemaExplorer.test.tsx +461 -0
- package/tests/components/schema-explorer/TableItem.test.tsx +415 -0
- package/tests/components/sidebar/ConnectionItem.test.tsx +201 -0
- package/tests/components/sidebar/ConnectionsList.test.tsx +176 -0
- package/tests/components/sidebar/Sidebar.test.tsx +187 -0
- package/tests/components/studio/BottomPanel.test.tsx +383 -0
- package/tests/components/studio/QueryToolbar.test.tsx +321 -0
- package/tests/components/studio/StudioDesktopHeader.test.tsx +377 -0
- package/tests/components/studio/StudioMobileHeader.test.tsx +198 -0
- package/tests/components/studio/StudioTabBar.test.tsx +331 -0
- package/tests/fixtures/connections.ts +96 -0
- package/tests/fixtures/masking-configs.ts +86 -0
- package/tests/fixtures/query-results.ts +71 -0
- package/tests/fixtures/schemas.ts +64 -0
- package/tests/fixtures/seed-connections/invalid-config.yaml +7 -0
- package/tests/fixtures/seed-connections/minimal-config.yaml +8 -0
- package/tests/fixtures/seed-connections/mixed-credentials.yaml +23 -0
- package/tests/fixtures/seed-connections/multi-role-config.yaml +30 -0
- package/tests/fixtures/seed-connections/valid-config.json +15 -0
- package/tests/fixtures/seed-connections/valid-config.yaml +51 -0
- package/tests/helpers/mock-fetch.ts +59 -0
- package/tests/helpers/mock-monaco.ts +112 -0
- package/tests/helpers/mock-navigation.ts +28 -0
- package/tests/helpers/mock-next.ts +80 -0
- package/tests/helpers/mock-provider.ts +133 -0
- package/tests/helpers/mock-sonner.ts +29 -0
- package/tests/helpers/render-with-providers.tsx +19 -0
- package/tests/hooks/use-ai-chat.test.ts +600 -0
- package/tests/hooks/use-auth.test.ts +371 -0
- package/tests/hooks/use-connection-form.test.ts +743 -0
- package/tests/hooks/use-connection-manager.test.ts +466 -0
- package/tests/hooks/use-inline-editing.test.ts +321 -0
- package/tests/hooks/use-mobile.test.ts +177 -0
- package/tests/hooks/use-monitoring-data.test.ts +819 -0
- package/tests/hooks/use-provider-metadata.test.ts +228 -0
- package/tests/hooks/use-query-execution.test.ts +1212 -0
- package/tests/hooks/use-tab-manager.test.ts +756 -0
- package/tests/hooks/use-toast.test.ts +74 -0
- package/tests/hooks/use-transaction-control.test.ts +211 -0
- package/tests/integration/db/mongodb-provider.test.ts +698 -0
- package/tests/integration/db/mssql-provider.test.ts +840 -0
- package/tests/integration/db/mysql-provider.test.ts +872 -0
- package/tests/integration/db/oracle-provider.test.ts +843 -0
- package/tests/integration/db/postgres-provider.test.ts +1382 -0
- package/tests/integration/db/redis-provider.test.ts +526 -0
- package/tests/integration/db/sqlite-provider.test.ts +480 -0
- package/tests/integration/seed/seed-pipeline.test.ts +102 -0
- package/tests/isolated/factory-singleton.test.ts +150 -0
- package/tests/isolated/use-storage-sync.test.ts +389 -0
- package/tests/run-components.sh +196 -0
- package/tests/setup-dom.ts +58 -0
- package/tests/setup.ts +40 -0
- package/tests/unit/api-errors.test.ts +210 -0
- package/tests/unit/code-generator-functions.test.ts +271 -0
- package/tests/unit/components/column-list.test.tsx +190 -0
- package/tests/unit/components/data-import-modal.test.tsx +441 -0
- package/tests/unit/components/studio-mobile-header.test.tsx +327 -0
- package/tests/unit/data-charts-functions.test.ts +496 -0
- package/tests/unit/data-import-functions.test.ts +320 -0
- package/tests/unit/data-import-utils.test.ts +125 -0
- package/tests/unit/db/base-provider.test.ts +517 -0
- package/tests/unit/db/errors.test.ts +403 -0
- package/tests/unit/db/factory.test.ts +436 -0
- package/tests/unit/db/pool-manager.test.ts +440 -0
- package/tests/unit/db/query-limiter.test.ts +387 -0
- package/tests/unit/db/sql-base.test.ts +438 -0
- package/tests/unit/lib/api/error-codes.test.ts +39 -0
- package/tests/unit/lib/audit.test.ts +326 -0
- package/tests/unit/lib/auth.test.ts +146 -0
- package/tests/unit/lib/connection-string-parser.test.ts +424 -0
- package/tests/unit/lib/data-masking.test.ts +583 -0
- package/tests/unit/lib/db-icons.test.tsx +41 -0
- package/tests/unit/lib/monitoring-thresholds.test.ts +133 -0
- package/tests/unit/lib/oidc.test.ts +509 -0
- package/tests/unit/lib/query-generators.test.ts +127 -0
- package/tests/unit/lib/storage/factory.test.ts +71 -0
- package/tests/unit/lib/storage/local-storage.test.ts +114 -0
- package/tests/unit/lib/storage/providers/postgres.test.ts +312 -0
- package/tests/unit/lib/storage/providers/sqlite.test.ts +232 -0
- package/tests/unit/lib/storage/storage-facade-extended.test.ts +331 -0
- package/tests/unit/lib/storage/storage-facade.test.ts +184 -0
- package/tests/unit/lib/storage.test.ts +317 -0
- package/tests/unit/lib/time-series-buffer.test.ts +212 -0
- package/tests/unit/lib/utils.test.ts +24 -0
- package/tests/unit/llm/base-provider.test.ts +238 -0
- package/tests/unit/llm/config.test.ts +262 -0
- package/tests/unit/llm/custom-provider.test.ts +281 -0
- package/tests/unit/llm/gemini-provider.test.ts +248 -0
- package/tests/unit/llm/llm-factory.test.ts +155 -0
- package/tests/unit/llm/ollama-provider.test.ts +288 -0
- package/tests/unit/llm/openai-provider.test.ts +324 -0
- package/tests/unit/llm/retry.test.ts +180 -0
- package/tests/unit/llm/streaming.test.ts +355 -0
- package/tests/unit/logger.test.ts +198 -0
- package/tests/unit/mongodb-completions.test.ts +516 -0
- package/tests/unit/pivot-table-functions.test.ts +76 -0
- package/tests/unit/query-cancelled-error.test.ts +81 -0
- package/tests/unit/schema-diff/diff-engine.test.ts +367 -0
- package/tests/unit/schema-diff/migration-generator.test.ts +513 -0
- package/tests/unit/seed/config-loader.test.ts +73 -0
- package/tests/unit/seed/connection-filter.test.ts +91 -0
- package/tests/unit/seed/credential-resolver.test.ts +85 -0
- package/tests/unit/seed/index.test.ts +72 -0
- package/tests/unit/seed/resolve-connection.test.ts +74 -0
- package/tests/unit/seed/types.test.ts +129 -0
- package/tests/unit/sql/alias-extractor.test.ts +444 -0
- package/tests/unit/sql/statement-splitter.test.ts +348 -0
- package/tests/unit/sql-completions.test.ts +463 -0
- package/tests/unit/ssh-tunnel.test.ts +465 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
import '../setup-dom';
|
|
2
|
+
import '../helpers/mock-sonner';
|
|
3
|
+
import '../helpers/mock-navigation';
|
|
4
|
+
|
|
5
|
+
import { mock } from 'bun:test';
|
|
6
|
+
import { setupFramerMotionMock } from '../helpers/mock-monaco';
|
|
7
|
+
|
|
8
|
+
// Enhanced XYFlow mock that renders nodes via nodeTypes
|
|
9
|
+
mock.module('@xyflow/react', () => {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
|
+
const React = require('react');
|
|
12
|
+
return {
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
ReactFlow: ({ children, nodes = [], nodeTypes = {}, onNodeClick, onPaneClick }: Record<string, any>) => {
|
|
15
|
+
const renderedNodes = nodes.map((node: { id: string; type: string; data: Record<string, unknown> }) => {
|
|
16
|
+
const NodeComp = nodeTypes[node.type];
|
|
17
|
+
if (!NodeComp) return null;
|
|
18
|
+
return React.createElement('div', {
|
|
19
|
+
key: node.id,
|
|
20
|
+
'data-testid': `node-${node.id}`,
|
|
21
|
+
'data-node-id': node.id,
|
|
22
|
+
onClick: (e: React.MouseEvent) => { e.stopPropagation(); onNodeClick?.(e, node); },
|
|
23
|
+
}, React.createElement(NodeComp, { id: node.id, data: node.data, type: node.type }));
|
|
24
|
+
});
|
|
25
|
+
// Wrap nodes in a keyed container to avoid reconciliation issues
|
|
26
|
+
// when the number of nodes changes (e.g. during search filtering)
|
|
27
|
+
return React.createElement('div', {
|
|
28
|
+
'data-testid': 'mock-react-flow',
|
|
29
|
+
className: 'react-flow',
|
|
30
|
+
onClick: (e: React.MouseEvent) => { if (e.target === e.currentTarget) onPaneClick?.(); },
|
|
31
|
+
},
|
|
32
|
+
React.createElement('div', { key: '__nodes__', 'data-testid': 'nodes-container' }, renderedNodes),
|
|
33
|
+
React.createElement('svg', { key: '__svg__' }),
|
|
34
|
+
React.createElement(React.Fragment, { key: '__children__' }, children),
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
ReactFlowProvider: ({ children }: { children: unknown }) => children,
|
|
38
|
+
MiniMap: () => React.createElement('div', { 'data-testid': 'mock-minimap' }),
|
|
39
|
+
Controls: () => null,
|
|
40
|
+
Background: () => null,
|
|
41
|
+
Handle: () => null,
|
|
42
|
+
useNodesState: () => [[], mock(() => {}), mock(() => {})],
|
|
43
|
+
useEdgesState: () => [[], mock(() => {}), mock(() => {})],
|
|
44
|
+
useReactFlow: () => ({ fitView: mock(() => {}), getNodes: mock(() => []), getEdges: mock(() => []) }),
|
|
45
|
+
Position: { Top: 'top', Bottom: 'bottom', Left: 'left', Right: 'right' },
|
|
46
|
+
MarkerType: { ArrowClosed: 'arrowclosed' },
|
|
47
|
+
Panel: ({ children, position }: { children: unknown; position?: string }) =>
|
|
48
|
+
React.createElement('div', { 'data-testid': `mock-panel-${position || 'default'}` }, children),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
setupFramerMotionMock();
|
|
53
|
+
|
|
54
|
+
// Mock elkjs
|
|
55
|
+
mock.module('elkjs/lib/elk.bundled.js', () => ({
|
|
56
|
+
default: class MockELK {
|
|
57
|
+
layout(graph: unknown) {
|
|
58
|
+
return Promise.resolve(graph);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// Track html2canvas calls
|
|
64
|
+
const mockHtml2canvas = mock(() => Promise.resolve({
|
|
65
|
+
toDataURL: () => 'data:image/png;base64,mock',
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
mock.module('html2canvas', () => ({
|
|
69
|
+
default: mockHtml2canvas,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
73
|
+
import { render, fireEvent, within, cleanup, act } from '@testing-library/react';
|
|
74
|
+
import React from 'react';
|
|
75
|
+
|
|
76
|
+
import { SchemaDiagram } from '@/components/SchemaDiagram';
|
|
77
|
+
import { mockSchema, emptySchema } from '../fixtures/schemas';
|
|
78
|
+
import type { TableSchema } from '@/lib/types';
|
|
79
|
+
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// Test Data
|
|
82
|
+
// =============================================================================
|
|
83
|
+
|
|
84
|
+
// Schema with NO foreign keys at all (triggers heuristic fallback)
|
|
85
|
+
const schemaNoFK: TableSchema[] = [
|
|
86
|
+
{
|
|
87
|
+
name: 'users',
|
|
88
|
+
columns: [
|
|
89
|
+
{ name: 'id', type: 'integer', nullable: false, isPrimary: true },
|
|
90
|
+
{ name: 'name', type: 'varchar(255)', nullable: false, isPrimary: false },
|
|
91
|
+
],
|
|
92
|
+
indexes: [],
|
|
93
|
+
foreignKeys: [],
|
|
94
|
+
rowCount: 100,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'posts',
|
|
98
|
+
columns: [
|
|
99
|
+
{ name: 'id', type: 'integer', nullable: false, isPrimary: true },
|
|
100
|
+
{ name: 'title', type: 'text', nullable: false, isPrimary: false },
|
|
101
|
+
],
|
|
102
|
+
indexes: [],
|
|
103
|
+
foreignKeys: [],
|
|
104
|
+
rowCount: 50,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// Schema with heuristic _id column (no FK data, but column ends with _id)
|
|
109
|
+
const schemaHeuristic: TableSchema[] = [
|
|
110
|
+
{
|
|
111
|
+
name: 'users',
|
|
112
|
+
columns: [
|
|
113
|
+
{ name: 'id', type: 'integer', nullable: false, isPrimary: true },
|
|
114
|
+
{ name: 'email', type: 'varchar', nullable: true, isPrimary: false },
|
|
115
|
+
],
|
|
116
|
+
indexes: [],
|
|
117
|
+
foreignKeys: [],
|
|
118
|
+
rowCount: 10,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'comments',
|
|
122
|
+
columns: [
|
|
123
|
+
{ name: 'id', type: 'integer', nullable: false, isPrimary: true },
|
|
124
|
+
{ name: 'user_id', type: 'integer', nullable: false, isPrimary: false },
|
|
125
|
+
{ name: 'body', type: 'text', nullable: false, isPrimary: false },
|
|
126
|
+
],
|
|
127
|
+
indexes: [],
|
|
128
|
+
foreignKeys: [],
|
|
129
|
+
rowCount: 200,
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// Schema with heuristic _id column matching singular table name (no plural 's')
|
|
134
|
+
const schemaHeuristicSingular: TableSchema[] = [
|
|
135
|
+
{
|
|
136
|
+
name: 'author',
|
|
137
|
+
columns: [
|
|
138
|
+
{ name: 'id', type: 'integer', nullable: false, isPrimary: true },
|
|
139
|
+
{ name: 'name', type: 'varchar(255)', nullable: false, isPrimary: false },
|
|
140
|
+
],
|
|
141
|
+
indexes: [],
|
|
142
|
+
foreignKeys: [],
|
|
143
|
+
rowCount: 10,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'books',
|
|
147
|
+
columns: [
|
|
148
|
+
{ name: 'id', type: 'integer', nullable: false, isPrimary: true },
|
|
149
|
+
{ name: 'author_id', type: 'integer', nullable: false, isPrimary: false },
|
|
150
|
+
{ name: 'title', type: 'text', nullable: false, isPrimary: false },
|
|
151
|
+
],
|
|
152
|
+
indexes: [],
|
|
153
|
+
foreignKeys: [],
|
|
154
|
+
rowCount: 50,
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
// Schema with foreignKeys field omitted (tests `|| []` guards)
|
|
159
|
+
const schemaUndefinedFK: TableSchema[] = [
|
|
160
|
+
{
|
|
161
|
+
name: 'items',
|
|
162
|
+
columns: [
|
|
163
|
+
{ name: 'id', type: 'integer', nullable: false, isPrimary: true },
|
|
164
|
+
{ name: 'label', type: 'text', nullable: true, isPrimary: false },
|
|
165
|
+
],
|
|
166
|
+
indexes: [],
|
|
167
|
+
rowCount: 20,
|
|
168
|
+
} as TableSchema,
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
// Multi-FK schema for highlighting tests
|
|
172
|
+
const schemaMultiFK: TableSchema[] = [
|
|
173
|
+
{
|
|
174
|
+
name: 'users',
|
|
175
|
+
columns: [
|
|
176
|
+
{ name: 'id', type: 'integer', nullable: false, isPrimary: true },
|
|
177
|
+
{ name: 'name', type: 'varchar(255)', nullable: false, isPrimary: false },
|
|
178
|
+
],
|
|
179
|
+
indexes: [],
|
|
180
|
+
foreignKeys: [],
|
|
181
|
+
rowCount: 100,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'orders',
|
|
185
|
+
columns: [
|
|
186
|
+
{ name: 'id', type: 'integer', nullable: false, isPrimary: true },
|
|
187
|
+
{ name: 'user_id', type: 'integer', nullable: false, isPrimary: false },
|
|
188
|
+
{ name: 'total', type: 'numeric(10,2)', nullable: false, isPrimary: false },
|
|
189
|
+
],
|
|
190
|
+
indexes: [],
|
|
191
|
+
foreignKeys: [
|
|
192
|
+
{ columnName: 'user_id', referencedTable: 'users', referencedColumn: 'id' },
|
|
193
|
+
],
|
|
194
|
+
rowCount: 500,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'items',
|
|
198
|
+
columns: [
|
|
199
|
+
{ name: 'id', type: 'integer', nullable: false, isPrimary: true },
|
|
200
|
+
{ name: 'order_id', type: 'integer', nullable: false, isPrimary: false },
|
|
201
|
+
{ name: 'product', type: 'varchar(255)', nullable: false, isPrimary: false },
|
|
202
|
+
],
|
|
203
|
+
indexes: [],
|
|
204
|
+
foreignKeys: [
|
|
205
|
+
{ columnName: 'order_id', referencedTable: 'orders', referencedColumn: 'id' },
|
|
206
|
+
],
|
|
207
|
+
rowCount: 1000,
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
// Single table schema
|
|
212
|
+
const singleTableSchema: TableSchema[] = [
|
|
213
|
+
{
|
|
214
|
+
name: 'settings',
|
|
215
|
+
columns: [
|
|
216
|
+
{ name: 'key', type: 'text', nullable: false, isPrimary: true },
|
|
217
|
+
{ name: 'value', type: 'text', nullable: true, isPrimary: false },
|
|
218
|
+
],
|
|
219
|
+
indexes: [],
|
|
220
|
+
foreignKeys: [],
|
|
221
|
+
rowCount: 5,
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
// =============================================================================
|
|
226
|
+
// Helpers
|
|
227
|
+
// =============================================================================
|
|
228
|
+
|
|
229
|
+
function createDefaultProps(overrides: Partial<Parameters<typeof SchemaDiagram>[0]> = {}) {
|
|
230
|
+
return {
|
|
231
|
+
schema: mockSchema,
|
|
232
|
+
onClose: mock(() => {}),
|
|
233
|
+
...overrides,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// =============================================================================
|
|
238
|
+
// SchemaDiagram Tests
|
|
239
|
+
// =============================================================================
|
|
240
|
+
|
|
241
|
+
describe('SchemaDiagram', () => {
|
|
242
|
+
afterEach(() => {
|
|
243
|
+
cleanup();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
beforeEach(() => {
|
|
247
|
+
mockHtml2canvas.mockClear();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ── Rendering ───────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
test('renders ReactFlow container', () => {
|
|
253
|
+
const props = createDefaultProps();
|
|
254
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
255
|
+
|
|
256
|
+
expect(container.querySelector('[data-testid="mock-react-flow"]')).not.toBeNull();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('shows top-right panel with buttons', () => {
|
|
260
|
+
const props = createDefaultProps();
|
|
261
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
262
|
+
|
|
263
|
+
expect(container.querySelector('[data-testid="mock-panel-top-right"]')).not.toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('shows top-left info panel', () => {
|
|
267
|
+
const props = createDefaultProps();
|
|
268
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
269
|
+
|
|
270
|
+
expect(container.querySelector('[data-testid="mock-panel-top-left"]')).not.toBeNull();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('renders ERD Visualizer heading', () => {
|
|
274
|
+
const props = createDefaultProps();
|
|
275
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
276
|
+
const view = within(container);
|
|
277
|
+
|
|
278
|
+
expect(view.queryByText('ERD Visualizer')).not.toBeNull();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ── Close button ────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
test('onClose fires when close button clicked', () => {
|
|
284
|
+
const onClose = mock(() => {});
|
|
285
|
+
const props = createDefaultProps({ onClose });
|
|
286
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
287
|
+
|
|
288
|
+
const closeButton = Array.from(container.querySelectorAll('button')).find(btn =>
|
|
289
|
+
btn.className.includes('rounded-full')
|
|
290
|
+
);
|
|
291
|
+
expect(closeButton).not.toBeNull();
|
|
292
|
+
|
|
293
|
+
fireEvent.click(closeButton!);
|
|
294
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// ── Table count and relationships ───────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
test('shows table info from schema in ERD panel', () => {
|
|
300
|
+
const props = createDefaultProps();
|
|
301
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
302
|
+
const view = within(container);
|
|
303
|
+
|
|
304
|
+
expect(view.queryByText('3 tables')).not.toBeNull();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('shows relationship count', () => {
|
|
308
|
+
const props = createDefaultProps();
|
|
309
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
310
|
+
const view = within(container);
|
|
311
|
+
|
|
312
|
+
// mockSchema has orders → users FK, so 1 relationship
|
|
313
|
+
expect(view.queryByText('1 relationships')).not.toBeNull();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('shows 0 relationships for schema without FKs', () => {
|
|
317
|
+
const props = createDefaultProps({ schema: schemaNoFK });
|
|
318
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
319
|
+
const view = within(container);
|
|
320
|
+
|
|
321
|
+
expect(view.queryByText('0 relationships')).not.toBeNull();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('shows heuristic relationships count for _id columns', () => {
|
|
325
|
+
const props = createDefaultProps({ schema: schemaHeuristic });
|
|
326
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
327
|
+
const view = within(container);
|
|
328
|
+
|
|
329
|
+
// comments.user_id → users heuristic edge
|
|
330
|
+
expect(view.queryByText('1 relationships')).not.toBeNull();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('shows single table count', () => {
|
|
334
|
+
const props = createDefaultProps({ schema: singleTableSchema });
|
|
335
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
336
|
+
const view = within(container);
|
|
337
|
+
|
|
338
|
+
expect(view.queryByText('1 tables')).not.toBeNull();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ── Export buttons ──────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
test('export buttons present (PNG, SVG)', () => {
|
|
344
|
+
const props = createDefaultProps();
|
|
345
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
346
|
+
const view = within(container);
|
|
347
|
+
|
|
348
|
+
expect(view.queryByText('PNG')).not.toBeNull();
|
|
349
|
+
expect(view.queryByText('SVG')).not.toBeNull();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('PNG export button click does not crash', () => {
|
|
353
|
+
const props = createDefaultProps();
|
|
354
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
355
|
+
const view = within(container);
|
|
356
|
+
|
|
357
|
+
const pngButton = view.getByText('PNG').closest('button')!;
|
|
358
|
+
fireEvent.click(pngButton);
|
|
359
|
+
// Should not throw
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('SVG export button click does not crash', () => {
|
|
363
|
+
const props = createDefaultProps();
|
|
364
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
365
|
+
const view = within(container);
|
|
366
|
+
|
|
367
|
+
const svgButton = view.getByText('SVG').closest('button')!;
|
|
368
|
+
fireEvent.click(svgButton);
|
|
369
|
+
// Should not throw
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ── Search input ────────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
test('search input present with placeholder', () => {
|
|
375
|
+
const props = createDefaultProps();
|
|
376
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
377
|
+
const view = within(container);
|
|
378
|
+
|
|
379
|
+
expect(view.queryByPlaceholderText('Filter tables...')).not.toBeNull();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('search filters tables and updates count', () => {
|
|
383
|
+
const props = createDefaultProps();
|
|
384
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
385
|
+
const view = within(container);
|
|
386
|
+
|
|
387
|
+
// Initially 3 tables
|
|
388
|
+
expect(view.queryByText('3 tables')).not.toBeNull();
|
|
389
|
+
|
|
390
|
+
const searchInput = view.getByPlaceholderText('Filter tables...');
|
|
391
|
+
fireEvent.change(searchInput, { target: { value: 'users' } });
|
|
392
|
+
|
|
393
|
+
// After filtering, only 1 table matches
|
|
394
|
+
expect(view.queryByText('1 tables')).not.toBeNull();
|
|
395
|
+
expect(view.queryByText('3 tables')).toBeNull();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('search is case-insensitive', () => {
|
|
399
|
+
const props = createDefaultProps();
|
|
400
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
401
|
+
const view = within(container);
|
|
402
|
+
|
|
403
|
+
const searchInput = view.getByPlaceholderText('Filter tables...');
|
|
404
|
+
fireEvent.change(searchInput, { target: { value: 'ORDERS' } });
|
|
405
|
+
|
|
406
|
+
expect(view.queryByText('1 tables')).not.toBeNull();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('search with no matches shows 0 tables', () => {
|
|
410
|
+
const props = createDefaultProps();
|
|
411
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
412
|
+
const view = within(container);
|
|
413
|
+
|
|
414
|
+
const searchInput = view.getByPlaceholderText('Filter tables...');
|
|
415
|
+
fireEvent.change(searchInput, { target: { value: 'nonexistent' } });
|
|
416
|
+
|
|
417
|
+
expect(view.queryByText('0 tables')).not.toBeNull();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('clearing search restores all tables', () => {
|
|
421
|
+
const props = createDefaultProps();
|
|
422
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
423
|
+
const view = within(container);
|
|
424
|
+
|
|
425
|
+
const searchInput = view.getByPlaceholderText('Filter tables...');
|
|
426
|
+
|
|
427
|
+
// Type to filter
|
|
428
|
+
fireEvent.change(searchInput, { target: { value: 'users' } });
|
|
429
|
+
expect(view.queryByText('1 tables')).not.toBeNull();
|
|
430
|
+
|
|
431
|
+
// Clear the search
|
|
432
|
+
fireEvent.change(searchInput, { target: { value: '' } });
|
|
433
|
+
expect(view.queryByText('3 tables')).not.toBeNull();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// ── Compact mode toggle ─────────────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
test('compact mode toggle present showing "Compact" initially', () => {
|
|
439
|
+
const props = createDefaultProps();
|
|
440
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
441
|
+
const view = within(container);
|
|
442
|
+
|
|
443
|
+
expect(view.queryByText('Compact')).not.toBeNull();
|
|
444
|
+
expect(view.queryByText('Detail')).toBeNull();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('clicking Compact toggles to Detail', () => {
|
|
448
|
+
const props = createDefaultProps();
|
|
449
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
450
|
+
const view = within(container);
|
|
451
|
+
|
|
452
|
+
const compactButton = view.getByText('Compact').closest('button')!;
|
|
453
|
+
fireEvent.click(compactButton);
|
|
454
|
+
|
|
455
|
+
expect(view.queryByText('Detail')).not.toBeNull();
|
|
456
|
+
expect(view.queryByText('Compact')).toBeNull();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test('clicking Detail toggles back to Compact', () => {
|
|
460
|
+
const props = createDefaultProps();
|
|
461
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
462
|
+
const view = within(container);
|
|
463
|
+
|
|
464
|
+
// Toggle to compact
|
|
465
|
+
const compactButton = view.getByText('Compact').closest('button')!;
|
|
466
|
+
fireEvent.click(compactButton);
|
|
467
|
+
expect(view.queryByText('Detail')).not.toBeNull();
|
|
468
|
+
|
|
469
|
+
// Toggle back to detail
|
|
470
|
+
const detailButton = view.getByText('Detail').closest('button')!;
|
|
471
|
+
fireEvent.click(detailButton);
|
|
472
|
+
expect(view.queryByText('Compact')).not.toBeNull();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('compact button has blue text class when compact mode is active', () => {
|
|
476
|
+
const props = createDefaultProps();
|
|
477
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
478
|
+
const view = within(container);
|
|
479
|
+
|
|
480
|
+
const compactButton = view.getByText('Compact').closest('button')!;
|
|
481
|
+
expect(compactButton.className).not.toContain('text-blue-400');
|
|
482
|
+
|
|
483
|
+
fireEvent.click(compactButton);
|
|
484
|
+
const detailButton = view.getByText('Detail').closest('button')!;
|
|
485
|
+
expect(detailButton.className).toContain('text-blue-400');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// ── No FK warning ───────────────────────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
test('shows no-FK warning when schema has no foreign keys', () => {
|
|
491
|
+
const props = createDefaultProps({ schema: schemaNoFK });
|
|
492
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
493
|
+
const view = within(container);
|
|
494
|
+
|
|
495
|
+
expect(view.queryByText(/No FK data available/)).not.toBeNull();
|
|
496
|
+
expect(view.queryByText(/heuristic relationships/)).not.toBeNull();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('does not show no-FK warning when schema has foreign keys', () => {
|
|
500
|
+
const props = createDefaultProps(); // mockSchema has FK on orders
|
|
501
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
502
|
+
const view = within(container);
|
|
503
|
+
|
|
504
|
+
expect(view.queryByText(/No FK data available/)).toBeNull();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// ── Selected node info ──────────────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
test('does not show selected node info by default', () => {
|
|
510
|
+
const props = createDefaultProps();
|
|
511
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
512
|
+
const view = within(container);
|
|
513
|
+
|
|
514
|
+
expect(view.queryByText('Selected:')).toBeNull();
|
|
515
|
+
expect(view.queryByText('clear')).toBeNull();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// ── Empty schema / loading state ────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
test('empty schema shows loading/generating state', () => {
|
|
521
|
+
const props = createDefaultProps({ schema: emptySchema });
|
|
522
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
523
|
+
const view = within(container);
|
|
524
|
+
|
|
525
|
+
expect(view.queryByText('Generating ERD Diagram...')).not.toBeNull();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('empty schema does not render ReactFlow', () => {
|
|
529
|
+
const props = createDefaultProps({ schema: emptySchema });
|
|
530
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
531
|
+
|
|
532
|
+
expect(container.querySelector('[data-testid="mock-react-flow"]')).toBeNull();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test('empty schema does not render panels', () => {
|
|
536
|
+
const props = createDefaultProps({ schema: emptySchema });
|
|
537
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
538
|
+
const view = within(container);
|
|
539
|
+
|
|
540
|
+
expect(view.queryByText('ERD Visualizer')).toBeNull();
|
|
541
|
+
expect(view.queryByText('PNG')).toBeNull();
|
|
542
|
+
expect(view.queryByText('Compact')).toBeNull();
|
|
543
|
+
expect(view.queryByPlaceholderText('Filter tables...')).toBeNull();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// ── Search affects edge count ───────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
test('filtering to table with FK shows its relationships', () => {
|
|
549
|
+
const props = createDefaultProps();
|
|
550
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
551
|
+
const view = within(container);
|
|
552
|
+
|
|
553
|
+
// Search for "orders" — has FK to users, but users is filtered out
|
|
554
|
+
const searchInput = view.getByPlaceholderText('Filter tables...');
|
|
555
|
+
fireEvent.change(searchInput, { target: { value: 'orders' } });
|
|
556
|
+
|
|
557
|
+
// Only orders table visible, users is filtered out → FK edge excluded (target not in set)
|
|
558
|
+
expect(view.queryByText('1 tables')).not.toBeNull();
|
|
559
|
+
expect(view.queryByText('0 relationships')).not.toBeNull();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// ── Heuristic edge detection ────────────────────────────────────────────
|
|
563
|
+
|
|
564
|
+
test('heuristic edges are created for _id columns when no FK data', () => {
|
|
565
|
+
const props = createDefaultProps({ schema: schemaHeuristic });
|
|
566
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
567
|
+
const view = within(container);
|
|
568
|
+
|
|
569
|
+
// comments has user_id → should heuristically link to users
|
|
570
|
+
expect(view.queryByText('1 relationships')).not.toBeNull();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test('heuristic edges not created when real FK data exists', () => {
|
|
574
|
+
// mockSchema has real FK on orders.user_id → users.id
|
|
575
|
+
// so heuristic fallback should NOT run
|
|
576
|
+
const props = createDefaultProps();
|
|
577
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
578
|
+
const view = within(container);
|
|
579
|
+
|
|
580
|
+
// Only 1 real FK edge, no extra heuristic
|
|
581
|
+
expect(view.queryByText('1 relationships')).not.toBeNull();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// ── Multiple re-renders don't crash ─────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
test('re-rendering with different schema does not crash', () => {
|
|
587
|
+
const onClose = mock(() => {});
|
|
588
|
+
const { container, rerender } = render(
|
|
589
|
+
<SchemaDiagram schema={mockSchema} onClose={onClose} />
|
|
590
|
+
);
|
|
591
|
+
const view = within(container);
|
|
592
|
+
expect(view.queryByText('3 tables')).not.toBeNull();
|
|
593
|
+
|
|
594
|
+
rerender(<SchemaDiagram schema={singleTableSchema} onClose={onClose} />);
|
|
595
|
+
expect(view.queryByText('1 tables')).not.toBeNull();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// ── Panel buttons ─────────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
test('top-right panel has PNG, SVG, Compact, and close buttons', () => {
|
|
601
|
+
const props = createDefaultProps();
|
|
602
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
603
|
+
const view = within(container);
|
|
604
|
+
expect(view.queryByText('PNG')).not.toBeNull();
|
|
605
|
+
expect(view.queryByText('SVG')).not.toBeNull();
|
|
606
|
+
expect(view.queryByText('Compact')).not.toBeNull();
|
|
607
|
+
// Close button (X icon)
|
|
608
|
+
const closeBtn = Array.from(container.querySelectorAll('button')).find(btn =>
|
|
609
|
+
btn.className.includes('rounded-full')
|
|
610
|
+
);
|
|
611
|
+
expect(closeBtn).not.toBeNull();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// ── MiniMap rendered ─────────────────────────────────────────────────
|
|
615
|
+
|
|
616
|
+
test('MiniMap component renders', () => {
|
|
617
|
+
const props = createDefaultProps();
|
|
618
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
619
|
+
expect(container.querySelector('[data-testid="mock-minimap"]')).not.toBeNull();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// ── Schema with many tables ─────────────────────────────────────────
|
|
623
|
+
|
|
624
|
+
test('schema with many tables renders correct count', () => {
|
|
625
|
+
const manyTables: TableSchema[] = Array.from({ length: 10 }, (_, i) => ({
|
|
626
|
+
name: `table_${i}`,
|
|
627
|
+
columns: [{ name: 'id', type: 'integer', nullable: false, isPrimary: true }],
|
|
628
|
+
indexes: [],
|
|
629
|
+
foreignKeys: [],
|
|
630
|
+
rowCount: i * 10,
|
|
631
|
+
}));
|
|
632
|
+
const props = createDefaultProps({ schema: manyTables });
|
|
633
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
634
|
+
const view = within(container);
|
|
635
|
+
expect(view.queryByText('10 tables')).not.toBeNull();
|
|
636
|
+
expect(view.queryByText('0 relationships')).not.toBeNull();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// ── Search with partial match ───────────────────────────────────────
|
|
640
|
+
|
|
641
|
+
test('search with partial match filters correctly', () => {
|
|
642
|
+
const props = createDefaultProps();
|
|
643
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
644
|
+
const view = within(container);
|
|
645
|
+
|
|
646
|
+
const searchInput = view.getByPlaceholderText('Filter tables...');
|
|
647
|
+
fireEvent.change(searchInput, { target: { value: 'ord' } });
|
|
648
|
+
// 'orders' matches 'ord'
|
|
649
|
+
expect(view.queryByText('1 tables')).not.toBeNull();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
653
|
+
// NEW: TableNode Rendering Tests
|
|
654
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
655
|
+
|
|
656
|
+
describe('TableNode rendering', () => {
|
|
657
|
+
test('renders table name in header', () => {
|
|
658
|
+
const props = createDefaultProps();
|
|
659
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
660
|
+
const view = within(container);
|
|
661
|
+
|
|
662
|
+
// Each table name should appear in uppercase in the header
|
|
663
|
+
expect(view.queryByText('users')).not.toBeNull();
|
|
664
|
+
expect(view.queryByText('orders')).not.toBeNull();
|
|
665
|
+
expect(view.queryByText('products')).not.toBeNull();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test('shows column count badge', () => {
|
|
669
|
+
const props = createDefaultProps();
|
|
670
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
671
|
+
const view = within(container);
|
|
672
|
+
|
|
673
|
+
// users has 6 columns, orders has 5, products has 4
|
|
674
|
+
expect(view.queryByText('6 cols')).not.toBeNull();
|
|
675
|
+
expect(view.queryByText('5 cols')).not.toBeNull();
|
|
676
|
+
expect(view.queryByText('4 cols')).not.toBeNull();
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test('displays column names', () => {
|
|
680
|
+
const props = createDefaultProps({ schema: singleTableSchema });
|
|
681
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
682
|
+
const view = within(container);
|
|
683
|
+
|
|
684
|
+
expect(view.queryByText('key')).not.toBeNull();
|
|
685
|
+
expect(view.queryByText('value')).not.toBeNull();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test('displays column type text', () => {
|
|
689
|
+
const props = createDefaultProps({ schema: singleTableSchema });
|
|
690
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
691
|
+
|
|
692
|
+
// Column types should be rendered in uppercase
|
|
693
|
+
const texts = Array.from(container.querySelectorAll('.font-mono'));
|
|
694
|
+
const typeTexts = texts.map(el => el.textContent);
|
|
695
|
+
expect(typeTexts).toContain('text');
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test('shows NN for NOT NULL columns', () => {
|
|
699
|
+
const props = createDefaultProps({ schema: singleTableSchema });
|
|
700
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
701
|
+
|
|
702
|
+
// 'key' column has nullable: false
|
|
703
|
+
const nnElements = container.querySelectorAll('span');
|
|
704
|
+
const nnTexts = Array.from(nnElements).map(el => el.textContent);
|
|
705
|
+
expect(nnTexts).toContain('NN');
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test('compact mode hides column details', () => {
|
|
709
|
+
const props = createDefaultProps({ schema: singleTableSchema });
|
|
710
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
711
|
+
const view = within(container);
|
|
712
|
+
|
|
713
|
+
// Before compact — columns visible
|
|
714
|
+
expect(view.queryByText('key')).not.toBeNull();
|
|
715
|
+
expect(view.queryByText('value')).not.toBeNull();
|
|
716
|
+
|
|
717
|
+
// Toggle compact mode
|
|
718
|
+
const compactButton = view.getByText('Compact').closest('button')!;
|
|
719
|
+
fireEvent.click(compactButton);
|
|
720
|
+
|
|
721
|
+
// In compact mode, columns should be hidden (only header visible)
|
|
722
|
+
// The header still shows settings and "2 cols"
|
|
723
|
+
expect(view.queryByText('settings')).not.toBeNull();
|
|
724
|
+
expect(view.queryByText('2 cols')).not.toBeNull();
|
|
725
|
+
// Column names should not appear as separate elements in the columns list
|
|
726
|
+
// key/value are column names, but the column list section is hidden in compact
|
|
727
|
+
const nodeEl = container.querySelector('[data-node-id="settings"]');
|
|
728
|
+
expect(nodeEl).not.toBeNull();
|
|
729
|
+
// In compact mode, the p-1 div with columns is not rendered
|
|
730
|
+
// We check that column type badges disappear
|
|
731
|
+
const fontMonoElements = nodeEl!.querySelectorAll('.font-mono');
|
|
732
|
+
expect(fontMonoElements.length).toBe(0);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test('renders node for each table in schema', () => {
|
|
736
|
+
const props = createDefaultProps();
|
|
737
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
738
|
+
|
|
739
|
+
expect(container.querySelector('[data-node-id="users"]')).not.toBeNull();
|
|
740
|
+
expect(container.querySelector('[data-node-id="orders"]')).not.toBeNull();
|
|
741
|
+
expect(container.querySelector('[data-node-id="products"]')).not.toBeNull();
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test('node with empty/null data returns nothing', () => {
|
|
745
|
+
// Schema with a valid table ensures at least one node renders
|
|
746
|
+
// The guard `if (!data) return null; if (!table) return null;` is tested
|
|
747
|
+
// by the fact that the enhanced mock passes correct data through
|
|
748
|
+
const props = createDefaultProps({ schema: singleTableSchema });
|
|
749
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
750
|
+
|
|
751
|
+
const nodeEl = container.querySelector('[data-node-id="settings"]');
|
|
752
|
+
expect(nodeEl).not.toBeNull();
|
|
753
|
+
// The node should have content (table header)
|
|
754
|
+
expect(nodeEl!.textContent).toContain('settings');
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
759
|
+
// NEW: Node Selection Tests
|
|
760
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
761
|
+
|
|
762
|
+
describe('Node selection', () => {
|
|
763
|
+
test('clicking a node shows "Selected:" info panel', () => {
|
|
764
|
+
const props = createDefaultProps();
|
|
765
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
766
|
+
const view = within(container);
|
|
767
|
+
|
|
768
|
+
// Initially no selection
|
|
769
|
+
expect(view.queryByText('Selected:')).toBeNull();
|
|
770
|
+
|
|
771
|
+
// Click the users node
|
|
772
|
+
const usersNode = container.querySelector('[data-node-id="users"]')!;
|
|
773
|
+
fireEvent.click(usersNode);
|
|
774
|
+
|
|
775
|
+
// Selection should appear with selected node name and clear button
|
|
776
|
+
expect(view.queryByText('Selected:')).not.toBeNull();
|
|
777
|
+
// The selected table name appears in a font-mono span
|
|
778
|
+
const selectedSpan = container.querySelector('.font-mono.font-bold');
|
|
779
|
+
expect(selectedSpan).not.toBeNull();
|
|
780
|
+
expect(selectedSpan!.textContent).toBe('users');
|
|
781
|
+
expect(view.queryByText('clear')).not.toBeNull();
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test('clicking the same node again deselects (toggle)', () => {
|
|
785
|
+
const props = createDefaultProps();
|
|
786
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
787
|
+
const view = within(container);
|
|
788
|
+
|
|
789
|
+
const usersNode = container.querySelector('[data-node-id="users"]')!;
|
|
790
|
+
|
|
791
|
+
// Select
|
|
792
|
+
fireEvent.click(usersNode);
|
|
793
|
+
expect(view.queryByText('Selected:')).not.toBeNull();
|
|
794
|
+
|
|
795
|
+
// Deselect
|
|
796
|
+
fireEvent.click(usersNode);
|
|
797
|
+
expect(view.queryByText('Selected:')).toBeNull();
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
test('clicking "clear" button clears selection', () => {
|
|
801
|
+
const props = createDefaultProps();
|
|
802
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
803
|
+
const view = within(container);
|
|
804
|
+
|
|
805
|
+
// Select a node
|
|
806
|
+
const usersNode = container.querySelector('[data-node-id="users"]')!;
|
|
807
|
+
fireEvent.click(usersNode);
|
|
808
|
+
expect(view.queryByText('Selected:')).not.toBeNull();
|
|
809
|
+
|
|
810
|
+
// Click clear
|
|
811
|
+
const clearButton = view.getByText('clear');
|
|
812
|
+
fireEvent.click(clearButton);
|
|
813
|
+
expect(view.queryByText('Selected:')).toBeNull();
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test('clicking pane background clears selection', () => {
|
|
817
|
+
const props = createDefaultProps();
|
|
818
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
819
|
+
const view = within(container);
|
|
820
|
+
|
|
821
|
+
// Select a node
|
|
822
|
+
const usersNode = container.querySelector('[data-node-id="users"]')!;
|
|
823
|
+
fireEvent.click(usersNode);
|
|
824
|
+
expect(view.queryByText('Selected:')).not.toBeNull();
|
|
825
|
+
|
|
826
|
+
// Click the pane background (the react-flow container itself)
|
|
827
|
+
const reactFlowContainer = container.querySelector('[data-testid="mock-react-flow"]')!;
|
|
828
|
+
// Fire click directly on the container element (target === currentTarget)
|
|
829
|
+
fireEvent.click(reactFlowContainer);
|
|
830
|
+
expect(view.queryByText('Selected:')).toBeNull();
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
835
|
+
// NEW: Node/Edge Highlighting Tests
|
|
836
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
837
|
+
|
|
838
|
+
describe('Node/Edge highlighting', () => {
|
|
839
|
+
test('selected node gets highlighted (blue border)', () => {
|
|
840
|
+
const props = createDefaultProps();
|
|
841
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
842
|
+
|
|
843
|
+
// Click users node
|
|
844
|
+
const usersNode = container.querySelector('[data-node-id="users"]')!;
|
|
845
|
+
fireEvent.click(usersNode);
|
|
846
|
+
|
|
847
|
+
// The TableNode's root div inside the data-node-id div should have blue border
|
|
848
|
+
const innerDiv = usersNode.querySelector('.border-blue-500\\/60');
|
|
849
|
+
expect(innerDiv).not.toBeNull();
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test('FK target of selected node is highlighted', () => {
|
|
853
|
+
const props = createDefaultProps();
|
|
854
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
855
|
+
|
|
856
|
+
// Select 'orders' which has FK to 'users'
|
|
857
|
+
const ordersNode = container.querySelector('[data-node-id="orders"]')!;
|
|
858
|
+
fireEvent.click(ordersNode);
|
|
859
|
+
|
|
860
|
+
// The 'users' table should also be highlighted (FK target)
|
|
861
|
+
const usersNode = container.querySelector('[data-node-id="users"]')!;
|
|
862
|
+
const usersInner = usersNode.querySelector('.border-blue-500\\/60');
|
|
863
|
+
expect(usersInner).not.toBeNull();
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
test('FK source of selected node is highlighted', () => {
|
|
867
|
+
const props = createDefaultProps();
|
|
868
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
869
|
+
|
|
870
|
+
// Select 'users' — orders has FK pointing to users
|
|
871
|
+
const usersNode = container.querySelector('[data-node-id="users"]')!;
|
|
872
|
+
fireEvent.click(usersNode);
|
|
873
|
+
|
|
874
|
+
// The 'orders' table should be highlighted (it references users via FK)
|
|
875
|
+
const ordersNode = container.querySelector('[data-node-id="orders"]')!;
|
|
876
|
+
const ordersInner = ordersNode.querySelector('.border-blue-500\\/60');
|
|
877
|
+
expect(ordersInner).not.toBeNull();
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
test('non-related node is NOT highlighted when another is selected', () => {
|
|
881
|
+
const props = createDefaultProps();
|
|
882
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
883
|
+
|
|
884
|
+
// Select 'orders' (related to users via FK, not related to products)
|
|
885
|
+
const ordersNode = container.querySelector('[data-node-id="orders"]')!;
|
|
886
|
+
fireEvent.click(ordersNode);
|
|
887
|
+
|
|
888
|
+
// Products should NOT be highlighted
|
|
889
|
+
const productsNode = container.querySelector('[data-node-id="products"]')!;
|
|
890
|
+
const productsInner = productsNode.querySelector('.border-blue-500\\/60');
|
|
891
|
+
expect(productsInner).toBeNull();
|
|
892
|
+
// Products should have default border
|
|
893
|
+
const productsDefault = productsNode.querySelector('.border-white\\/10');
|
|
894
|
+
expect(productsDefault).not.toBeNull();
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
899
|
+
// NEW: Export Internals Tests
|
|
900
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
901
|
+
|
|
902
|
+
describe('Export functionality', () => {
|
|
903
|
+
test('PNG export calls html2canvas and creates download link', async () => {
|
|
904
|
+
const clickMock = mock(() => {});
|
|
905
|
+
const originalCreateElement = document.createElement.bind(document);
|
|
906
|
+
const createElementSpy = mock((tag: string) => {
|
|
907
|
+
const el = originalCreateElement(tag);
|
|
908
|
+
if (tag === 'a') {
|
|
909
|
+
Object.defineProperty(el, 'click', { value: clickMock });
|
|
910
|
+
}
|
|
911
|
+
return el;
|
|
912
|
+
});
|
|
913
|
+
document.createElement = createElementSpy as unknown as typeof document.createElement;
|
|
914
|
+
|
|
915
|
+
const props = createDefaultProps();
|
|
916
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
917
|
+
const view = within(container);
|
|
918
|
+
|
|
919
|
+
const pngButton = view.getByText('PNG').closest('button')!;
|
|
920
|
+
await act(async () => {
|
|
921
|
+
fireEvent.click(pngButton);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// html2canvas should have been called
|
|
925
|
+
expect(mockHtml2canvas).toHaveBeenCalledTimes(1);
|
|
926
|
+
|
|
927
|
+
// Wait for the async chain
|
|
928
|
+
await act(async () => {
|
|
929
|
+
await new Promise(r => setTimeout(r, 10));
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
expect(clickMock).toHaveBeenCalled();
|
|
933
|
+
|
|
934
|
+
// Restore
|
|
935
|
+
document.createElement = originalCreateElement;
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test('SVG export uses XMLSerializer and creates download link', async () => {
|
|
939
|
+
const clickMock = mock(() => {});
|
|
940
|
+
const revokeObjectURLMock = mock(() => {});
|
|
941
|
+
const originalCreateElement = document.createElement.bind(document);
|
|
942
|
+
const createElementSpy = mock((tag: string) => {
|
|
943
|
+
const el = originalCreateElement(tag);
|
|
944
|
+
if (tag === 'a') {
|
|
945
|
+
Object.defineProperty(el, 'click', { value: clickMock });
|
|
946
|
+
}
|
|
947
|
+
return el;
|
|
948
|
+
});
|
|
949
|
+
document.createElement = createElementSpy as unknown as typeof document.createElement;
|
|
950
|
+
|
|
951
|
+
const originalRevokeObjectURL = URL.revokeObjectURL;
|
|
952
|
+
URL.revokeObjectURL = revokeObjectURLMock;
|
|
953
|
+
|
|
954
|
+
const props = createDefaultProps();
|
|
955
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
956
|
+
const view = within(container);
|
|
957
|
+
|
|
958
|
+
const svgButton = view.getByText('SVG').closest('button')!;
|
|
959
|
+
await act(async () => {
|
|
960
|
+
fireEvent.click(svgButton);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// Wait for async chain
|
|
964
|
+
await act(async () => {
|
|
965
|
+
await new Promise(r => setTimeout(r, 10));
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
expect(clickMock).toHaveBeenCalled();
|
|
969
|
+
expect(revokeObjectURLMock).toHaveBeenCalled();
|
|
970
|
+
|
|
971
|
+
// Restore
|
|
972
|
+
document.createElement = originalCreateElement;
|
|
973
|
+
URL.revokeObjectURL = originalRevokeObjectURL;
|
|
974
|
+
});
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
978
|
+
// NEW: Edge Construction & Misc Tests
|
|
979
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
980
|
+
|
|
981
|
+
describe('Edge construction and misc', () => {
|
|
982
|
+
test('heuristic matches singular table name (author_id → author)', () => {
|
|
983
|
+
const props = createDefaultProps({ schema: schemaHeuristicSingular });
|
|
984
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
985
|
+
const view = within(container);
|
|
986
|
+
|
|
987
|
+
// books.author_id → author (singular match, not authors)
|
|
988
|
+
expect(view.queryByText('1 relationships')).not.toBeNull();
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
test('schema with undefined foreignKeys does not crash', () => {
|
|
992
|
+
const props = createDefaultProps({ schema: schemaUndefinedFK });
|
|
993
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
994
|
+
const view = within(container);
|
|
995
|
+
|
|
996
|
+
expect(view.queryByText('1 tables')).not.toBeNull();
|
|
997
|
+
expect(view.queryByText('0 relationships')).not.toBeNull();
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test('multi-FK schema shows correct relationship count', () => {
|
|
1001
|
+
const props = createDefaultProps({ schema: schemaMultiFK });
|
|
1002
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
1003
|
+
const view = within(container);
|
|
1004
|
+
|
|
1005
|
+
// orders→users + items→orders = 2 relationships
|
|
1006
|
+
expect(view.queryByText('2 relationships')).not.toBeNull();
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
test('multi-FK: selecting middle node highlights both connected nodes', () => {
|
|
1010
|
+
const props = createDefaultProps({ schema: schemaMultiFK });
|
|
1011
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
1012
|
+
|
|
1013
|
+
// Select 'orders' which is FK target of 'items' and FK source pointing to 'users'
|
|
1014
|
+
const ordersNode = container.querySelector('[data-node-id="orders"]')!;
|
|
1015
|
+
fireEvent.click(ordersNode);
|
|
1016
|
+
|
|
1017
|
+
// 'users' should be highlighted (orders has FK to users)
|
|
1018
|
+
const usersNode = container.querySelector('[data-node-id="users"]')!;
|
|
1019
|
+
expect(usersNode.querySelector('.border-blue-500\\/60')).not.toBeNull();
|
|
1020
|
+
|
|
1021
|
+
// 'items' should be highlighted (items has FK to orders)
|
|
1022
|
+
const itemsNode = container.querySelector('[data-node-id="items"]')!;
|
|
1023
|
+
expect(itemsNode.querySelector('.border-blue-500\\/60')).not.toBeNull();
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test('no-FK warning shown for schema with undefined foreignKeys', () => {
|
|
1027
|
+
const props = createDefaultProps({ schema: schemaUndefinedFK });
|
|
1028
|
+
const { container } = render(<SchemaDiagram {...props} />);
|
|
1029
|
+
const view = within(container);
|
|
1030
|
+
|
|
1031
|
+
expect(view.queryByText(/No FK data available/)).not.toBeNull();
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
});
|