@jmruthers/pace-core 0.6.4 → 0.6.6
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/CHANGELOG.md +104 -0
- package/README.md +5 -403
- package/core-usage-manifest.json +93 -0
- package/cursor-rules/00-pace-core-compliance.mdc +128 -26
- package/cursor-rules/01-standards-compliance.mdc +49 -8
- package/cursor-rules/02-project-structure.mdc +6 -0
- package/cursor-rules/03-solid-principles.mdc +2 -0
- package/cursor-rules/04-testing-standards.mdc +2 -0
- package/cursor-rules/05-bug-reports-and-features.mdc +2 -0
- package/cursor-rules/06-code-quality.mdc +2 -0
- package/cursor-rules/07-tech-stack-compliance.mdc +2 -0
- package/cursor-rules/08-markup-quality.mdc +52 -27
- package/cursor-rules/09-rbac-compliance.mdc +462 -0
- package/cursor-rules/10-error-handling-patterns.mdc +179 -0
- package/cursor-rules/11-performance-optimization.mdc +169 -0
- package/cursor-rules/12-ci-cd-integration.mdc +150 -0
- package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
- package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-2N_tqbfq.d.ts} +1 -1
- package/dist/DataTable-LRJL4IRV.js +15 -0
- package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-BBH6Vqg7.d.ts} +72 -139
- package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
- package/dist/api-Y4MQWOFW.js +4 -0
- package/dist/audit-MYQXYZFU.js +3 -0
- package/dist/{chunk-J36DSWQK.js → chunk-2HGJFNAH.js} +8 -28
- package/dist/{chunk-OEWDTMG7.js → chunk-3O3WHILE.js} +38 -121
- package/dist/{chunk-M43Y4SSO.js → chunk-3QC3KRHK.js} +1 -14
- package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
- package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
- package/dist/chunk-4T7OBVTU.js +62 -0
- package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
- package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
- package/dist/{chunk-NN6WWZ5U.js → chunk-7TYHROIV.js} +579 -563
- package/dist/{chunk-M7MPQISP.js → chunk-A55DK444.js} +9 -16
- package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
- package/dist/{chunk-L4OXEN46.js → chunk-BVP2BCJF.js} +2 -16
- package/dist/chunk-C7NSAPTL.js +1 -0
- package/dist/{chunk-YKRAFF5K.js → chunk-FENMYN2U.js} +73 -149
- package/dist/{chunk-AVMLPIM7.js → chunk-FTCRZOG2.js} +284 -432
- package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
- package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
- package/dist/{chunk-I6DAQMWX.js → chunk-LAZMKTTF.js} +930 -891
- package/dist/{chunk-5EC5MEWX.js → chunk-MAGBIDNS.js} +77 -222
- package/dist/chunk-MBADTM7L.js +64 -0
- package/dist/chunk-OHIK3MIO.js +994 -0
- package/dist/{chunk-6SOIHG6Z.js → chunk-S7DKJPLT.js} +115 -44
- package/dist/{chunk-FMUCXFII.js → chunk-SD6WQY43.js} +1 -5
- package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
- package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
- package/dist/{chunk-FFQEQTNW.js → chunk-UIYSCEV7.js} +134 -45
- package/dist/{chunk-3LPHPB62.js → chunk-ZFYPMX46.js} +271 -87
- package/dist/{chunk-7JPAB3T5.js → chunk-ZS5VO5JB.js} +1989 -1283
- package/dist/components.d.ts +6 -6
- package/dist/components.js +57 -267
- package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
- package/dist/eslint-rules/index.cjs +22 -0
- package/dist/eslint-rules/rules/compliance.cjs +348 -0
- package/dist/eslint-rules/rules/components.cjs +113 -0
- package/dist/eslint-rules/rules/imports.cjs +102 -0
- package/dist/eslint-rules/rules/rbac.cjs +790 -0
- package/dist/eslint-rules/utils/helpers.cjs +42 -0
- package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
- package/dist/hooks.d.ts +5 -5
- package/dist/hooks.js +62 -270
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +36 -26
- package/dist/index.js +87 -690
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +8 -35
- package/dist/rbac/eslint-rules.d.ts +46 -44
- package/dist/rbac/eslint-rules.js +7 -4
- package/dist/rbac/index.d.ts +124 -594
- package/dist/rbac/index.js +14 -207
- package/dist/styles/index.js +2 -12
- package/dist/theming/runtime.js +3 -19
- package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
- package/dist/{types-CkbwOr4Y.d.ts → types-B-K_5VnO.d.ts} +4 -0
- package/dist/types-t9H8qKRw.d.ts +55 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +7 -94
- package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-COZ28Mvq.d.ts} +9 -9
- package/dist/utils.d.ts +24 -117
- package/dist/utils.js +54 -392
- package/docs/README.md +16 -6
- package/docs/api/README.md +4 -402
- package/docs/api/modules.md +454 -930
- package/docs/api-reference/components.md +3 -1
- package/docs/api-reference/deprecated.md +31 -6
- package/docs/api-reference/rpc-functions.md +78 -3
- package/docs/best-practices/accessibility.md +6 -3
- package/docs/getting-started/cursor-rules.md +3 -23
- package/docs/getting-started/dependencies.md +650 -0
- package/docs/getting-started/installation-guide.md +20 -7
- package/docs/getting-started/quick-start.md +23 -12
- package/docs/implementation-guides/permission-enforcement.md +4 -0
- package/docs/rbac/MIGRATION_GUIDE.md +819 -0
- package/docs/rbac/RBAC_CONTRACT.md +724 -0
- package/docs/rbac/README.md +12 -3
- package/docs/rbac/edge-functions-guide.md +376 -0
- package/docs/rbac/secure-client-protection.md +0 -34
- package/docs/standards/00-pace-core-compliance.md +967 -0
- package/docs/standards/01-standards-compliance.md +188 -0
- package/docs/standards/02-project-structure.md +985 -0
- package/docs/standards/03-solid-principles.md +39 -0
- package/docs/standards/04-testing-standards.md +36 -0
- package/docs/standards/05-bug-reports-and-features.md +27 -0
- package/docs/standards/{04-code-style-standard.md → 06-code-quality.md} +2 -0
- package/docs/standards/07-tech-stack-compliance.md +30 -0
- package/docs/standards/08-markup-quality.md +345 -0
- package/docs/standards/{07-rbac-and-rls-standard.md → 09-rbac-compliance.md} +149 -54
- package/docs/standards/10-error-handling-patterns.md +401 -0
- package/docs/standards/11-performance-optimization.md +348 -0
- package/docs/standards/12-ci-cd-integration.md +370 -0
- package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +192 -0
- package/docs/standards/README.md +62 -33
- package/docs/troubleshooting/organisation-context-setup.md +42 -19
- package/eslint-config-pace-core.cjs +20 -4
- package/package.json +31 -21
- package/scripts/audit/audit-compliance.cjs +1295 -0
- package/scripts/audit/audit-components.cjs +260 -0
- package/scripts/audit/audit-dependencies.cjs +395 -0
- package/scripts/audit/audit-rbac.cjs +954 -0
- package/scripts/audit/audit-standards.cjs +1268 -0
- package/scripts/audit/index.cjs +1898 -194
- package/scripts/install-cursor-rules.cjs +259 -8
- package/scripts/validate-master.js +1 -1
- package/src/__tests__/fixtures/supabase.ts +1 -1
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +1 -1
- package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +3 -3
- package/src/__tests__/helpers/component-test-utils.tsx +1 -1
- package/src/__tests__/helpers/supabaseMock.ts +2 -2
- package/src/__tests__/public-recipe-view.test.ts +38 -9
- package/src/components/Button/Button.tsx +5 -1
- package/src/components/ContextSelector/ContextSelector.tsx +42 -39
- package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
- package/src/components/DataTable/components/DataTableBody.tsx +55 -31
- package/src/components/DataTable/components/DataTableCore.tsx +186 -13
- package/src/components/DataTable/components/DataTableLayout.tsx +30 -5
- package/src/components/DataTable/components/EditFields.tsx +23 -3
- package/src/components/DataTable/components/EditableRow.tsx +7 -2
- package/src/components/DataTable/components/ImportModal.tsx +4 -6
- package/src/components/DataTable/components/RowComponent.tsx +12 -0
- package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
- package/src/components/DataTable/core/DataTableContext.tsx +1 -1
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
- package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
- package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
- package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
- package/src/components/DataTable/types.ts +5 -0
- package/src/components/DateTimeField/DateTimeField.tsx +20 -20
- package/src/components/DateTimeField/README.md +5 -2
- package/src/components/Dialog/Dialog.test.tsx +361 -318
- package/src/components/Dialog/Dialog.tsx +1154 -323
- package/src/components/Dialog/index.ts +3 -3
- package/src/components/FileDisplay/FileDisplay.test.tsx +45 -2
- package/src/components/FileDisplay/FileDisplay.tsx +28 -22
- package/src/components/Form/Form.test.tsx +9 -10
- package/src/components/Form/Form.tsx +369 -9
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
- package/src/components/LoginForm/LoginForm.tsx +2 -2
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
- package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +30 -41
- package/src/components/PaceAppLayout/README.md +10 -9
- package/src/components/PaceAppLayout/test-setup.tsx +40 -31
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
- package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
- package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
- package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
- package/src/components/Select/Select.tsx +23 -21
- package/src/components/Select/types.ts +1 -1
- package/src/components/UserMenu/UserMenu.test.tsx +38 -6
- package/src/components/UserMenu/UserMenu.tsx +39 -34
- package/src/components/index.ts +3 -4
- package/src/eslint-rules/index.cjs +22 -0
- package/src/eslint-rules/rules/compliance.cjs +348 -0
- package/src/eslint-rules/rules/components.cjs +113 -0
- package/src/eslint-rules/rules/imports.cjs +102 -0
- package/src/eslint-rules/rules/rbac.cjs +790 -0
- package/src/eslint-rules/utils/helpers.cjs +42 -0
- package/src/eslint-rules/utils/manifest-loader.cjs +75 -0
- package/src/hooks/__tests__/hooks.integration.test.tsx +6 -8
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
- package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
- package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
- package/src/hooks/public/usePublicEvent.ts +62 -190
- package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
- package/src/hooks/public/usePublicEventLogo.ts +19 -9
- package/src/hooks/useAppConfig.ts +26 -24
- package/src/hooks/useEventTheme.test.ts +211 -233
- package/src/hooks/useEventTheme.ts +19 -28
- package/src/hooks/useEvents.ts +11 -7
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useOrganisationPermissions.ts +9 -11
- package/src/hooks/useOrganisations.ts +13 -7
- package/src/hooks/useQueryCache.ts +0 -1
- package/src/hooks/useSessionDraft.ts +380 -0
- package/src/hooks/useSessionRestoration.ts +3 -1
- package/src/icons/index.ts +27 -0
- package/src/index.ts +16 -1
- package/src/providers/OrganisationProvider.tsx +23 -14
- package/src/providers/services/EventServiceProvider.tsx +1 -24
- package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +3 -0
- package/src/rbac/README.md +20 -20
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
- package/src/rbac/adapters.tsx +7 -295
- package/src/rbac/api.test.ts +44 -56
- package/src/rbac/api.ts +10 -17
- package/src/rbac/cache-invalidation.ts +0 -1
- package/src/rbac/compliance/index.ts +10 -0
- package/src/rbac/compliance/pattern-detector.ts +553 -0
- package/src/rbac/compliance/runtime-compliance.ts +22 -0
- package/src/rbac/components/AccessDenied.tsx +150 -0
- package/src/rbac/components/NavigationGuard.tsx +12 -20
- package/src/rbac/components/PagePermissionGuard.tsx +4 -24
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
- package/src/rbac/components/index.ts +3 -41
- package/src/rbac/eslint-rules.js +1 -1
- package/src/rbac/hooks/index.ts +0 -3
- package/src/rbac/hooks/permissions/index.ts +0 -3
- package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
- package/src/rbac/hooks/usePermissions.ts +0 -3
- package/src/rbac/hooks/useRBAC.test.ts +21 -3
- package/src/rbac/hooks/useRBAC.ts +4 -3
- package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
- package/src/rbac/hooks/useResolvedScope.ts +58 -140
- package/src/rbac/hooks/useResourcePermissions.test.ts +241 -60
- package/src/rbac/hooks/useResourcePermissions.ts +182 -63
- package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
- package/src/rbac/hooks/useRoleManagement.ts +147 -19
- package/src/rbac/hooks/useSecureSupabase.ts +4 -8
- package/src/rbac/index.ts +7 -9
- package/src/rbac/permissions.ts +17 -17
- package/src/rbac/utils/contextValidator.ts +45 -7
- package/src/services/AuthService.ts +132 -23
- package/src/services/EventService.ts +4 -97
- package/src/services/InactivityService.ts +155 -58
- package/src/services/OrganisationService.ts +7 -44
- package/src/services/__tests__/OrganisationService.test.ts +26 -8
- package/src/services/base/BaseService.ts +0 -3
- package/src/styles/core.css +4 -0
- package/src/types/database.generated.ts +4733 -3809
- package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
- package/src/utils/context/organisationContext.test.ts +13 -28
- package/src/utils/context/organisationContext.ts +21 -52
- package/src/utils/dynamic/dynamicUtils.ts +1 -1
- package/src/utils/file-reference/index.ts +39 -15
- package/src/utils/formatting/formatDateTime.test.ts +3 -2
- package/src/utils/formatting/formatTime.test.ts +3 -2
- package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
- package/src/utils/index.ts +4 -1
- package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
- package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
- package/src/utils/persistence/keyDerivation.ts +304 -0
- package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
- package/src/utils/security/secureStorage.ts +5 -5
- package/src/utils/storage/helpers.ts +3 -3
- package/src/utils/supabase/createBaseClient.ts +147 -0
- package/src/utils/timezone/timezone.test.ts +1 -2
- package/src/utils/timezone/timezone.ts +1 -1
- package/src/utils/validation/csrf.ts +4 -4
- package/cursor-rules/CHANGELOG.md +0 -119
- package/cursor-rules/README.md +0 -192
- package/dist/DataTable-E7YQZD7D.js +0 -175
- package/dist/DataTable-E7YQZD7D.js.map +0 -1
- package/dist/UnifiedAuthProvider-QPXO24B4.js +0 -18
- package/dist/UnifiedAuthProvider-QPXO24B4.js.map +0 -1
- package/dist/api-6LVZTHDS.js +0 -52
- package/dist/api-6LVZTHDS.js.map +0 -1
- package/dist/audit-V53FV5AG.js +0 -17
- package/dist/audit-V53FV5AG.js.map +0 -1
- package/dist/chunk-36LVWXB2.js +0 -227
- package/dist/chunk-36LVWXB2.js.map +0 -1
- package/dist/chunk-3LPHPB62.js.map +0 -1
- package/dist/chunk-5DRSZLL2.js.map +0 -1
- package/dist/chunk-5EC5MEWX.js.map +0 -1
- package/dist/chunk-63FOKYGO.js.map +0 -1
- package/dist/chunk-6SOIHG6Z.js.map +0 -1
- package/dist/chunk-7JPAB3T5.js.map +0 -1
- package/dist/chunk-ATKZM7RX.js +0 -2053
- package/dist/chunk-ATKZM7RX.js.map +0 -1
- package/dist/chunk-AVMLPIM7.js.map +0 -1
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/chunk-E66EQZE6.js.map +0 -1
- package/dist/chunk-FFQEQTNW.js.map +0 -1
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-G37KK66H.js.map +0 -1
- package/dist/chunk-I6DAQMWX.js.map +0 -1
- package/dist/chunk-J36DSWQK.js.map +0 -1
- package/dist/chunk-KQCRWDSA.js +0 -1
- package/dist/chunk-KQCRWDSA.js.map +0 -1
- package/dist/chunk-L4OXEN46.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-M43Y4SSO.js.map +0 -1
- package/dist/chunk-M7MPQISP.js.map +0 -1
- package/dist/chunk-NN6WWZ5U.js.map +0 -1
- package/dist/chunk-OEWDTMG7.js.map +0 -1
- package/dist/chunk-PWLANIRT.js.map +0 -1
- package/dist/chunk-QXHPKYJV.js.map +0 -1
- package/dist/chunk-VBXEHIUJ.js.map +0 -1
- package/dist/chunk-YKRAFF5K.js.map +0 -1
- package/dist/chunk-ZSAAAMVR.js.map +0 -1
- package/dist/components.js.map +0 -1
- package/dist/contextValidator-OOPCLPZW.js +0 -9
- package/dist/contextValidator-OOPCLPZW.js.map +0 -1
- package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
- package/dist/hooks.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/providers.js.map +0 -1
- package/dist/rbac/eslint-rules.js.map +0 -1
- package/dist/rbac/index.js.map +0 -1
- package/dist/styles/index.js.map +0 -1
- package/dist/theming/runtime.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/docs/standards/01-architecture-standard.md +0 -44
- package/docs/standards/02-api-and-rpc-standard.md +0 -39
- package/docs/standards/03-component-standard.md +0 -32
- package/docs/standards/05-security-standard.md +0 -44
- package/docs/standards/06-testing-and-docs-standard.md +0 -29
- package/docs/standards/pace-core-compliance.md +0 -432
- package/scripts/audit/core/checks/accessibility.cjs +0 -197
- package/scripts/audit/core/checks/api-usage.cjs +0 -191
- package/scripts/audit/core/checks/bundle.cjs +0 -142
- package/scripts/audit/core/checks/compliance.cjs +0 -2706
- package/scripts/audit/core/checks/config.cjs +0 -54
- package/scripts/audit/core/checks/coverage.cjs +0 -84
- package/scripts/audit/core/checks/dependencies.cjs +0 -994
- package/scripts/audit/core/checks/documentation.cjs +0 -268
- package/scripts/audit/core/checks/environment.cjs +0 -116
- package/scripts/audit/core/checks/error-handling.cjs +0 -340
- package/scripts/audit/core/checks/forms.cjs +0 -172
- package/scripts/audit/core/checks/heuristics.cjs +0 -68
- package/scripts/audit/core/checks/hooks.cjs +0 -334
- package/scripts/audit/core/checks/imports.cjs +0 -244
- package/scripts/audit/core/checks/performance.cjs +0 -325
- package/scripts/audit/core/checks/routes.cjs +0 -117
- package/scripts/audit/core/checks/state.cjs +0 -130
- package/scripts/audit/core/checks/structure.cjs +0 -65
- package/scripts/audit/core/checks/style.cjs +0 -584
- package/scripts/audit/core/checks/testing.cjs +0 -122
- package/scripts/audit/core/checks/typescript.cjs +0 -61
- package/scripts/audit/core/scanner.cjs +0 -199
- package/scripts/audit/core/utils.cjs +0 -137
- package/scripts/audit/reporters/console.cjs +0 -151
- package/scripts/audit/reporters/json.cjs +0 -54
- package/scripts/audit/reporters/markdown.cjs +0 -124
- package/scripts/audit-consuming-app.cjs +0 -86
- package/src/eslint-rules/pace-core-compliance.cjs +0 -510
- package/src/eslint-rules/pace-core-compliance.js +0 -638
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
- package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
- package/src/rbac/components/NavigationProvider.test.tsx +0 -481
- package/src/rbac/components/NavigationProvider.tsx +0 -345
- package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
- package/src/rbac/components/PagePermissionProvider.tsx +0 -279
- package/src/rbac/components/PermissionEnforcer.tsx +0 -312
- package/src/rbac/components/RoleBasedRouter.tsx +0 -440
- package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
- package/src/rbac/components/SecureDataProvider.tsx +0 -339
- package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
- package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
- package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +0 -90
|
@@ -14,24 +14,71 @@ import userEvent from '@testing-library/user-event';
|
|
|
14
14
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
15
15
|
import { ImportModal } from '../ImportModal';
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
17
|
+
// Helper function to wait for dialog to be accessible
|
|
18
|
+
// Native dialog elements are only accessible after showModal() completes
|
|
19
|
+
// In test environments, we use querySelector as fallback since getByRole may not work
|
|
20
|
+
// Note: In test environments (jsdom), dialog.open may not be set even when dialog is rendered
|
|
21
|
+
// Also note: Dialog uses requestAnimationFrame before showModal(), so we need to wait for content
|
|
22
|
+
const waitForDialog = async (): Promise<HTMLElement> => {
|
|
23
|
+
return await waitFor(
|
|
24
|
+
() => {
|
|
25
|
+
// Try getByRole first (works in browsers with full dialog support)
|
|
26
|
+
try {
|
|
27
|
+
const dialog = screen.getByRole('dialog');
|
|
28
|
+
expect(dialog).toBeInTheDocument();
|
|
29
|
+
return dialog;
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// Fallback: use querySelector for test environments that don't fully support dialog accessibility
|
|
32
|
+
const dialog = document.querySelector('dialog[role="dialog"]') as HTMLDialogElement;
|
|
33
|
+
if (!dialog) {
|
|
34
|
+
throw new Error('Dialog not found in DOM');
|
|
35
|
+
}
|
|
36
|
+
// In test environments, dialog.open may not be set even when dialog is rendered
|
|
37
|
+
// Just check that dialog exists in DOM - that's sufficient for testing
|
|
38
|
+
return dialog;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{ timeout: 5000 }
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Helper function to find buttons in dialogs (more reliable than getByRole in test environments)
|
|
46
|
+
const findButtonByText = (text: string | RegExp): HTMLButtonElement | null => {
|
|
47
|
+
// Try getByRole first
|
|
48
|
+
try {
|
|
49
|
+
const button = screen.getByRole('button', { name: text });
|
|
50
|
+
return button as HTMLButtonElement;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Fallback: search all buttons by text content
|
|
53
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
54
|
+
const regex = typeof text === 'string' ? new RegExp(text, 'i') : text;
|
|
55
|
+
return buttons.find(btn => regex.test(btn.textContent || '')) as HTMLButtonElement || null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Helper function to upload a file and wait for preview to appear
|
|
60
|
+
const uploadFileAndWaitForPreview = async (user: ReturnType<typeof userEvent.setup>, file: File) => {
|
|
61
|
+
// Wait for dialog and file input
|
|
62
|
+
await waitForDialog();
|
|
63
|
+
await waitFor(() => {
|
|
64
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
65
|
+
expect(fileInput).toBeInTheDocument();
|
|
66
|
+
}, { timeout: 5000 });
|
|
67
|
+
|
|
68
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
69
|
+
|
|
70
|
+
// Upload the file
|
|
71
|
+
await user.upload(fileInput, file);
|
|
72
|
+
|
|
73
|
+
// Wait for preview table to appear (file processing is async)
|
|
74
|
+
// Try multiple ways to find the table since queryByRole may not work in all test environments
|
|
75
|
+
await waitFor(() => {
|
|
76
|
+
const table = screen.queryByRole('table') ||
|
|
77
|
+
document.querySelector('table') ||
|
|
78
|
+
document.querySelector('table.min-w-full');
|
|
79
|
+
expect(table).toBeInTheDocument();
|
|
80
|
+
}, { timeout: 10000 });
|
|
81
|
+
};
|
|
35
82
|
|
|
36
83
|
// Mock Button component
|
|
37
84
|
vi.mock('../../Button/Button', () => ({
|
|
@@ -97,20 +144,41 @@ describe('[component] ImportModal', () => {
|
|
|
97
144
|
|
|
98
145
|
const createCSVFile = (content: string, filename = 'test.csv'): File => {
|
|
99
146
|
const blob = new Blob([content], { type: 'text/csv' });
|
|
100
|
-
|
|
147
|
+
const file = new File([blob], filename, { type: 'text/csv' });
|
|
148
|
+
// Store content for File.text() mock to access
|
|
149
|
+
(file as any)._content = content;
|
|
150
|
+
return file;
|
|
101
151
|
};
|
|
102
152
|
|
|
103
153
|
beforeEach(() => {
|
|
104
154
|
vi.clearAllMocks();
|
|
155
|
+
|
|
156
|
+
// Mock showModal for dialog elements (needed for test environments)
|
|
157
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function(this: HTMLDialogElement) {
|
|
158
|
+
this.setAttribute('open', '');
|
|
159
|
+
this.dispatchEvent(new Event('show', { bubbles: true }));
|
|
160
|
+
});
|
|
161
|
+
HTMLDialogElement.prototype.close = vi.fn(function(this: HTMLDialogElement) {
|
|
162
|
+
this.removeAttribute('open');
|
|
163
|
+
this.dispatchEvent(new Event('close', { bubbles: true }));
|
|
164
|
+
});
|
|
165
|
+
|
|
105
166
|
// Mock File.text() method for jsdom compatibility
|
|
106
|
-
// File.text() reads the file content asynchronously
|
|
167
|
+
// File.text() reads the file content asynchronously
|
|
168
|
+
// In tests, we read directly from the Blob content synchronously
|
|
107
169
|
if (!File.prototype.text) {
|
|
108
170
|
Object.defineProperty(File.prototype, 'text', {
|
|
109
171
|
writable: true,
|
|
110
172
|
configurable: true,
|
|
111
173
|
value: async function(this: File) {
|
|
174
|
+
// For test files, read from the stored content or from the Blob
|
|
175
|
+
const file = this as any;
|
|
176
|
+
if (file._content) {
|
|
177
|
+
// Use stored content if available
|
|
178
|
+
return Promise.resolve(file._content);
|
|
179
|
+
}
|
|
180
|
+
// Otherwise, try to read from Blob using FileReader
|
|
112
181
|
return new Promise((resolve, reject) => {
|
|
113
|
-
// Use FileReader to read the file content
|
|
114
182
|
const reader = new FileReader();
|
|
115
183
|
reader.onload = (e) => {
|
|
116
184
|
resolve(e.target?.result as string);
|
|
@@ -138,23 +206,30 @@ describe('[component] ImportModal', () => {
|
|
|
138
206
|
expect(container.firstChild).toBeNull();
|
|
139
207
|
});
|
|
140
208
|
|
|
141
|
-
it('renders modal when open', () => {
|
|
209
|
+
it('renders modal when open', async () => {
|
|
142
210
|
render(<ImportModal {...baseProps} />);
|
|
143
211
|
|
|
144
|
-
//
|
|
145
|
-
|
|
212
|
+
// Wait for dialog to be accessible
|
|
213
|
+
await waitForDialog();
|
|
146
214
|
// Check for content instead of testids
|
|
147
215
|
expect(screen.getByText('Import Data')).toBeInTheDocument();
|
|
148
216
|
});
|
|
149
217
|
|
|
150
|
-
it('renders default title', () => {
|
|
218
|
+
it('renders default title', async () => {
|
|
151
219
|
render(<ImportModal {...baseProps} />);
|
|
152
220
|
|
|
153
|
-
//
|
|
154
|
-
|
|
221
|
+
// Wait for dialog content to be rendered (showModal is async via requestAnimationFrame)
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
// Try by role first, fallback to text content
|
|
224
|
+
try {
|
|
225
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
226
|
+
} catch (e) {
|
|
227
|
+
expect(screen.getByText('Import Data')).toBeInTheDocument();
|
|
228
|
+
}
|
|
229
|
+
}, { timeout: 5000 });
|
|
155
230
|
});
|
|
156
231
|
|
|
157
|
-
it('renders custom title from config', () => {
|
|
232
|
+
it('renders custom title from config', async () => {
|
|
158
233
|
render(
|
|
159
234
|
<ImportModal
|
|
160
235
|
{...baseProps}
|
|
@@ -162,18 +237,27 @@ describe('[component] ImportModal', () => {
|
|
|
162
237
|
/>
|
|
163
238
|
);
|
|
164
239
|
|
|
165
|
-
//
|
|
166
|
-
|
|
240
|
+
// Wait for dialog content to be rendered (showModal is async via requestAnimationFrame)
|
|
241
|
+
await waitFor(() => {
|
|
242
|
+
// Try by role first, fallback to text content
|
|
243
|
+
try {
|
|
244
|
+
expect(screen.getByRole('heading', { name: 'Custom Import Title' })).toBeInTheDocument();
|
|
245
|
+
} catch (e) {
|
|
246
|
+
expect(screen.getByText('Custom Import Title')).toBeInTheDocument();
|
|
247
|
+
}
|
|
248
|
+
}, { timeout: 5000 });
|
|
167
249
|
});
|
|
168
250
|
|
|
169
|
-
it('renders default description', () => {
|
|
251
|
+
it('renders default description', async () => {
|
|
170
252
|
render(<ImportModal {...baseProps} />);
|
|
171
253
|
|
|
172
|
-
//
|
|
254
|
+
// Wait for dialog to be accessible
|
|
255
|
+
await waitForDialog();
|
|
256
|
+
// Description is rendered as p in DialogHeader
|
|
173
257
|
expect(screen.getByText('Upload a CSV file to import multiple records at once.')).toBeInTheDocument();
|
|
174
258
|
});
|
|
175
259
|
|
|
176
|
-
it('renders custom description from config', () => {
|
|
260
|
+
it('renders custom description from config', async () => {
|
|
177
261
|
render(
|
|
178
262
|
<ImportModal
|
|
179
263
|
{...baseProps}
|
|
@@ -181,27 +265,79 @@ describe('[component] ImportModal', () => {
|
|
|
181
265
|
/>
|
|
182
266
|
);
|
|
183
267
|
|
|
184
|
-
//
|
|
268
|
+
// Wait for dialog to be accessible
|
|
269
|
+
await waitForDialog();
|
|
270
|
+
// Description is rendered as p in DialogHeader
|
|
185
271
|
expect(screen.getByText('Custom description')).toBeInTheDocument();
|
|
186
272
|
});
|
|
187
273
|
|
|
188
|
-
it('renders file upload area', () => {
|
|
274
|
+
it('renders file upload area', async () => {
|
|
189
275
|
render(<ImportModal {...baseProps} />);
|
|
190
276
|
|
|
191
|
-
|
|
192
|
-
|
|
277
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
278
|
+
// Use getByRole for heading to avoid multiple matches (h2 and button both have "Import Data")
|
|
279
|
+
await waitFor(() => {
|
|
280
|
+
try {
|
|
281
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
282
|
+
} catch (e) {
|
|
283
|
+
// Fallback: check if any element with "Import Data" exists
|
|
284
|
+
const elements = screen.getAllByText('Import Data');
|
|
285
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
286
|
+
}
|
|
287
|
+
}, { timeout: 5000 });
|
|
288
|
+
|
|
289
|
+
// Then check for upload area text and button (use querySelector as fallback for buttons in dialogs)
|
|
290
|
+
await waitFor(() => {
|
|
291
|
+
expect(screen.getByText(/choose a csv file/i)).toBeInTheDocument();
|
|
292
|
+
const selectFileButton = findButtonByText(/select file/i);
|
|
293
|
+
expect(selectFileButton).toBeInTheDocument();
|
|
294
|
+
}, { timeout: 5000 });
|
|
193
295
|
});
|
|
194
296
|
|
|
195
|
-
it('renders cancel button', () => {
|
|
297
|
+
it('renders cancel button', async () => {
|
|
196
298
|
render(<ImportModal {...baseProps} />);
|
|
197
299
|
|
|
198
|
-
|
|
300
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
301
|
+
// Use getByRole for heading to avoid multiple matches (h2 and button both have "Import Data")
|
|
302
|
+
await waitFor(() => {
|
|
303
|
+
try {
|
|
304
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
305
|
+
} catch (e) {
|
|
306
|
+
// Fallback: check if any element with "Import Data" exists
|
|
307
|
+
const elements = screen.getAllByText('Import Data');
|
|
308
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
309
|
+
}
|
|
310
|
+
}, { timeout: 5000 });
|
|
311
|
+
|
|
312
|
+
// Then check for cancel button (use querySelector as fallback for buttons in dialogs)
|
|
313
|
+
await waitFor(() => {
|
|
314
|
+
const cancelButton = screen.queryByRole('button', { name: /cancel/i })
|
|
315
|
+
|| Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.match(/cancel/i));
|
|
316
|
+
expect(cancelButton).toBeInTheDocument();
|
|
317
|
+
}, { timeout: 5000 });
|
|
199
318
|
});
|
|
200
319
|
|
|
201
|
-
it('renders import button', () => {
|
|
320
|
+
it('renders import button', async () => {
|
|
202
321
|
render(<ImportModal {...baseProps} />);
|
|
203
322
|
|
|
204
|
-
|
|
323
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
324
|
+
// Use getByRole for heading to avoid multiple matches (h2 and button both have "Import Data")
|
|
325
|
+
await waitFor(() => {
|
|
326
|
+
try {
|
|
327
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
328
|
+
} catch (e) {
|
|
329
|
+
// Fallback: check if any element with "Import Data" exists
|
|
330
|
+
const elements = screen.getAllByText('Import Data');
|
|
331
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
332
|
+
}
|
|
333
|
+
}, { timeout: 5000 });
|
|
334
|
+
|
|
335
|
+
// Then check for import button (use querySelector as fallback for buttons in dialogs)
|
|
336
|
+
await waitFor(() => {
|
|
337
|
+
const importButton = screen.queryByRole('button', { name: /import/i })
|
|
338
|
+
|| Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.match(/^import$/i));
|
|
339
|
+
expect(importButton).toBeInTheDocument();
|
|
340
|
+
}, { timeout: 5000 });
|
|
205
341
|
});
|
|
206
342
|
});
|
|
207
343
|
|
|
@@ -228,6 +364,13 @@ describe('[component] ImportModal', () => {
|
|
|
228
364
|
|
|
229
365
|
const { rerender } = render(<ImportModal {...baseProps} />);
|
|
230
366
|
|
|
367
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
368
|
+
await waitForDialog();
|
|
369
|
+
await waitFor(() => {
|
|
370
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
371
|
+
expect(fileInput).toBeInTheDocument();
|
|
372
|
+
}, { timeout: 5000 });
|
|
373
|
+
|
|
231
374
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
232
375
|
await user.upload(fileInput, file);
|
|
233
376
|
|
|
@@ -236,7 +379,17 @@ describe('[component] ImportModal', () => {
|
|
|
236
379
|
});
|
|
237
380
|
|
|
238
381
|
rerender(<ImportModal {...baseProps} isOpen={false} />);
|
|
382
|
+
|
|
383
|
+
// Wait for dialog to close
|
|
384
|
+
await waitFor(() => {
|
|
385
|
+
const dialog = document.querySelector('dialog[role="dialog"]');
|
|
386
|
+
expect(dialog).not.toBeInTheDocument();
|
|
387
|
+
});
|
|
388
|
+
|
|
239
389
|
rerender(<ImportModal {...baseProps} isOpen={true} />);
|
|
390
|
+
|
|
391
|
+
// Wait for dialog to reopen
|
|
392
|
+
await waitForDialog();
|
|
240
393
|
|
|
241
394
|
// File should be reset
|
|
242
395
|
expect(screen.queryByText(`Selected: ${file.name}`)).not.toBeInTheDocument();
|
|
@@ -251,20 +404,14 @@ describe('[component] ImportModal', () => {
|
|
|
251
404
|
|
|
252
405
|
render(<ImportModal {...baseProps} />);
|
|
253
406
|
|
|
254
|
-
|
|
255
|
-
await user
|
|
256
|
-
|
|
257
|
-
// Wait for preview table to appear
|
|
258
|
-
await waitFor(() => {
|
|
259
|
-
const table = screen.queryByRole('table');
|
|
260
|
-
expect(table).toBeInTheDocument();
|
|
261
|
-
}, { timeout: 5000 });
|
|
407
|
+
// Upload file and wait for preview
|
|
408
|
+
await uploadFileAndWaitForPreview(user, file);
|
|
262
409
|
|
|
263
410
|
// Once preview table is visible, check for table headers
|
|
264
411
|
await waitFor(() => {
|
|
265
412
|
expect(screen.getByText(/name/i)).toBeInTheDocument();
|
|
266
413
|
expect(screen.getByText(/email/i)).toBeInTheDocument();
|
|
267
|
-
}, { timeout:
|
|
414
|
+
}, { timeout: 5000 });
|
|
268
415
|
});
|
|
269
416
|
|
|
270
417
|
it('shows preview table with parsed data', async () => {
|
|
@@ -274,14 +421,23 @@ describe('[component] ImportModal', () => {
|
|
|
274
421
|
|
|
275
422
|
render(<ImportModal {...baseProps} />);
|
|
276
423
|
|
|
424
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
425
|
+
await waitForDialog();
|
|
426
|
+
await waitFor(() => {
|
|
427
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
428
|
+
expect(fileInput).toBeInTheDocument();
|
|
429
|
+
}, { timeout: 5000 });
|
|
430
|
+
|
|
277
431
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
278
432
|
await user.upload(fileInput, file);
|
|
279
433
|
|
|
280
434
|
// Wait for preview table to appear
|
|
281
435
|
await waitFor(() => {
|
|
282
|
-
const table = screen.queryByRole('table')
|
|
436
|
+
const table = screen.queryByRole('table') ||
|
|
437
|
+
document.querySelector('table') ||
|
|
438
|
+
document.querySelector('table.min-w-full');
|
|
283
439
|
expect(table).toBeInTheDocument();
|
|
284
|
-
}, { timeout:
|
|
440
|
+
}, { timeout: 10000 });
|
|
285
441
|
|
|
286
442
|
// Then check for data
|
|
287
443
|
await waitFor(() => {
|
|
@@ -297,14 +453,23 @@ describe('[component] ImportModal', () => {
|
|
|
297
453
|
|
|
298
454
|
render(<ImportModal {...baseProps} />);
|
|
299
455
|
|
|
456
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
457
|
+
await waitForDialog();
|
|
458
|
+
await waitFor(() => {
|
|
459
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
460
|
+
expect(fileInput).toBeInTheDocument();
|
|
461
|
+
}, { timeout: 5000 });
|
|
462
|
+
|
|
300
463
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
301
464
|
await user.upload(fileInput, file);
|
|
302
465
|
|
|
303
466
|
// Wait for preview to appear first
|
|
304
467
|
await waitFor(() => {
|
|
305
|
-
const table = screen.queryByRole('table')
|
|
468
|
+
const table = screen.queryByRole('table') ||
|
|
469
|
+
document.querySelector('table') ||
|
|
470
|
+
document.querySelector('table.min-w-full');
|
|
306
471
|
expect(table).toBeInTheDocument();
|
|
307
|
-
}, { timeout:
|
|
472
|
+
}, { timeout: 10000 });
|
|
308
473
|
|
|
309
474
|
// Then check for total row count
|
|
310
475
|
await waitFor(() => {
|
|
@@ -319,14 +484,23 @@ describe('[component] ImportModal', () => {
|
|
|
319
484
|
|
|
320
485
|
render(<ImportModal {...baseProps} />);
|
|
321
486
|
|
|
487
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
488
|
+
await waitForDialog();
|
|
489
|
+
await waitFor(() => {
|
|
490
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
491
|
+
expect(fileInput).toBeInTheDocument();
|
|
492
|
+
}, { timeout: 5000 });
|
|
493
|
+
|
|
322
494
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
323
495
|
await user.upload(fileInput, file);
|
|
324
496
|
|
|
325
497
|
// Wait for preview table first
|
|
326
498
|
await waitFor(() => {
|
|
327
|
-
const table = screen.queryByRole('table')
|
|
499
|
+
const table = screen.queryByRole('table') ||
|
|
500
|
+
document.querySelector('table') ||
|
|
501
|
+
document.querySelector('table.min-w-full');
|
|
328
502
|
expect(table).toBeInTheDocument();
|
|
329
|
-
}, { timeout:
|
|
503
|
+
}, { timeout: 10000 });
|
|
330
504
|
|
|
331
505
|
// Then check for data
|
|
332
506
|
await waitFor(() => {
|
|
@@ -341,14 +515,23 @@ describe('[component] ImportModal', () => {
|
|
|
341
515
|
|
|
342
516
|
render(<ImportModal {...baseProps} />);
|
|
343
517
|
|
|
518
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
519
|
+
await waitForDialog();
|
|
520
|
+
await waitFor(() => {
|
|
521
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
522
|
+
expect(fileInput).toBeInTheDocument();
|
|
523
|
+
}, { timeout: 5000 });
|
|
524
|
+
|
|
344
525
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
345
526
|
await user.upload(fileInput, file);
|
|
346
527
|
|
|
347
528
|
// Wait for preview table first
|
|
348
529
|
await waitFor(() => {
|
|
349
|
-
const table = screen.queryByRole('table')
|
|
530
|
+
const table = screen.queryByRole('table') ||
|
|
531
|
+
document.querySelector('table') ||
|
|
532
|
+
document.querySelector('table.min-w-full');
|
|
350
533
|
expect(table).toBeInTheDocument();
|
|
351
|
-
}, { timeout:
|
|
534
|
+
}, { timeout: 10000 });
|
|
352
535
|
|
|
353
536
|
// Then check for data
|
|
354
537
|
await waitFor(() => {
|
|
@@ -367,6 +550,13 @@ describe('[component] ImportModal', () => {
|
|
|
367
550
|
|
|
368
551
|
render(<ImportModal {...baseProps} />);
|
|
369
552
|
|
|
553
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
554
|
+
await waitForDialog();
|
|
555
|
+
await waitFor(() => {
|
|
556
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
557
|
+
expect(fileInput).toBeInTheDocument();
|
|
558
|
+
}, { timeout: 5000 });
|
|
559
|
+
|
|
370
560
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
371
561
|
await user.upload(fileInput, file);
|
|
372
562
|
|
|
@@ -386,6 +576,13 @@ describe('[component] ImportModal', () => {
|
|
|
386
576
|
|
|
387
577
|
render(<ImportModal {...baseProps} />);
|
|
388
578
|
|
|
579
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
580
|
+
await waitForDialog();
|
|
581
|
+
await waitFor(() => {
|
|
582
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
583
|
+
expect(fileInput).toBeInTheDocument();
|
|
584
|
+
}, { timeout: 5000 });
|
|
585
|
+
|
|
389
586
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
390
587
|
await user.upload(fileInput, file);
|
|
391
588
|
|
|
@@ -427,21 +624,33 @@ describe('[component] ImportModal', () => {
|
|
|
427
624
|
|
|
428
625
|
render(<ImportModal {...baseProps} onImport={onImport} />);
|
|
429
626
|
|
|
627
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
628
|
+
await waitForDialog();
|
|
629
|
+
await waitFor(() => {
|
|
630
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
631
|
+
expect(fileInput).toBeInTheDocument();
|
|
632
|
+
}, { timeout: 5000 });
|
|
633
|
+
|
|
430
634
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
431
635
|
await user.upload(fileInput, file);
|
|
432
636
|
|
|
433
637
|
// Wait for preview to appear
|
|
434
638
|
await waitFor(() => {
|
|
435
|
-
const table = screen.queryByRole('table')
|
|
639
|
+
const table = screen.queryByRole('table') ||
|
|
640
|
+
document.querySelector('table') ||
|
|
641
|
+
document.querySelector('table.min-w-full');
|
|
436
642
|
expect(table).toBeInTheDocument();
|
|
437
|
-
}, { timeout:
|
|
643
|
+
}, { timeout: 10000 });
|
|
438
644
|
|
|
439
645
|
await waitFor(() => {
|
|
440
646
|
expect(screen.getByText('John')).toBeInTheDocument();
|
|
441
647
|
}, { timeout: 2000 });
|
|
442
648
|
|
|
443
|
-
const importButton =
|
|
444
|
-
|
|
649
|
+
const importButton = findButtonByText(/^import$/i);
|
|
650
|
+
expect(importButton).toBeInTheDocument();
|
|
651
|
+
if (importButton) {
|
|
652
|
+
await user.click(importButton);
|
|
653
|
+
}
|
|
445
654
|
|
|
446
655
|
await waitFor(() => {
|
|
447
656
|
expect(onImport).toHaveBeenCalledTimes(1);
|
|
@@ -456,36 +665,66 @@ describe('[component] ImportModal', () => {
|
|
|
456
665
|
}, { timeout: 3000 });
|
|
457
666
|
});
|
|
458
667
|
|
|
459
|
-
it('disables import button when no file is selected', () => {
|
|
668
|
+
it('disables import button when no file is selected', async () => {
|
|
460
669
|
render(<ImportModal {...baseProps} />);
|
|
461
670
|
|
|
462
|
-
|
|
463
|
-
|
|
671
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
672
|
+
// Use getByRole for heading to avoid multiple matches (h2 and button both have "Import Data")
|
|
673
|
+
await waitFor(() => {
|
|
674
|
+
try {
|
|
675
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
676
|
+
} catch (e) {
|
|
677
|
+
// Fallback: check if any element with "Import Data" exists
|
|
678
|
+
const elements = screen.getAllByText('Import Data');
|
|
679
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
680
|
+
}
|
|
681
|
+
}, { timeout: 5000 });
|
|
682
|
+
|
|
683
|
+
// Then check for import button and its disabled state
|
|
684
|
+
await waitFor(() => {
|
|
685
|
+
const importButton = findButtonByText(/^import$/i);
|
|
686
|
+
expect(importButton).toBeInTheDocument();
|
|
687
|
+
expect(importButton).toBeDisabled();
|
|
688
|
+
}, { timeout: 5000 });
|
|
464
689
|
});
|
|
465
690
|
|
|
466
691
|
it('disables import button while processing', async () => {
|
|
467
692
|
const user = userEvent.setup();
|
|
468
|
-
const onImport = vi.fn(() =>
|
|
693
|
+
const onImport = vi.fn(async () => {
|
|
694
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
695
|
+
});
|
|
469
696
|
const csvContent = 'name,email\nJohn,john@example.com';
|
|
470
697
|
const file = createCSVFile(csvContent);
|
|
471
698
|
|
|
472
699
|
render(<ImportModal {...baseProps} onImport={onImport} />);
|
|
473
700
|
|
|
701
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
702
|
+
await waitForDialog();
|
|
703
|
+
await waitFor(() => {
|
|
704
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
705
|
+
expect(fileInput).toBeInTheDocument();
|
|
706
|
+
}, { timeout: 5000 });
|
|
707
|
+
|
|
474
708
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
475
709
|
await user.upload(fileInput, file);
|
|
476
710
|
|
|
477
711
|
// Wait for preview to appear
|
|
478
712
|
await waitFor(() => {
|
|
479
|
-
const table = screen.queryByRole('table')
|
|
713
|
+
const table = screen.queryByRole('table') ||
|
|
714
|
+
document.querySelector('table') ||
|
|
715
|
+
document.querySelector('table.min-w-full');
|
|
480
716
|
expect(table).toBeInTheDocument();
|
|
481
|
-
}, { timeout:
|
|
717
|
+
}, { timeout: 10000 });
|
|
482
718
|
|
|
483
719
|
await waitFor(() => {
|
|
484
720
|
expect(screen.getByText('John')).toBeInTheDocument();
|
|
485
721
|
}, { timeout: 2000 });
|
|
486
722
|
|
|
487
|
-
const importButton =
|
|
488
|
-
|
|
723
|
+
const importButton = findButtonByText(/^import$/i);
|
|
724
|
+
expect(importButton).toBeInTheDocument();
|
|
725
|
+
if (importButton) {
|
|
726
|
+
await user.click(importButton);
|
|
727
|
+
}
|
|
489
728
|
|
|
490
729
|
await waitFor(() => {
|
|
491
730
|
expect(importButton).toBeDisabled();
|
|
@@ -494,31 +733,45 @@ describe('[component] ImportModal', () => {
|
|
|
494
733
|
|
|
495
734
|
it('shows processing text while importing', async () => {
|
|
496
735
|
const user = userEvent.setup();
|
|
497
|
-
const onImport = vi.fn(() =>
|
|
736
|
+
const onImport = vi.fn(async () => {
|
|
737
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
738
|
+
});
|
|
498
739
|
const csvContent = 'name,email\nJohn,john@example.com';
|
|
499
740
|
const file = createCSVFile(csvContent);
|
|
500
741
|
|
|
501
742
|
render(<ImportModal {...baseProps} onImport={onImport} />);
|
|
502
743
|
|
|
744
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
745
|
+
await waitForDialog();
|
|
746
|
+
await waitFor(() => {
|
|
747
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
748
|
+
expect(fileInput).toBeInTheDocument();
|
|
749
|
+
}, { timeout: 5000 });
|
|
750
|
+
|
|
503
751
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
504
752
|
await user.upload(fileInput, file);
|
|
505
753
|
|
|
506
754
|
// Wait for preview to appear
|
|
507
755
|
await waitFor(() => {
|
|
508
|
-
const table = screen.queryByRole('table')
|
|
756
|
+
const table = screen.queryByRole('table') ||
|
|
757
|
+
document.querySelector('table') ||
|
|
758
|
+
document.querySelector('table.min-w-full');
|
|
509
759
|
expect(table).toBeInTheDocument();
|
|
510
|
-
}, { timeout:
|
|
760
|
+
}, { timeout: 10000 });
|
|
511
761
|
|
|
512
762
|
await waitFor(() => {
|
|
513
763
|
expect(screen.getByText('John')).toBeInTheDocument();
|
|
514
764
|
}, { timeout: 2000 });
|
|
515
765
|
|
|
516
|
-
const importButton =
|
|
517
|
-
|
|
766
|
+
const importButton = findButtonByText(/^import$/i);
|
|
767
|
+
expect(importButton).toBeInTheDocument();
|
|
768
|
+
if (importButton) {
|
|
769
|
+
await user.click(importButton);
|
|
770
|
+
}
|
|
518
771
|
|
|
519
772
|
// Button text changes to "Processing..." when isProcessing is true
|
|
520
773
|
await waitFor(() => {
|
|
521
|
-
const processingButton =
|
|
774
|
+
const processingButton = findButtonByText(/processing/i);
|
|
522
775
|
expect(processingButton).toBeInTheDocument();
|
|
523
776
|
}, { timeout: 1000 });
|
|
524
777
|
});
|
|
@@ -531,21 +784,33 @@ describe('[component] ImportModal', () => {
|
|
|
531
784
|
|
|
532
785
|
render(<ImportModal {...baseProps} onClose={onClose} />);
|
|
533
786
|
|
|
787
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
788
|
+
await waitForDialog();
|
|
789
|
+
await waitFor(() => {
|
|
790
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
791
|
+
expect(fileInput).toBeInTheDocument();
|
|
792
|
+
}, { timeout: 5000 });
|
|
793
|
+
|
|
534
794
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
535
795
|
await user.upload(fileInput, file);
|
|
536
796
|
|
|
537
797
|
// Wait for preview to appear
|
|
538
798
|
await waitFor(() => {
|
|
539
|
-
const table = screen.queryByRole('table')
|
|
799
|
+
const table = screen.queryByRole('table') ||
|
|
800
|
+
document.querySelector('table') ||
|
|
801
|
+
document.querySelector('table.min-w-full');
|
|
540
802
|
expect(table).toBeInTheDocument();
|
|
541
|
-
}, { timeout:
|
|
803
|
+
}, { timeout: 10000 });
|
|
542
804
|
|
|
543
805
|
await waitFor(() => {
|
|
544
806
|
expect(screen.getByText('John')).toBeInTheDocument();
|
|
545
807
|
}, { timeout: 2000 });
|
|
546
808
|
|
|
547
|
-
const importButton =
|
|
548
|
-
|
|
809
|
+
const importButton = findButtonByText(/^import$/i);
|
|
810
|
+
expect(importButton).toBeInTheDocument();
|
|
811
|
+
if (importButton) {
|
|
812
|
+
await user.click(importButton);
|
|
813
|
+
}
|
|
549
814
|
|
|
550
815
|
await waitFor(() => {
|
|
551
816
|
expect(onClose).toHaveBeenCalled();
|
|
@@ -560,8 +825,29 @@ describe('[component] ImportModal', () => {
|
|
|
560
825
|
|
|
561
826
|
render(<ImportModal {...baseProps} onClose={onClose} />);
|
|
562
827
|
|
|
563
|
-
|
|
564
|
-
|
|
828
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
829
|
+
// Use getByRole for heading to avoid multiple matches (h2 and button both have "Import Data")
|
|
830
|
+
await waitFor(() => {
|
|
831
|
+
try {
|
|
832
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
833
|
+
} catch (e) {
|
|
834
|
+
// Fallback: check if any element with "Import Data" exists
|
|
835
|
+
const elements = screen.getAllByText('Import Data');
|
|
836
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
837
|
+
}
|
|
838
|
+
}, { timeout: 5000 });
|
|
839
|
+
|
|
840
|
+
// Then wait for cancel button
|
|
841
|
+
await waitFor(() => {
|
|
842
|
+
const cancelButton = findButtonByText(/cancel/i);
|
|
843
|
+
expect(cancelButton).toBeInTheDocument();
|
|
844
|
+
}, { timeout: 5000 });
|
|
845
|
+
|
|
846
|
+
const cancelButton = findButtonByText(/cancel/i);
|
|
847
|
+
expect(cancelButton).toBeInTheDocument();
|
|
848
|
+
if (cancelButton) {
|
|
849
|
+
await user.click(cancelButton);
|
|
850
|
+
}
|
|
565
851
|
|
|
566
852
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
567
853
|
});
|
|
@@ -573,14 +859,23 @@ describe('[component] ImportModal', () => {
|
|
|
573
859
|
|
|
574
860
|
const { rerender } = render(<ImportModal {...baseProps} />);
|
|
575
861
|
|
|
862
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
863
|
+
await waitForDialog();
|
|
864
|
+
await waitFor(() => {
|
|
865
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
866
|
+
expect(fileInput).toBeInTheDocument();
|
|
867
|
+
}, { timeout: 5000 });
|
|
868
|
+
|
|
576
869
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
577
870
|
await user.upload(fileInput, file);
|
|
578
871
|
|
|
579
872
|
// Wait for preview to appear
|
|
580
873
|
await waitFor(() => {
|
|
581
|
-
const table = screen.queryByRole('table')
|
|
874
|
+
const table = screen.queryByRole('table') ||
|
|
875
|
+
document.querySelector('table') ||
|
|
876
|
+
document.querySelector('table.min-w-full');
|
|
582
877
|
expect(table).toBeInTheDocument();
|
|
583
|
-
}, { timeout:
|
|
878
|
+
}, { timeout: 10000 });
|
|
584
879
|
|
|
585
880
|
await waitFor(() => {
|
|
586
881
|
expect(screen.getByText('John')).toBeInTheDocument();
|
|
@@ -597,7 +892,7 @@ describe('[component] ImportModal', () => {
|
|
|
597
892
|
});
|
|
598
893
|
|
|
599
894
|
describe('Custom Configuration', () => {
|
|
600
|
-
it('uses custom button texts from config', () => {
|
|
895
|
+
it('uses custom button texts from config', async () => {
|
|
601
896
|
render(
|
|
602
897
|
<ImportModal
|
|
603
898
|
{...baseProps}
|
|
@@ -609,11 +904,27 @@ describe('[component] ImportModal', () => {
|
|
|
609
904
|
/>
|
|
610
905
|
);
|
|
611
906
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
907
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
908
|
+
// Use getByRole for heading to avoid multiple matches (h2 and button both have "Import Data")
|
|
909
|
+
await waitFor(() => {
|
|
910
|
+
try {
|
|
911
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
912
|
+
} catch (e) {
|
|
913
|
+
// Fallback: check if any element with "Import Data" exists
|
|
914
|
+
const elements = screen.getAllByText('Import Data');
|
|
915
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
916
|
+
}
|
|
917
|
+
}, { timeout: 5000 });
|
|
918
|
+
|
|
919
|
+
// Then wait for buttons
|
|
920
|
+
await waitFor(() => {
|
|
921
|
+
const browseButton = findButtonByText(/browse files/i);
|
|
922
|
+
const importDataButton = findButtonByText(/import data/i);
|
|
923
|
+
expect(browseButton).toBeInTheDocument();
|
|
924
|
+
expect(importDataButton).toBeInTheDocument();
|
|
925
|
+
}, { timeout: 5000 });
|
|
926
|
+
// Dialog has a close button too, so find the Cancel one by text
|
|
927
|
+
const cancelButton = findButtonByText(/^close$/i);
|
|
617
928
|
expect(cancelButton).toBeInTheDocument();
|
|
618
929
|
});
|
|
619
930
|
|
|
@@ -650,14 +961,23 @@ describe('[component] ImportModal', () => {
|
|
|
650
961
|
/>
|
|
651
962
|
);
|
|
652
963
|
|
|
964
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
965
|
+
await waitForDialog();
|
|
966
|
+
await waitFor(() => {
|
|
967
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
968
|
+
expect(fileInput).toBeInTheDocument();
|
|
969
|
+
}, { timeout: 5000 });
|
|
970
|
+
|
|
653
971
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
654
972
|
await user.upload(fileInput, file);
|
|
655
973
|
|
|
656
974
|
// Wait for preview to appear
|
|
657
975
|
await waitFor(() => {
|
|
658
|
-
const table = screen.queryByRole('table')
|
|
976
|
+
const table = screen.queryByRole('table') ||
|
|
977
|
+
document.querySelector('table') ||
|
|
978
|
+
document.querySelector('table.min-w-full');
|
|
659
979
|
expect(table).toBeInTheDocument();
|
|
660
|
-
}, { timeout:
|
|
980
|
+
}, { timeout: 10000 });
|
|
661
981
|
|
|
662
982
|
await waitFor(() => {
|
|
663
983
|
expect(screen.getByText(/found 2 records/i)).toBeInTheDocument();
|
|
@@ -672,6 +992,13 @@ describe('[component] ImportModal', () => {
|
|
|
672
992
|
|
|
673
993
|
render(<ImportModal {...baseProps} />);
|
|
674
994
|
|
|
995
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
996
|
+
await waitForDialog();
|
|
997
|
+
await waitFor(() => {
|
|
998
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
999
|
+
expect(fileInput).toBeInTheDocument();
|
|
1000
|
+
}, { timeout: 5000 });
|
|
1001
|
+
|
|
675
1002
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
676
1003
|
await user.upload(fileInput, file);
|
|
677
1004
|
|
|
@@ -686,6 +1013,13 @@ describe('[component] ImportModal', () => {
|
|
|
686
1013
|
|
|
687
1014
|
render(<ImportModal {...baseProps} />);
|
|
688
1015
|
|
|
1016
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
1017
|
+
await waitForDialog();
|
|
1018
|
+
await waitFor(() => {
|
|
1019
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
1020
|
+
expect(fileInput).toBeInTheDocument();
|
|
1021
|
+
}, { timeout: 5000 });
|
|
1022
|
+
|
|
689
1023
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
690
1024
|
await user.upload(fileInput, file);
|
|
691
1025
|
|
|
@@ -703,6 +1037,13 @@ describe('[component] ImportModal', () => {
|
|
|
703
1037
|
|
|
704
1038
|
render(<ImportModal {...baseProps} />);
|
|
705
1039
|
|
|
1040
|
+
// Wait for dialog to be accessible first, then wait for file input
|
|
1041
|
+
await waitForDialog();
|
|
1042
|
+
await waitFor(() => {
|
|
1043
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
1044
|
+
expect(fileInput).toBeInTheDocument();
|
|
1045
|
+
}, { timeout: 5000 });
|
|
1046
|
+
|
|
706
1047
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
707
1048
|
await user.upload(fileInput, file);
|
|
708
1049
|
|
|
@@ -722,12 +1063,30 @@ describe('[component] ImportModal', () => {
|
|
|
722
1063
|
expect(fileInput).toHaveAttribute('accept', '.csv');
|
|
723
1064
|
});
|
|
724
1065
|
|
|
725
|
-
it('provides accessible button labels', () => {
|
|
1066
|
+
it('provides accessible button labels', async () => {
|
|
726
1067
|
render(<ImportModal {...baseProps} />);
|
|
727
1068
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1069
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
1070
|
+
// Use getByRole for heading to avoid multiple matches (h2 and button both have "Import Data")
|
|
1071
|
+
await waitFor(() => {
|
|
1072
|
+
try {
|
|
1073
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
1074
|
+
} catch (e) {
|
|
1075
|
+
// Fallback: check if any element with "Import Data" exists
|
|
1076
|
+
const elements = screen.getAllByText('Import Data');
|
|
1077
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
1078
|
+
}
|
|
1079
|
+
}, { timeout: 5000 });
|
|
1080
|
+
|
|
1081
|
+
// Then wait for buttons
|
|
1082
|
+
await waitFor(() => {
|
|
1083
|
+
const selectFileButton = findButtonByText(/select file/i);
|
|
1084
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1085
|
+
const cancelButton = findButtonByText(/cancel/i);
|
|
1086
|
+
expect(selectFileButton).toBeInTheDocument();
|
|
1087
|
+
expect(importButton).toBeInTheDocument();
|
|
1088
|
+
expect(cancelButton).toBeInTheDocument();
|
|
1089
|
+
}, { timeout: 5000 });
|
|
731
1090
|
});
|
|
732
1091
|
});
|
|
733
1092
|
});
|