@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,804 @@
|
|
|
1
|
+
import '../setup-dom';
|
|
2
|
+
import '../helpers/mock-sonner';
|
|
3
|
+
import '../helpers/mock-navigation';
|
|
4
|
+
|
|
5
|
+
import { mock } from 'bun:test';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
// ── Mock framer-motion ──────────────────────────────────────────────────────
|
|
9
|
+
mock.module('framer-motion', () => {
|
|
10
|
+
const passthrough = ({ children, ...props }: Record<string, unknown>) =>
|
|
11
|
+
React.createElement('div', props, children as React.ReactNode);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
motion: new Proxy({}, {
|
|
15
|
+
get: () => passthrough,
|
|
16
|
+
}),
|
|
17
|
+
AnimatePresence: ({ children }: { children: React.ReactNode }) => children,
|
|
18
|
+
useAnimation: () => ({ start: mock(() => {}), stop: mock(() => {}) }),
|
|
19
|
+
useInView: () => true,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// ── Mock data-masking ───────────────────────────────────────────────────────
|
|
24
|
+
const mockShouldMask = mock(() => false);
|
|
25
|
+
const mockCanToggleMasking = mock(() => true);
|
|
26
|
+
const mockCanReveal = mock(() => true);
|
|
27
|
+
const mockDetectSensitiveColumnsFromConfig = mock(() => new Map());
|
|
28
|
+
const mockMaskValueByPattern = mock(() => '***');
|
|
29
|
+
const mockLoadMaskingConfig = mock(() => ({
|
|
30
|
+
enabled: false,
|
|
31
|
+
patterns: [],
|
|
32
|
+
roles: {},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
mock.module('@/lib/data-masking', () => ({
|
|
36
|
+
shouldMask: mockShouldMask,
|
|
37
|
+
canToggleMasking: mockCanToggleMasking,
|
|
38
|
+
canReveal: mockCanReveal,
|
|
39
|
+
detectSensitiveColumnsFromConfig: mockDetectSensitiveColumnsFromConfig,
|
|
40
|
+
maskValueByPattern: mockMaskValueByPattern,
|
|
41
|
+
loadMaskingConfig: mockLoadMaskingConfig,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// ── Mock sub-components to simplify testing ─────────────────────────────────
|
|
45
|
+
mock.module('@/components/results-grid/ResultCard', () => ({
|
|
46
|
+
ResultCard: (props: Record<string, unknown>) =>
|
|
47
|
+
React.createElement('div', { 'data-testid': 'result-card', 'data-index': props.index }),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
mock.module('@/components/results-grid/RowDetailSheet', () => ({
|
|
51
|
+
RowDetailSheet: (props: Record<string, unknown>) =>
|
|
52
|
+
props.isOpen
|
|
53
|
+
? React.createElement('div', { 'data-testid': 'row-detail-sheet' }, 'Row Detail')
|
|
54
|
+
: null,
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
mock.module('@/components/results-grid/StatsBar', () => ({
|
|
58
|
+
StatsBar: (props: Record<string, unknown>) =>
|
|
59
|
+
React.createElement('div', { 'data-testid': 'stats-bar' },
|
|
60
|
+
React.createElement('span', { 'data-testid': 'row-count' }, `${(props.result as { rows: unknown[] })?.rows?.length ?? 0} rows`),
|
|
61
|
+
React.createElement('span', { 'data-testid': 'filtered-count' }, `${props.filteredRowCount} filtered`),
|
|
62
|
+
React.createElement('span', { 'data-testid': 'exec-time' }, `EXEC TIME: ${(props.result as { executionTime?: number })?.executionTime ?? 0}ms`),
|
|
63
|
+
props.onToggleMasking
|
|
64
|
+
? React.createElement('button', { 'data-testid': 'masking-toggle', onClick: props.onToggleMasking as () => void }, 'MASK')
|
|
65
|
+
: null,
|
|
66
|
+
props.editingEnabled && props.pendingChanges && (props.pendingChanges as unknown[]).length > 0
|
|
67
|
+
? React.createElement('span', { 'data-testid': 'pending-changes' }, `${(props.pendingChanges as unknown[]).length} changes`)
|
|
68
|
+
: null,
|
|
69
|
+
(props.activeFilterCount as number) > 0
|
|
70
|
+
? React.createElement('button', { 'data-testid': 'clear-filters', onClick: props.onClearFilters as () => void }, 'Clear Filters')
|
|
71
|
+
: null,
|
|
72
|
+
),
|
|
73
|
+
LoadMoreFooter: (props: Record<string, unknown>) =>
|
|
74
|
+
props.hasMore
|
|
75
|
+
? React.createElement('div', { 'data-testid': 'load-more-footer' },
|
|
76
|
+
React.createElement('button', { onClick: props.onLoadMore as () => void, 'data-testid': 'load-more-btn' }, 'Load More (500 rows)')
|
|
77
|
+
)
|
|
78
|
+
: null,
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// ── Mock @tanstack/react-virtual ────────────────────────────────────────────
|
|
82
|
+
mock.module('@tanstack/react-virtual', () => ({
|
|
83
|
+
useVirtualizer: (opts: { count: number }) => ({
|
|
84
|
+
getVirtualItems: () =>
|
|
85
|
+
Array.from({ length: opts.count }, (_, i) => ({
|
|
86
|
+
index: i,
|
|
87
|
+
start: i * 36,
|
|
88
|
+
size: 36,
|
|
89
|
+
key: i,
|
|
90
|
+
})),
|
|
91
|
+
getTotalSize: () => opts.count * 36,
|
|
92
|
+
}),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
// ── Mock lucide-react icons ─────────────────────────────────────────────────
|
|
96
|
+
mock.module('lucide-react', () => {
|
|
97
|
+
return new Proxy({}, {
|
|
98
|
+
get: (_target, prop) => {
|
|
99
|
+
if (prop === '__esModule') return true;
|
|
100
|
+
return (props: Record<string, unknown>) =>
|
|
101
|
+
React.createElement('span', { 'data-icon': prop, className: props.className as string });
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── Imports AFTER mocks ─────────────────────────────────────────────────────
|
|
107
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
108
|
+
import { render, fireEvent, cleanup, act } from '@testing-library/react';
|
|
109
|
+
import { ResultsGrid, type CellChange } from '@/components/ResultsGrid';
|
|
110
|
+
import type { QueryResult } from '@/lib/types';
|
|
111
|
+
|
|
112
|
+
// ── Test data ───────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
const mockResult: QueryResult = {
|
|
115
|
+
rows: [
|
|
116
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
117
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
|
118
|
+
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
|
|
119
|
+
],
|
|
120
|
+
fields: ['id', 'name', 'email'],
|
|
121
|
+
rowCount: 3,
|
|
122
|
+
executionTime: 12,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const mockEmptyResult: QueryResult = {
|
|
126
|
+
rows: [],
|
|
127
|
+
fields: [],
|
|
128
|
+
rowCount: 0,
|
|
129
|
+
executionTime: 1,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const mockPaginatedResult: QueryResult = {
|
|
133
|
+
rows: Array.from({ length: 50 }, (_, i) => ({
|
|
134
|
+
id: i + 1,
|
|
135
|
+
name: `User ${i + 1}`,
|
|
136
|
+
email: `user${i + 1}@example.com`,
|
|
137
|
+
})),
|
|
138
|
+
fields: ['id', 'name', 'email'],
|
|
139
|
+
rowCount: 50,
|
|
140
|
+
executionTime: 25,
|
|
141
|
+
pagination: {
|
|
142
|
+
limit: 50,
|
|
143
|
+
offset: 0,
|
|
144
|
+
hasMore: true,
|
|
145
|
+
totalReturned: 50,
|
|
146
|
+
wasLimited: true,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// ResultsGrid Tests
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
describe('ResultsGrid', () => {
|
|
155
|
+
afterEach(() => {
|
|
156
|
+
cleanup();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
mockShouldMask.mockClear();
|
|
161
|
+
mockCanToggleMasking.mockClear();
|
|
162
|
+
mockCanReveal.mockClear();
|
|
163
|
+
mockDetectSensitiveColumnsFromConfig.mockClear();
|
|
164
|
+
mockDetectSensitiveColumnsFromConfig.mockReturnValue(new Map());
|
|
165
|
+
mockShouldMask.mockReturnValue(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── 1. Renders "No results" when result has empty rows ────────────────────
|
|
169
|
+
|
|
170
|
+
test('renders empty state when result has empty rows', () => {
|
|
171
|
+
const { queryByText } = render(React.createElement(ResultsGrid, { result: mockEmptyResult }));
|
|
172
|
+
|
|
173
|
+
expect(queryByText('Query returned no data')).not.toBeNull();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── 2. Renders column headers from result.fields ──────────────────────────
|
|
177
|
+
|
|
178
|
+
test('renders column headers from result.fields', () => {
|
|
179
|
+
const { queryAllByText } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
180
|
+
|
|
181
|
+
expect(queryAllByText('id').length).toBeGreaterThan(0);
|
|
182
|
+
expect(queryAllByText('name').length).toBeGreaterThan(0);
|
|
183
|
+
expect(queryAllByText('email').length).toBeGreaterThan(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ── 3. Renders data rows from result.rows ─────────────────────────────────
|
|
187
|
+
|
|
188
|
+
test('renders data rows from result.rows', () => {
|
|
189
|
+
const { queryAllByText } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
190
|
+
|
|
191
|
+
expect(queryAllByText('Alice').length).toBeGreaterThan(0);
|
|
192
|
+
expect(queryAllByText('Bob').length).toBeGreaterThan(0);
|
|
193
|
+
expect(queryAllByText('Charlie').length).toBeGreaterThan(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ── 4. Shows row count via StatsBar ───────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
test('shows row count in stats bar', () => {
|
|
199
|
+
const { queryByTestId } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
200
|
+
|
|
201
|
+
const rowCount = queryByTestId('row-count');
|
|
202
|
+
expect(rowCount).not.toBeNull();
|
|
203
|
+
expect(rowCount!.textContent).toContain('3 rows');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── 5. Shows execution time via StatsBar ──────────────────────────────────
|
|
207
|
+
|
|
208
|
+
test('shows execution time in stats bar', () => {
|
|
209
|
+
const { queryByTestId } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
210
|
+
|
|
211
|
+
const execTime = queryByTestId('exec-time');
|
|
212
|
+
expect(execTime).not.toBeNull();
|
|
213
|
+
expect(execTime!.textContent).toContain('12ms');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ── 6. Load More button shows when pagination hasMore ─────────────────────
|
|
217
|
+
|
|
218
|
+
test('Load More button shows when pagination hasMore', () => {
|
|
219
|
+
const onLoadMore = mock(() => {});
|
|
220
|
+
const { queryByTestId } = render(React.createElement(ResultsGrid, {
|
|
221
|
+
result: mockPaginatedResult,
|
|
222
|
+
onLoadMore,
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
const loadMoreBtn = queryByTestId('load-more-btn');
|
|
226
|
+
expect(loadMoreBtn).not.toBeNull();
|
|
227
|
+
expect(loadMoreBtn!.textContent).toContain('Load More');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ── 7. Load More button fires onLoadMore ──────────────────────────────────
|
|
231
|
+
|
|
232
|
+
test('Load More button fires onLoadMore callback', () => {
|
|
233
|
+
const onLoadMore = mock(() => {});
|
|
234
|
+
const { getByTestId } = render(React.createElement(ResultsGrid, {
|
|
235
|
+
result: mockPaginatedResult,
|
|
236
|
+
onLoadMore,
|
|
237
|
+
}));
|
|
238
|
+
|
|
239
|
+
const loadMoreBtn = getByTestId('load-more-btn');
|
|
240
|
+
fireEvent.click(loadMoreBtn);
|
|
241
|
+
expect(onLoadMore).toHaveBeenCalledTimes(1);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ── 8. Masking toggle button renders when onToggleMasking provided ────────
|
|
245
|
+
|
|
246
|
+
test('masking toggle button renders when onToggleMasking provided', () => {
|
|
247
|
+
const onToggleMasking = mock(() => {});
|
|
248
|
+
const { queryByTestId } = render(React.createElement(ResultsGrid, {
|
|
249
|
+
result: mockResult,
|
|
250
|
+
onToggleMasking,
|
|
251
|
+
}));
|
|
252
|
+
|
|
253
|
+
const maskToggle = queryByTestId('masking-toggle');
|
|
254
|
+
expect(maskToggle).not.toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ── 9. Masking toggle button not rendered without onToggleMasking ─────────
|
|
258
|
+
|
|
259
|
+
test('masking toggle button not rendered without onToggleMasking', () => {
|
|
260
|
+
const { queryByTestId } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
261
|
+
|
|
262
|
+
const maskToggle = queryByTestId('masking-toggle');
|
|
263
|
+
expect(maskToggle).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ── 10. Pending changes indicator shows when editing enabled ──────────────
|
|
267
|
+
|
|
268
|
+
test('pending changes indicator shows when editingEnabled with changes', () => {
|
|
269
|
+
const pendingChanges: CellChange[] = [
|
|
270
|
+
{ rowIndex: 0, columnId: 'name', originalValue: 'Alice', newValue: 'Alicia' },
|
|
271
|
+
];
|
|
272
|
+
const { queryByTestId } = render(React.createElement(ResultsGrid, {
|
|
273
|
+
result: mockResult,
|
|
274
|
+
editingEnabled: true,
|
|
275
|
+
pendingChanges,
|
|
276
|
+
onCellChange: mock(() => {}),
|
|
277
|
+
onApplyChanges: mock(() => {}),
|
|
278
|
+
onDiscardChanges: mock(() => {}),
|
|
279
|
+
}));
|
|
280
|
+
|
|
281
|
+
const changesIndicator = queryByTestId('pending-changes');
|
|
282
|
+
expect(changesIndicator).not.toBeNull();
|
|
283
|
+
expect(changesIndicator!.textContent).toContain('1 changes');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ── 11. No Load More when no pagination ───────────────────────────────────
|
|
287
|
+
|
|
288
|
+
test('no Load More footer when pagination not present', () => {
|
|
289
|
+
const { queryByTestId } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
290
|
+
|
|
291
|
+
const loadMore = queryByTestId('load-more-footer');
|
|
292
|
+
expect(loadMore).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── 12. Empty state message is descriptive ────────────────────────────────
|
|
296
|
+
|
|
297
|
+
test('empty state contains helpful message', () => {
|
|
298
|
+
const { queryByText } = render(React.createElement(ResultsGrid, { result: mockEmptyResult }));
|
|
299
|
+
|
|
300
|
+
expect(queryByText('The operation was successful, but the result set is currently empty.')).not.toBeNull();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ── 13. Column headers are interactive (sort on click) ────────────────────
|
|
304
|
+
|
|
305
|
+
test('column headers render as interactive elements', () => {
|
|
306
|
+
const { queryAllByText } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
307
|
+
// Headers render with field names
|
|
308
|
+
const idHeaders = queryAllByText('id');
|
|
309
|
+
expect(idHeaders.length).toBeGreaterThan(0);
|
|
310
|
+
// Click doesn't crash
|
|
311
|
+
fireEvent.click(idHeaders[0]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// ── 14. Click sort toggles data order ──────────────────────────────────
|
|
315
|
+
|
|
316
|
+
test('clicking column header twice for sort toggle does not crash', () => {
|
|
317
|
+
const { queryAllByText, container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
318
|
+
const idHeaders = queryAllByText('id');
|
|
319
|
+
if (idHeaders[0]) {
|
|
320
|
+
fireEvent.click(idHeaders[0]);
|
|
321
|
+
fireEvent.click(idHeaders[0]);
|
|
322
|
+
}
|
|
323
|
+
expect(container.textContent).toContain('Alice');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ── 15. Filter inputs render ──────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
test('filter input renders for column filtering', () => {
|
|
329
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
330
|
+
// Filter inputs have type="text" and specific placeholder
|
|
331
|
+
const inputs = container.querySelectorAll('input');
|
|
332
|
+
// Should have at least some filter inputs
|
|
333
|
+
expect(inputs.length).toBeGreaterThanOrEqual(0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ── 16. Masking toggle fires callback ───────────────────────────────────
|
|
337
|
+
|
|
338
|
+
test('masking toggle fires onToggleMasking callback', () => {
|
|
339
|
+
const onToggleMasking = mock(() => {});
|
|
340
|
+
const { getByTestId } = render(React.createElement(ResultsGrid, {
|
|
341
|
+
result: mockResult,
|
|
342
|
+
onToggleMasking,
|
|
343
|
+
}));
|
|
344
|
+
const maskToggle = getByTestId('masking-toggle');
|
|
345
|
+
fireEvent.click(maskToggle);
|
|
346
|
+
expect(onToggleMasking).toHaveBeenCalledTimes(1);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// ── 17. Large dataset renders with virtualizer ──────────────────────────
|
|
350
|
+
|
|
351
|
+
test('large dataset renders rows via virtualizer', () => {
|
|
352
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockPaginatedResult }));
|
|
353
|
+
// Data rows should be rendered
|
|
354
|
+
expect(container.textContent).toContain('User 1');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ── 18. No pending changes indicator when no changes ────────────────────
|
|
358
|
+
|
|
359
|
+
test('no pending changes indicator when pendingChanges is empty', () => {
|
|
360
|
+
const { queryByTestId } = render(React.createElement(ResultsGrid, {
|
|
361
|
+
result: mockResult,
|
|
362
|
+
editingEnabled: true,
|
|
363
|
+
pendingChanges: [],
|
|
364
|
+
onCellChange: mock(() => {}),
|
|
365
|
+
onApplyChanges: mock(() => {}),
|
|
366
|
+
onDiscardChanges: mock(() => {}),
|
|
367
|
+
}));
|
|
368
|
+
expect(queryByTestId('pending-changes')).toBeNull();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ── 19. Result with single row ──────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
test('renders single row result correctly', () => {
|
|
374
|
+
const singleRow: QueryResult = {
|
|
375
|
+
rows: [{ id: 1, status: 'OK' }],
|
|
376
|
+
fields: ['id', 'status'],
|
|
377
|
+
rowCount: 1,
|
|
378
|
+
executionTime: 2,
|
|
379
|
+
};
|
|
380
|
+
const { queryAllByText, queryByTestId } = render(React.createElement(ResultsGrid, { result: singleRow }));
|
|
381
|
+
expect(queryAllByText('OK').length).toBeGreaterThan(0);
|
|
382
|
+
expect(queryByTestId('row-count')?.textContent).toContain('1 rows');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ── 20. NULL values display ─────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
test('null values are displayed', () => {
|
|
388
|
+
const withNulls: QueryResult = {
|
|
389
|
+
rows: [{ id: 1, name: null }],
|
|
390
|
+
fields: ['id', 'name'],
|
|
391
|
+
rowCount: 1,
|
|
392
|
+
executionTime: 1,
|
|
393
|
+
};
|
|
394
|
+
const { container } = render(React.createElement(ResultsGrid, { result: withNulls }));
|
|
395
|
+
// NULL should be displayed in some form
|
|
396
|
+
expect(container.textContent).toContain('NULL');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// ── 21. Boolean values display ──────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
test('boolean values are displayed', () => {
|
|
402
|
+
const withBool: QueryResult = {
|
|
403
|
+
rows: [{ id: 1, active: true }],
|
|
404
|
+
fields: ['id', 'active'],
|
|
405
|
+
rowCount: 1,
|
|
406
|
+
executionTime: 1,
|
|
407
|
+
};
|
|
408
|
+
const { container } = render(React.createElement(ResultsGrid, { result: withBool }));
|
|
409
|
+
expect(container.textContent).toContain('true');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// ── 22. Row number column shown ─────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
test('row number column shown', () => {
|
|
415
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
416
|
+
// Row numbers (1, 2, 3) should appear in the rendered output
|
|
417
|
+
expect(container.textContent).toContain('1');
|
|
418
|
+
expect(container.textContent).toContain('2');
|
|
419
|
+
expect(container.textContent).toContain('3');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// ── 23. Masking enabled shows lock icons ────────────────────────────────
|
|
423
|
+
|
|
424
|
+
test('masked cells display masked values when masking enabled', () => {
|
|
425
|
+
mockShouldMask.mockReturnValue(true);
|
|
426
|
+
mockDetectSensitiveColumnsFromConfig.mockReturnValue(new Map([
|
|
427
|
+
['email', { maskType: 'email', pattern: { name: 'email', maskType: 'email' as const, columnPatterns: ['email'], enabled: true, id: 'e1' } }],
|
|
428
|
+
]));
|
|
429
|
+
const { container } = render(React.createElement(ResultsGrid, {
|
|
430
|
+
result: mockResult,
|
|
431
|
+
maskingEnabled: true,
|
|
432
|
+
maskingConfig: { enabled: true, patterns: [], roleSettings: { admin: { canToggle: true, canReveal: true }, user: { canToggle: false, canReveal: false } } },
|
|
433
|
+
}));
|
|
434
|
+
// When masking is enabled and shouldMask returns true, values should be masked
|
|
435
|
+
// The mock maskValueByPattern returns '***'
|
|
436
|
+
expect(container.textContent).toContain('***');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
440
|
+
// Column Filtering Tests
|
|
441
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
442
|
+
|
|
443
|
+
describe('Column filtering', () => {
|
|
444
|
+
test('clicking filter button opens filter dropdown with input', () => {
|
|
445
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
446
|
+
|
|
447
|
+
const filterButtons = container.querySelectorAll('button[title="Filter column"]');
|
|
448
|
+
expect(filterButtons.length).toBeGreaterThan(0);
|
|
449
|
+
|
|
450
|
+
fireEvent.click(filterButtons[0]);
|
|
451
|
+
|
|
452
|
+
const filterInput = container.querySelector('input[placeholder="Filter id..."]');
|
|
453
|
+
expect(filterInput).not.toBeNull();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('typing in filter input filters rows', () => {
|
|
457
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
458
|
+
|
|
459
|
+
const filterButtons = container.querySelectorAll('button[title="Filter column"]');
|
|
460
|
+
fireEvent.click(filterButtons[1]);
|
|
461
|
+
|
|
462
|
+
const filterInput = container.querySelector('input[placeholder="Filter name..."]');
|
|
463
|
+
expect(filterInput).not.toBeNull();
|
|
464
|
+
|
|
465
|
+
fireEvent.change(filterInput!, { target: { value: 'Alice' } });
|
|
466
|
+
|
|
467
|
+
const filteredCount = container.querySelector('[data-testid="filtered-count"]');
|
|
468
|
+
expect(filteredCount?.textContent).toContain('1 filtered');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test('clearing filter value in input removes filter', () => {
|
|
472
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
473
|
+
|
|
474
|
+
const filterButtons = container.querySelectorAll('button[title="Filter column"]');
|
|
475
|
+
fireEvent.click(filterButtons[1]);
|
|
476
|
+
|
|
477
|
+
const filterInput = container.querySelector('input[placeholder="Filter name..."]')!;
|
|
478
|
+
fireEvent.change(filterInput, { target: { value: 'Alice' } });
|
|
479
|
+
expect(container.querySelector('[data-testid="filtered-count"]')?.textContent).toContain('1 filtered');
|
|
480
|
+
|
|
481
|
+
// Re-query input after state change (TanStack Table recreates columns)
|
|
482
|
+
const filterInput2 = container.querySelector('input[placeholder="Filter name..."]')!;
|
|
483
|
+
fireEvent.change(filterInput2, { target: { value: '' } });
|
|
484
|
+
expect(container.querySelector('[data-testid="filtered-count"]')?.textContent).toContain('3 filtered');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('Clear filter button removes single column filter', () => {
|
|
488
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
489
|
+
|
|
490
|
+
const filterButtons = container.querySelectorAll('button[title="Filter column"]');
|
|
491
|
+
fireEvent.click(filterButtons[1]);
|
|
492
|
+
|
|
493
|
+
const filterInput = container.querySelector('input[placeholder="Filter name..."]')!;
|
|
494
|
+
fireEvent.change(filterInput, { target: { value: 'Alice' } });
|
|
495
|
+
|
|
496
|
+
// "Clear filter" button should appear inside dropdown
|
|
497
|
+
const clearBtn = Array.from(container.querySelectorAll('button')).find(
|
|
498
|
+
btn => btn.textContent === 'Clear filter'
|
|
499
|
+
);
|
|
500
|
+
expect(clearBtn).not.toBeUndefined();
|
|
501
|
+
fireEvent.click(clearBtn!);
|
|
502
|
+
|
|
503
|
+
expect(container.querySelector('[data-testid="filtered-count"]')?.textContent).toContain('3 filtered');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test('Escape key closes filter dropdown', () => {
|
|
507
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
508
|
+
|
|
509
|
+
const filterButtons = container.querySelectorAll('button[title="Filter column"]');
|
|
510
|
+
fireEvent.click(filterButtons[0]);
|
|
511
|
+
|
|
512
|
+
const filterInput = container.querySelector('input[placeholder="Filter id..."]');
|
|
513
|
+
expect(filterInput).not.toBeNull();
|
|
514
|
+
|
|
515
|
+
fireEvent.keyDown(filterInput!, { key: 'Escape' });
|
|
516
|
+
|
|
517
|
+
expect(container.querySelector('input[placeholder="Filter id..."]')).toBeNull();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test('Enter key closes filter dropdown', () => {
|
|
521
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
522
|
+
|
|
523
|
+
const filterButtons = container.querySelectorAll('button[title="Filter column"]');
|
|
524
|
+
fireEvent.click(filterButtons[0]);
|
|
525
|
+
|
|
526
|
+
const filterInput = container.querySelector('input[placeholder="Filter id..."]');
|
|
527
|
+
expect(filterInput).not.toBeNull();
|
|
528
|
+
|
|
529
|
+
fireEvent.keyDown(filterInput!, { key: 'Enter' });
|
|
530
|
+
|
|
531
|
+
expect(container.querySelector('input[placeholder="Filter id..."]')).toBeNull();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test('clicking same filter button again closes dropdown', () => {
|
|
535
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
536
|
+
|
|
537
|
+
const filterButtons = container.querySelectorAll('button[title="Filter column"]');
|
|
538
|
+
fireEvent.click(filterButtons[0]);
|
|
539
|
+
expect(container.querySelector('input[placeholder="Filter id..."]')).not.toBeNull();
|
|
540
|
+
|
|
541
|
+
// Re-query button after re-render
|
|
542
|
+
const filterButtons2 = container.querySelectorAll('button[title="Filter column"]');
|
|
543
|
+
fireEvent.click(filterButtons2[0]);
|
|
544
|
+
expect(container.querySelector('input[placeholder="Filter id..."]')).toBeNull();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('clear all filters via StatsBar handleClearFilters', () => {
|
|
548
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
549
|
+
|
|
550
|
+
// Set a filter
|
|
551
|
+
const filterButtons = container.querySelectorAll('button[title="Filter column"]');
|
|
552
|
+
fireEvent.click(filterButtons[1]);
|
|
553
|
+
const filterInput = container.querySelector('input[placeholder="Filter name..."]')!;
|
|
554
|
+
fireEvent.change(filterInput, { target: { value: 'Alice' } });
|
|
555
|
+
|
|
556
|
+
// Close dropdown (re-query input after state change)
|
|
557
|
+
const filterInput2 = container.querySelector('input[placeholder="Filter name..."]')!;
|
|
558
|
+
fireEvent.keyDown(filterInput2, { key: 'Escape' });
|
|
559
|
+
|
|
560
|
+
// Clear all filters button should be visible (activeFilterCount > 0)
|
|
561
|
+
const clearAllBtn = container.querySelector('[data-testid="clear-filters"]');
|
|
562
|
+
expect(clearAllBtn).not.toBeNull();
|
|
563
|
+
fireEvent.click(clearAllBtn!);
|
|
564
|
+
|
|
565
|
+
// All rows restored
|
|
566
|
+
expect(container.querySelector('[data-testid="filtered-count"]')?.textContent).toContain('3 filtered');
|
|
567
|
+
expect(container.querySelector('[data-testid="clear-filters"]')).toBeNull();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test('filter with no matching rows shows 0 filtered', () => {
|
|
571
|
+
const { container } = render(React.createElement(ResultsGrid, { result: mockResult }));
|
|
572
|
+
|
|
573
|
+
const filterButtons = container.querySelectorAll('button[title="Filter column"]');
|
|
574
|
+
fireEvent.click(filterButtons[1]);
|
|
575
|
+
const filterInput = container.querySelector('input[placeholder="Filter name..."]')!;
|
|
576
|
+
fireEvent.change(filterInput, { target: { value: 'Nonexistent' } });
|
|
577
|
+
|
|
578
|
+
expect(container.querySelector('[data-testid="filtered-count"]')?.textContent).toContain('0 filtered');
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
583
|
+
// Inline Editing Tests
|
|
584
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
585
|
+
|
|
586
|
+
describe('Inline editing', () => {
|
|
587
|
+
function findEditInput(container: HTMLElement) {
|
|
588
|
+
return Array.from(container.querySelectorAll('input')).find(
|
|
589
|
+
input => input.className.includes('border-blue-500')
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
test('double-clicking cell enters edit mode with input', () => {
|
|
594
|
+
const onCellChange = mock(() => {});
|
|
595
|
+
const { container } = render(React.createElement(ResultsGrid, {
|
|
596
|
+
result: mockResult,
|
|
597
|
+
editingEnabled: true,
|
|
598
|
+
onCellChange,
|
|
599
|
+
pendingChanges: [],
|
|
600
|
+
}));
|
|
601
|
+
|
|
602
|
+
const cells = container.querySelectorAll('.cursor-text');
|
|
603
|
+
expect(cells.length).toBeGreaterThan(0);
|
|
604
|
+
|
|
605
|
+
fireEvent.doubleClick(cells[0]);
|
|
606
|
+
|
|
607
|
+
expect(findEditInput(container)).not.toBeUndefined();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('Enter key commits edit and calls onCellChange', () => {
|
|
611
|
+
const onCellChange = mock(() => {});
|
|
612
|
+
const { container } = render(React.createElement(ResultsGrid, {
|
|
613
|
+
result: mockResult,
|
|
614
|
+
editingEnabled: true,
|
|
615
|
+
onCellChange,
|
|
616
|
+
pendingChanges: [],
|
|
617
|
+
}));
|
|
618
|
+
|
|
619
|
+
const cells = container.querySelectorAll('.cursor-text');
|
|
620
|
+
const nameCell = Array.from(cells).find(c => c.textContent === 'Alice');
|
|
621
|
+
expect(nameCell).not.toBeUndefined();
|
|
622
|
+
fireEvent.doubleClick(nameCell!);
|
|
623
|
+
|
|
624
|
+
const editInput = findEditInput(container)!;
|
|
625
|
+
fireEvent.change(editInput, { target: { value: 'Alicia' } });
|
|
626
|
+
|
|
627
|
+
// Re-query after state change (columns memo recomputes on editValue change)
|
|
628
|
+
const updatedEditInput = findEditInput(container)!;
|
|
629
|
+
fireEvent.keyDown(updatedEditInput, { key: 'Enter' });
|
|
630
|
+
|
|
631
|
+
expect(onCellChange).toHaveBeenCalledTimes(1);
|
|
632
|
+
const callArg = (onCellChange.mock.calls as unknown[][])[0][0] as Record<string, unknown>;
|
|
633
|
+
expect(callArg.newValue).toBe('Alicia');
|
|
634
|
+
expect(callArg.originalValue).toBe('Alice');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test('Escape key cancels edit without calling onCellChange', () => {
|
|
638
|
+
const onCellChange = mock(() => {});
|
|
639
|
+
const { container } = render(React.createElement(ResultsGrid, {
|
|
640
|
+
result: mockResult,
|
|
641
|
+
editingEnabled: true,
|
|
642
|
+
onCellChange,
|
|
643
|
+
pendingChanges: [],
|
|
644
|
+
}));
|
|
645
|
+
|
|
646
|
+
const cells = container.querySelectorAll('.cursor-text');
|
|
647
|
+
fireEvent.doubleClick(cells[0]);
|
|
648
|
+
|
|
649
|
+
const editInput = findEditInput(container)!;
|
|
650
|
+
// Press Escape directly (no value change to avoid stale ref)
|
|
651
|
+
fireEvent.keyDown(editInput, { key: 'Escape' });
|
|
652
|
+
|
|
653
|
+
expect(onCellChange).not.toHaveBeenCalled();
|
|
654
|
+
expect(findEditInput(container)).toBeUndefined();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test('blur commits edit when value changed', () => {
|
|
658
|
+
const onCellChange = mock(() => {});
|
|
659
|
+
const { container } = render(React.createElement(ResultsGrid, {
|
|
660
|
+
result: mockResult,
|
|
661
|
+
editingEnabled: true,
|
|
662
|
+
onCellChange,
|
|
663
|
+
pendingChanges: [],
|
|
664
|
+
}));
|
|
665
|
+
|
|
666
|
+
const cells = container.querySelectorAll('.cursor-text');
|
|
667
|
+
const nameCell = Array.from(cells).find(c => c.textContent === 'Alice');
|
|
668
|
+
fireEvent.doubleClick(nameCell!);
|
|
669
|
+
|
|
670
|
+
const editInput = findEditInput(container)!;
|
|
671
|
+
fireEvent.change(editInput, { target: { value: 'Alicia' } });
|
|
672
|
+
|
|
673
|
+
// Re-query after state change
|
|
674
|
+
const updatedEditInput = findEditInput(container)!;
|
|
675
|
+
fireEvent.blur(updatedEditInput);
|
|
676
|
+
|
|
677
|
+
expect(onCellChange).toHaveBeenCalledTimes(1);
|
|
678
|
+
const callArg = (onCellChange.mock.calls as unknown[][])[0][0] as Record<string, unknown>;
|
|
679
|
+
expect(callArg.newValue).toBe('Alicia');
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test('Enter with unchanged value does not call onCellChange', () => {
|
|
683
|
+
const onCellChange = mock(() => {});
|
|
684
|
+
const { container } = render(React.createElement(ResultsGrid, {
|
|
685
|
+
result: mockResult,
|
|
686
|
+
editingEnabled: true,
|
|
687
|
+
onCellChange,
|
|
688
|
+
pendingChanges: [],
|
|
689
|
+
}));
|
|
690
|
+
|
|
691
|
+
const cells = container.querySelectorAll('.cursor-text');
|
|
692
|
+
const nameCell = Array.from(cells).find(c => c.textContent === 'Alice');
|
|
693
|
+
fireEvent.doubleClick(nameCell!);
|
|
694
|
+
|
|
695
|
+
const editInput = findEditInput(container)!;
|
|
696
|
+
// Don't change the value, just press Enter
|
|
697
|
+
fireEvent.keyDown(editInput, { key: 'Enter' });
|
|
698
|
+
|
|
699
|
+
expect(onCellChange).not.toHaveBeenCalled();
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test('blur with unchanged value does not call onCellChange', () => {
|
|
703
|
+
const onCellChange = mock(() => {});
|
|
704
|
+
const { container } = render(React.createElement(ResultsGrid, {
|
|
705
|
+
result: mockResult,
|
|
706
|
+
editingEnabled: true,
|
|
707
|
+
onCellChange,
|
|
708
|
+
pendingChanges: [],
|
|
709
|
+
}));
|
|
710
|
+
|
|
711
|
+
const cells = container.querySelectorAll('.cursor-text');
|
|
712
|
+
const nameCell = Array.from(cells).find(c => c.textContent === 'Alice');
|
|
713
|
+
fireEvent.doubleClick(nameCell!);
|
|
714
|
+
|
|
715
|
+
const editInput = findEditInput(container)!;
|
|
716
|
+
fireEvent.blur(editInput);
|
|
717
|
+
|
|
718
|
+
expect(onCellChange).not.toHaveBeenCalled();
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
723
|
+
// Cell Reveal Tests
|
|
724
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
725
|
+
|
|
726
|
+
describe('Cell reveal', () => {
|
|
727
|
+
function setupMasking() {
|
|
728
|
+
mockShouldMask.mockReturnValue(true);
|
|
729
|
+
mockCanReveal.mockReturnValue(true);
|
|
730
|
+
mockDetectSensitiveColumnsFromConfig.mockReturnValue(new Map([
|
|
731
|
+
['email', { name: 'email', maskType: 'email' as const, columnPatterns: ['email'], enabled: true, id: 'e1' }],
|
|
732
|
+
]));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const maskingProps = {
|
|
736
|
+
result: mockResult,
|
|
737
|
+
maskingEnabled: true,
|
|
738
|
+
maskingConfig: { enabled: true, patterns: [], roleSettings: { admin: { canToggle: true, canReveal: true }, user: { canToggle: false, canReveal: false } } },
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
test('clicking reveal button shows actual value with lock icon', () => {
|
|
742
|
+
setupMasking();
|
|
743
|
+
|
|
744
|
+
const { container } = render(React.createElement(ResultsGrid, maskingProps));
|
|
745
|
+
|
|
746
|
+
// Initially masked with '***'
|
|
747
|
+
expect(container.textContent).toContain('***');
|
|
748
|
+
|
|
749
|
+
// Find reveal button
|
|
750
|
+
const revealButton = container.querySelector('button[title="Reveal value (10s)"]');
|
|
751
|
+
expect(revealButton).not.toBeNull();
|
|
752
|
+
|
|
753
|
+
// Click reveal
|
|
754
|
+
fireEvent.click(revealButton!);
|
|
755
|
+
|
|
756
|
+
// After reveal, the cell should show actual email value (not ***)
|
|
757
|
+
// This confirms the revealed cell branch (lines 328-333) is hit
|
|
758
|
+
expect(container.textContent).toContain('alice@example.com');
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
test('revealed cell auto-hides after timeout', () => {
|
|
762
|
+
setupMasking();
|
|
763
|
+
|
|
764
|
+
const { container } = render(React.createElement(ResultsGrid, maskingProps));
|
|
765
|
+
|
|
766
|
+
// Mock setTimeout AFTER React initialization to avoid breaking React internals
|
|
767
|
+
const origSetTimeout = globalThis.setTimeout;
|
|
768
|
+
let capturedCallback: (() => void) | null = null;
|
|
769
|
+
globalThis.setTimeout = ((fn: (...args: unknown[]) => void, ms?: number) => {
|
|
770
|
+
if (ms === 10000) {
|
|
771
|
+
capturedCallback = fn as () => void;
|
|
772
|
+
return 0 as unknown as ReturnType<typeof setTimeout>;
|
|
773
|
+
}
|
|
774
|
+
return origSetTimeout(fn, ms);
|
|
775
|
+
}) as typeof setTimeout;
|
|
776
|
+
|
|
777
|
+
const revealButton = container.querySelector('button[title="Reveal value (10s)"]')!;
|
|
778
|
+
fireEvent.click(revealButton);
|
|
779
|
+
|
|
780
|
+
// Callback should have been captured
|
|
781
|
+
expect(capturedCallback).not.toBeNull();
|
|
782
|
+
|
|
783
|
+
// Execute the timeout callback to cover auto-hide lines (139-143)
|
|
784
|
+
act(() => { capturedCallback!(); });
|
|
785
|
+
|
|
786
|
+
globalThis.setTimeout = origSetTimeout;
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test('reveal button not shown when canReveal is false', () => {
|
|
790
|
+
mockShouldMask.mockReturnValue(true);
|
|
791
|
+
mockCanReveal.mockReturnValue(false);
|
|
792
|
+
mockDetectSensitiveColumnsFromConfig.mockReturnValue(new Map([
|
|
793
|
+
['email', { name: 'email', maskType: 'email' as const, columnPatterns: ['email'], enabled: true, id: 'e1' }],
|
|
794
|
+
]));
|
|
795
|
+
|
|
796
|
+
const { container } = render(React.createElement(ResultsGrid, maskingProps));
|
|
797
|
+
|
|
798
|
+
expect(container.textContent).toContain('***');
|
|
799
|
+
|
|
800
|
+
const revealButton = container.querySelector('button[title="Reveal value (10s)"]');
|
|
801
|
+
expect(revealButton).toBeNull();
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
});
|