@jmruthers/pace-core 0.6.10 → 0.6.12
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 +21 -0
- package/audit-tool/00-dependencies.cjs +46 -13
- package/audit-tool/audits/01-pace-core-compliance.cjs +96 -21
- package/audit-tool/audits/02-project-structure.cjs +13 -3
- package/audit-tool/audits/03-architecture.cjs +78 -4
- package/audit-tool/audits/04-code-quality.cjs +9 -2
- package/audit-tool/audits/05-styling.cjs +19 -7
- package/audit-tool/audits/06-security-rbac.cjs +105 -14
- package/audit-tool/audits/07-api-tech-stack.cjs +31 -15
- package/audit-tool/audits/08-testing-documentation.cjs +11 -3
- package/audit-tool/audits/09-operations.cjs +19 -7
- package/audit-tool/index.cjs +22 -11
- package/audit-tool/utils/report-utils.cjs +4 -0
- package/cursor-rules/01-pace-core-compliance.mdc +1 -0
- package/cursor-rules/02-project-structure.mdc +1 -0
- package/cursor-rules/03-architecture.mdc +3 -1
- package/cursor-rules/04-code-quality.mdc +1 -0
- package/cursor-rules/05-styling.mdc +41 -7
- package/cursor-rules/06-security-rbac.mdc +2 -1
- package/cursor-rules/07-api-tech-stack.mdc +1 -0
- package/cursor-rules/08-testing-documentation.mdc +1 -0
- package/cursor-rules/09-operations.mdc +1 -0
- package/dist/DataTable-AQAHSFLM.js +17 -0
- package/dist/InactivityServiceProvider-BbxwwDz1.d.ts +308 -0
- package/dist/UnifiedAuthProvider-Bkt_tzdS.d.ts +183 -0
- package/dist/api-6OQXYT67.js +6 -0
- package/dist/api-result-USV1Czr-.d.ts +51 -0
- package/dist/audit-HI2DHUVU.js +4 -0
- package/dist/auth-JvdRVaud.d.ts +49 -0
- package/dist/chunk-2DL2WSOE.js +327 -0
- package/dist/chunk-2GBDPPUC.js +61 -0
- package/dist/chunk-44CNXN4P.js +15 -0
- package/dist/chunk-AP5FG7W4.js +2159 -0
- package/dist/chunk-CU2BU2MQ.js +2 -0
- package/dist/chunk-D6BMFMQZ.js +200 -0
- package/dist/chunk-ENLXB7GP.js +721 -0
- package/dist/chunk-GHCUP64P.js +105 -0
- package/dist/chunk-H6RTU4DZ.js +1098 -0
- package/dist/chunk-HQTYP6BX.js +6468 -0
- package/dist/chunk-M7QE7XOA.js +529 -0
- package/dist/chunk-MVVWZ7JV.js +1329 -0
- package/dist/chunk-NJ7FGQWB.js +811 -0
- package/dist/chunk-QRYSEPHB.js +429 -0
- package/dist/chunk-QWIG36BZ.js +264 -0
- package/dist/chunk-S57OLCLO.js +2946 -0
- package/dist/chunk-VFLR5K2H.js +23 -0
- package/dist/chunk-XOJME5T7.js +407 -0
- package/dist/chunk-XPFVT3GN.js +492 -0
- package/dist/chunk-Y2LWSLLB.js +9614 -0
- package/dist/chunk-YFGNMB67.js +2390 -0
- package/dist/components.d.ts +10 -9
- package/dist/components.js +20 -17
- package/dist/database.generated-qkdoiVrJ.d.ts +9441 -0
- package/dist/eslint-rules/index.cjs +3 -0
- package/dist/eslint-rules/rules/03-architecture.cjs +74 -0
- package/dist/eslint-rules/rules/06-security-rbac.cjs +74 -0
- package/dist/event-BfCox3N2.d.ts +265 -0
- package/dist/file-reference-DU1hcawx.d.ts +164 -0
- package/dist/functions-hF5ImHCr.d.ts +208 -0
- package/dist/hooks.d.ts +22 -9
- package/dist/hooks.js +35 -25
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +69 -180
- package/dist/index.js +319 -342
- package/dist/pagination-BW1mqywp.d.ts +201 -0
- package/dist/providers.d.ts +6 -5
- package/dist/providers.js +6 -3
- package/dist/rbac/index.d.ts +133 -148
- package/dist/rbac/index.js +12 -9
- package/dist/theming/runtime.d.ts +19 -2
- package/dist/theming/runtime.js +1 -1
- package/dist/timezone-BTWWXKVY.d.ts +696 -0
- package/dist/types-Besvoyzb.d.ts +55 -0
- package/dist/types-CGHrxfqc.d.ts +114 -0
- package/dist/types.d.ts +19 -12
- package/dist/types.js +1 -0
- package/dist/usePublicPageContext-BQrHf95t.d.ts +4367 -0
- package/dist/usePublicRouteParams-BgV6VhMi.d.ts +946 -0
- package/dist/utils.d.ts +163 -145
- package/dist/utils.js +45 -27
- package/docs/api/@jmruthers/namespaces/DialogPortal/README.md +14 -0
- package/docs/api/@jmruthers/namespaces/DialogPortal/variables/displayName.md +11 -0
- package/docs/api/README.md +6 -2
- package/docs/api/_media/README.md +186 -0
- package/docs/api/classes/ColumnFactory.md +225 -0
- package/docs/api/classes/Logger.md +246 -0
- package/docs/api/classes/RBACAuditManager.md +299 -0
- package/docs/api/classes/RBACCache.md +389 -0
- package/docs/api/classes/RBACEngine.md +181 -0
- package/docs/api/classes/SecureSupabaseClient.md +168 -0
- package/docs/api/classes/StorageUtils.md +324 -0
- package/docs/api/enumerations/FileCategory.md +137 -0
- package/docs/api/enumerations/LogLevel.md +43 -0
- package/docs/api/enumerations/RBACErrorCode.md +169 -0
- package/docs/api/enumerations/RPCFunction.md +89 -0
- package/docs/api/functions/AccessDenied.md +30 -0
- package/docs/api/functions/AppSwitcher.md +21 -0
- package/docs/api/functions/Badge.md +42 -0
- package/docs/api/functions/ContextSelector.md +43 -0
- package/docs/api/functions/DataTable.md +36 -0
- package/docs/api/functions/DatePickerWithTimezone.md +28 -0
- package/docs/api/functions/DialogBody.md +24 -0
- package/docs/api/functions/DialogFooter.md +24 -0
- package/docs/api/functions/DialogHeader.md +24 -0
- package/docs/api/functions/ErrorBoundaryProvider.md +28 -0
- package/docs/api/functions/EventServiceProvider.md +28 -0
- package/docs/api/functions/FileDisplay.md +25 -0
- package/docs/api/functions/FileUpload.md +21 -0
- package/docs/api/functions/Form.md +50 -0
- package/docs/api/functions/FormField.md +102 -0
- package/docs/api/functions/Header.md +21 -0
- package/docs/api/functions/InactivityServiceProvider.md +28 -0
- package/docs/api/functions/InactivityWarningModal.md +21 -0
- package/docs/api/functions/Input.md +49 -0
- package/docs/api/functions/NavigationGuard.md +31 -0
- package/docs/api/functions/OrganisationServiceProvider.md +28 -0
- package/docs/api/functions/PaceAppLayout.md +169 -0
- package/docs/api/functions/PasswordChangeForm.md +21 -0
- package/docs/api/functions/ProtectedRoute.md +37 -0
- package/docs/api/functions/PublicPageFooter.md +21 -0
- package/docs/api/functions/PublicPageHeader.md +21 -0
- package/docs/api/functions/PublicPageLayout.md +33 -0
- package/docs/api/functions/PublicPageProvider.md +30 -0
- package/docs/api/functions/Textarea.md +49 -0
- package/docs/api/functions/Toaster.md +34 -0
- package/docs/api/functions/UnifiedAuthProvider.md +28 -0
- package/docs/api/functions/applyPalette.md +33 -0
- package/docs/api/functions/archiveFile.md +47 -0
- package/docs/api/functions/average.md +63 -0
- package/docs/api/functions/buildAppUrl.md +46 -0
- package/docs/api/functions/clearInFlightRequests.md +17 -0
- package/docs/api/functions/clearPalette.md +18 -0
- package/docs/api/functions/clearPublicEventCache.md +18 -0
- package/docs/api/functions/clearPublicFileDisplayCache.md +18 -0
- package/docs/api/functions/clearPublicLogoCache.md +18 -0
- package/docs/api/functions/cn.md +21 -0
- package/docs/api/functions/count.md +56 -0
- package/docs/api/functions/createAuditManager.md +45 -0
- package/docs/api/functions/createBaseClient.md +75 -0
- package/docs/api/functions/createLogger.md +95 -0
- package/docs/api/functions/createRBACConfig.md +21 -0
- package/docs/api/functions/createRBACEngine.md +33 -0
- package/docs/api/functions/createRBACExpressMiddleware.md +84 -0
- package/docs/api/functions/createRBACMiddleware.md +88 -0
- package/docs/api/functions/createSecureClient.md +80 -0
- package/docs/api/functions/createSecureDataAccess.md +39 -0
- package/docs/api/functions/deleteFile.md +33 -0
- package/docs/api/functions/disablePerformanceMonitoring.md +17 -0
- package/docs/api/functions/downloadFile.md +33 -0
- package/docs/api/functions/emitAuditEvent.md +27 -0
- package/docs/api/functions/enablePerformanceMonitoring.md +17 -0
- package/docs/api/functions/err.md +23 -0
- package/docs/api/functions/exportToCSV.md +56 -0
- package/docs/api/functions/exportToCSVWithTableRows.md +46 -0
- package/docs/api/functions/extractEventCodeFromPath.md +24 -0
- package/docs/api/functions/extractFileMetadata.md +33 -0
- package/docs/api/functions/formatCompactNumber.md +27 -0
- package/docs/api/functions/formatCurrency.md +31 -0
- package/docs/api/functions/formatDate.md +23 -0
- package/docs/api/functions/formatDateTime.md +24 -0
- package/docs/api/functions/formatFileSize.md +23 -0
- package/docs/api/functions/formatInTimeZone.md +46 -0
- package/docs/api/functions/formatNumber.md +31 -0
- package/docs/api/functions/formatPercent.md +64 -0
- package/docs/api/functions/formatTime.md +24 -0
- package/docs/api/functions/formatTimeInTimeZone.md +40 -0
- package/docs/api/functions/fromSupabaseClient.md +49 -0
- package/docs/api/functions/fromZonedTime.md +41 -0
- package/docs/api/functions/generateCSVContent.md +55 -0
- package/docs/api/functions/generateFilePath.md +29 -0
- package/docs/api/functions/generateFileUrlsBatch.md +33 -0
- package/docs/api/functions/generatePublicRoutePath.md +27 -0
- package/docs/api/functions/generateUniqueFileName.md +24 -0
- package/docs/api/functions/getAccessLevel.md +48 -0
- package/docs/api/functions/getAllAppPorts.md +19 -0
- package/docs/api/functions/getAllStylePaths.md +15 -0
- package/docs/api/functions/getAppConfig.md +17 -0
- package/docs/api/functions/getAppPort.md +34 -0
- package/docs/api/functions/getBucketName.md +27 -0
- package/docs/api/functions/getCurrentAppId.md +17 -0
- package/docs/api/functions/getCurrentAppName.md +17 -0
- package/docs/api/functions/getFileSizeLimit.md +23 -0
- package/docs/api/functions/getGlobalAuditManager.md +19 -0
- package/docs/api/functions/getInFlightRequestCount.md +19 -0
- package/docs/api/functions/getPerformanceMetrics.md +17 -0
- package/docs/api/functions/getPerformanceSummary.md +17 -0
- package/docs/api/functions/getPermissionMap.md +52 -0
- package/docs/api/functions/getPublicEventCacheStats.md +25 -0
- package/docs/api/functions/getPublicFileDisplayCacheStats.md +25 -0
- package/docs/api/functions/getPublicLogoCacheStats.md +25 -0
- package/docs/api/functions/getPublicUrl.md +39 -0
- package/docs/api/functions/getRBACConfig.md +15 -0
- package/docs/api/functions/getRBACLogger.md +15 -0
- package/docs/api/functions/getRoleContext.md +31 -0
- package/docs/api/functions/getSignedUrl.md +34 -0
- package/docs/api/functions/getStylePath.md +21 -0
- package/docs/api/functions/getTimeZoneDifference.md +40 -0
- package/docs/api/functions/getTimezoneAbbreviation.md +40 -0
- package/docs/api/functions/getUserTimeZone.md +26 -0
- package/docs/api/functions/hasAllPermissions.md +41 -0
- package/docs/api/functions/hasAnyPermission.md +41 -0
- package/docs/api/functions/isDebugMode.md +15 -0
- package/docs/api/functions/isDevelopmentMode.md +15 -0
- package/docs/api/functions/isErr.md +29 -0
- package/docs/api/functions/isOk.md +29 -0
- package/docs/api/functions/isPerformanceMonitoringEnabled.md +17 -0
- package/docs/api/functions/isPermitted.md +58 -0
- package/docs/api/functions/isPermittedCached.md +36 -0
- package/docs/api/functions/isRBACInitialized.md +19 -0
- package/docs/api/functions/isSecureClient.md +38 -0
- package/docs/api/functions/isValidPermission.md +27 -0
- package/docs/api/functions/listFiles.md +29 -0
- package/docs/api/functions/max.md +63 -0
- package/docs/api/functions/min.md +63 -0
- package/docs/api/functions/ok.md +29 -0
- package/docs/api/functions/parseAndNormalizeEventColours.md +105 -0
- package/docs/api/functions/recordAuditEvent.md +23 -0
- package/docs/api/functions/recordPermissionCheck.md +31 -0
- package/docs/api/functions/resetPerformanceMetrics.md +17 -0
- package/docs/api/functions/resolveAppContext.md +27 -0
- package/docs/api/functions/roundToNearestMinutes.md +41 -0
- package/docs/api/functions/sanitizeFormData.md +49 -0
- package/docs/api/functions/sanitizeHtml.md +39 -0
- package/docs/api/functions/sanitizeUserInput.md +27 -0
- package/docs/api/functions/setAppConfig.md +23 -0
- package/docs/api/functions/setGlobalAuditManager.md +25 -0
- package/docs/api/functions/setupRBAC.md +31 -0
- package/docs/api/functions/sum.md +63 -0
- package/docs/api/functions/toZonedTime.md +41 -0
- package/docs/api/functions/uploadFile.md +32 -0
- package/docs/api/functions/useAccessLevel.md +71 -0
- package/docs/api/functions/useAccessibleApps.md +55 -0
- package/docs/api/functions/useAppConfig.md +20 -0
- package/docs/api/functions/useAuthService.md +15 -0
- package/docs/api/functions/useCan.md +99 -0
- package/docs/api/functions/useEventService.md +15 -0
- package/docs/api/functions/useEventTheme.md +26 -0
- package/docs/api/functions/useEvents.md +45 -0
- package/docs/api/functions/useFileReference.md +264 -0
- package/docs/api/functions/useFileReferenceById.md +63 -0
- package/docs/api/functions/useFileReferenceForRecord.md +129 -0
- package/docs/api/functions/useFilesByCategory.md +80 -0
- package/docs/api/functions/useFormDialog.md +62 -0
- package/docs/api/functions/useInactivityService.md +15 -0
- package/docs/api/functions/useInactivityTracker.md +21 -0
- package/docs/api/functions/useIsPublicPage.md +19 -0
- package/docs/api/functions/useMultiplePermissions.md +88 -0
- package/docs/api/functions/useOptionalEvents.md +31 -0
- package/docs/api/functions/useOrganisationPermissions.md +27 -0
- package/docs/api/functions/useOrganisationSecurity.md +15 -0
- package/docs/api/functions/useOrganisationService.md +15 -0
- package/docs/api/functions/useOrganisations.md +48 -0
- package/docs/api/functions/usePermissions.md +130 -0
- package/docs/api/functions/usePublicEvent.md +36 -0
- package/docs/api/functions/usePublicEventCode.md +32 -0
- package/docs/api/functions/usePublicEventLogo.md +48 -0
- package/docs/api/functions/usePublicFileDisplay.md +54 -0
- package/docs/api/functions/usePublicPageContext.md +19 -0
- package/docs/api/functions/usePublicRouteParams.md +31 -0
- package/docs/api/functions/useRBAC.md +21 -0
- package/docs/api/functions/useResolvedScope.md +46 -0
- package/docs/api/functions/useResourcePermissions.md +25 -0
- package/docs/api/functions/useRoleManagement.md +121 -0
- package/docs/api/functions/useSecureSupabase.md +51 -0
- package/docs/api/functions/useSessionRestoration.md +15 -0
- package/docs/api/functions/useSessionTracking.md +62 -0
- package/docs/api/functions/useToast.md +83 -0
- package/docs/api/functions/useUnifiedAuth.md +24 -0
- package/docs/api/functions/useZodForm.md +27 -0
- package/docs/api/functions/validateFileSize.md +31 -0
- package/docs/api/functions/warnIfInsecureClient.md +40 -0
- package/docs/api/functions/withAccessLevelGuard.md +67 -0
- package/docs/api/functions/withPermissionGuard.md +73 -0
- package/docs/api/functions/withRoleGuard.md +86 -0
- package/docs/api/globals.md +502 -0
- package/docs/api/interfaces/AccessDeniedProps.md +87 -0
- package/docs/api/interfaces/AccessibleApp.md +41 -0
- package/docs/api/interfaces/AddressFieldProps.md +195 -0
- package/docs/api/interfaces/AddressFieldRef.md +67 -0
- package/docs/api/interfaces/AggregateConfig.md +35 -0
- package/docs/api/interfaces/AppSwitcherProps.md +51 -0
- package/docs/api/interfaces/AuthSessionData.md +27 -0
- package/docs/api/interfaces/AutocompleteOptions.md +61 -0
- package/docs/api/interfaces/AvatarProps.md +97 -0
- package/docs/api/interfaces/BadgeProps.md +30 -0
- package/docs/api/interfaces/BuildAppUrlOptions.md +41 -0
- package/docs/api/interfaces/ButtonProps.md +46 -0
- package/docs/api/interfaces/CalendarProps.md +60 -0
- package/docs/api/interfaces/CardProps.md +56 -0
- package/docs/api/interfaces/ColorPalette.md +13 -0
- package/docs/api/interfaces/ColorShade.md +58 -0
- package/docs/api/interfaces/ContextSelectorProps.md +131 -0
- package/docs/api/interfaces/DataRecord.md +16 -0
- package/docs/api/interfaces/DataTableAction.md +198 -0
- package/docs/api/interfaces/DataTableColumn.md +422 -0
- package/docs/api/interfaces/DataTableProps.md +511 -0
- package/docs/api/interfaces/DataTableToolbarButton.md +75 -0
- package/docs/api/interfaces/DatePickerWithTimezoneProps.md +75 -0
- package/docs/api/interfaces/DialogBodyProps.md +55 -0
- package/docs/api/interfaces/DialogCloseProps.md +25 -0
- package/docs/api/interfaces/DialogContentProps.md +160 -0
- package/docs/api/interfaces/DialogFooterProps.md +25 -0
- package/docs/api/interfaces/DialogHeaderProps.md +25 -0
- package/docs/api/interfaces/DialogPortalProps.md +19 -0
- package/docs/api/interfaces/DialogProps.md +53 -0
- package/docs/api/interfaces/DialogTriggerProps.md +53 -0
- package/docs/api/interfaces/EmptyStateConfig.md +55 -0
- package/docs/api/interfaces/ErrorBoundaryProps.md +131 -0
- package/docs/api/interfaces/ErrorBoundaryProviderProps.md +31 -0
- package/docs/api/interfaces/ErrorBoundaryState.md +61 -0
- package/docs/api/interfaces/EventAppRoleData.md +54 -0
- package/docs/api/interfaces/ExportColumn.md +69 -0
- package/docs/api/interfaces/ExportOptions.md +109 -0
- package/docs/api/interfaces/FileDisplayProps.md +192 -0
- package/docs/api/interfaces/FileMetadata.md +97 -0
- package/docs/api/interfaces/FileReference.md +89 -0
- package/docs/api/interfaces/FileSizeLimits.md +13 -0
- package/docs/api/interfaces/FileUploadOptions.md +107 -0
- package/docs/api/interfaces/FooterProps.md +37 -0
- package/docs/api/interfaces/FormFieldProps.md +171 -0
- package/docs/api/interfaces/FormProps.md +93 -0
- package/docs/api/interfaces/GrantEventAppRoleParams.md +97 -0
- package/docs/api/interfaces/ImportSummary.md +49 -0
- package/docs/api/interfaces/InactivityWarningModalProps.md +87 -0
- package/docs/api/interfaces/InputProps.md +46 -0
- package/docs/api/interfaces/InvalidScopeError.md +37 -0
- package/docs/api/interfaces/LabelProps.md +85 -0
- package/docs/api/interfaces/LoggerConfig.md +51 -0
- package/docs/api/interfaces/LoginFormProps.md +146 -0
- package/docs/api/interfaces/MissingUserContextError.md +37 -0
- package/docs/api/interfaces/NavigationGuardProps.md +109 -0
- package/docs/api/interfaces/NavigationItem.md +91 -0
- package/docs/api/interfaces/NavigationMenuProps.md +169 -0
- package/docs/api/interfaces/Organisation.md +105 -0
- package/docs/api/interfaces/OrganisationContextRequiredError.md +37 -0
- package/docs/api/interfaces/OrganisationMembership.md +105 -0
- package/docs/api/interfaces/OrganisationSecurityError.md +49 -0
- package/docs/api/interfaces/PaceAppLayoutPermissionConfig.md +127 -0
- package/docs/api/interfaces/PaceAppLayoutRouteConfigItem.md +91 -0
- package/docs/api/interfaces/PaceAppLayoutRoutingConfig.md +79 -0
- package/docs/api/interfaces/PaceLoginPageProps.md +41 -0
- package/docs/api/interfaces/PagePermissionGuardProps.md +143 -0
- package/docs/api/interfaces/PaletteData.md +33 -0
- package/docs/api/interfaces/ParsedAddress.md +91 -0
- package/docs/api/interfaces/PermissionDeniedError.md +37 -0
- package/docs/api/interfaces/ProgressProps.md +35 -0
- package/docs/api/interfaces/ProtectedRouteProps.md +67 -0
- package/docs/api/interfaces/PublicPageFooterProps.md +97 -0
- package/docs/api/interfaces/PublicPageHeaderProps.md +99 -0
- package/docs/api/interfaces/PublicPageLayoutProps.md +153 -0
- package/docs/api/interfaces/RBACAccessValidateParams.md +41 -0
- package/docs/api/interfaces/RBACAccessValidateResult.md +33 -0
- package/docs/api/interfaces/RBACAuditLogParams.md +65 -0
- package/docs/api/interfaces/RBACAuditLogResult.md +41 -0
- package/docs/api/interfaces/RBACContext.md +41 -0
- package/docs/api/interfaces/RBACError.md +37 -0
- package/docs/api/interfaces/RBACLogger.md +97 -0
- package/docs/api/interfaces/RBACNotInitializedError.md +37 -0
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +57 -0
- package/docs/api/interfaces/RBACPerformanceMetrics.md +109 -0
- package/docs/api/interfaces/RBACPermissionCheckParams.md +57 -0
- package/docs/api/interfaces/RBACPermissionCheckResult.md +41 -0
- package/docs/api/interfaces/RBACPermissionsGetParams.md +49 -0
- package/docs/api/interfaces/RBACPermissionsGetResult.md +49 -0
- package/docs/api/interfaces/RBACResult.md +47 -0
- package/docs/api/interfaces/RBACRoleGrantParams.md +49 -0
- package/docs/api/interfaces/RBACRoleGrantResult.md +41 -0
- package/docs/api/interfaces/RBACRoleRevokeParams.md +49 -0
- package/docs/api/interfaces/RBACRoleRevokeResult.md +41 -0
- package/docs/api/interfaces/RBACRoleValidateParams.md +41 -0
- package/docs/api/interfaces/RBACRoleValidateResult.md +49 -0
- package/docs/api/interfaces/RBACRolesListParams.md +41 -0
- package/docs/api/interfaces/RBACRolesListResult.md +57 -0
- package/docs/api/interfaces/RBACSessionTrackParams.md +57 -0
- package/docs/api/interfaces/RBACSessionTrackResult.md +41 -0
- package/docs/api/interfaces/ResourcePermissions.md +119 -0
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +81 -0
- package/docs/api/interfaces/RoleManagementResult.md +41 -0
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +29 -0
- package/docs/api/interfaces/StorageConfig.md +33 -0
- package/docs/api/interfaces/StorageFileInfo.md +57 -0
- package/docs/api/interfaces/StorageFileMetadata.md +113 -0
- package/docs/api/interfaces/StorageListOptions.md +79 -0
- package/docs/api/interfaces/StorageListResult.md +33 -0
- package/docs/api/interfaces/StorageUploadOptions.md +81 -0
- package/docs/api/interfaces/StorageUploadResult.md +49 -0
- package/docs/api/interfaces/StorageUploadSuccess.md +35 -0
- package/docs/api/interfaces/StorageUrlOptions.md +49 -0
- package/docs/api/interfaces/StyleImport.md +17 -0
- package/docs/api/interfaces/SwitchProps.md +30 -0
- package/docs/api/interfaces/TabsContentProps.md +13 -0
- package/docs/api/interfaces/TabsListProps.md +13 -0
- package/docs/api/interfaces/TabsProps.md +13 -0
- package/docs/api/interfaces/TabsTriggerProps.md +41 -0
- package/docs/api/interfaces/TextareaProps.md +43 -0
- package/docs/api/interfaces/ToastActionElement.md +16 -0
- package/docs/api/interfaces/ToastProps.md +13 -0
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +129 -0
- package/docs/api/interfaces/UseAccessibleAppsReturn.md +55 -0
- package/docs/api/interfaces/UseFormDialogOptions.md +49 -0
- package/docs/api/interfaces/UseFormDialogReturn.md +95 -0
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +103 -0
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +91 -0
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +69 -0
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +66 -0
- package/docs/api/interfaces/UsePublicEventOptions.md +29 -0
- package/docs/api/interfaces/UsePublicEventReturn.md +56 -0
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +39 -0
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +96 -0
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +76 -0
- package/docs/api/interfaces/UseResolvedScopeOptions.md +49 -0
- package/docs/api/interfaces/UseResolvedScopeReturn.md +39 -0
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +29 -0
- package/docs/api/interfaces/UserEventAccess.md +92 -0
- package/docs/api/interfaces/UserMenuProps.md +69 -0
- package/docs/api/interfaces/UserProfile.md +49 -0
- package/docs/api/type-aliases/AccessLevel.md +11 -0
- package/docs/api/type-aliases/AccessLevelContext.md +14 -0
- package/docs/api/type-aliases/AllPermissions.md +11 -0
- package/docs/api/type-aliases/ApiError.md +37 -0
- package/docs/api/type-aliases/ApiResult.md +19 -0
- package/docs/api/type-aliases/AuditEventType.md +11 -0
- package/docs/api/type-aliases/BadgeVariant.md +18 -0
- package/docs/api/type-aliases/DataTableFeatureConfig.md +14 -0
- package/docs/api/type-aliases/DialogSize.md +13 -0
- package/docs/api/type-aliases/EventAppRole.md +11 -0
- package/docs/api/type-aliases/FileUploadProps.md +15 -0
- package/docs/api/type-aliases/GetRowId.md +33 -0
- package/docs/api/type-aliases/GlobalErrorHandler.md +35 -0
- package/docs/api/type-aliases/GlobalRole.md +11 -0
- package/docs/api/type-aliases/ImportHandlerResult.md +13 -0
- package/docs/api/type-aliases/NavigationMode.md +13 -0
- package/docs/api/type-aliases/Operation.md +11 -0
- package/docs/api/type-aliases/OrganisationContextType.md +13 -0
- package/docs/api/type-aliases/OrganisationRole.md +11 -0
- package/docs/api/type-aliases/PaceAppLayoutProps.md +14 -0
- package/docs/api/type-aliases/Permission.md +11 -0
- package/docs/api/type-aliases/PermissionCheck.md +46 -0
- package/docs/api/type-aliases/PermissionMap.md +11 -0
- package/docs/api/type-aliases/PermissionSource.md +13 -0
- package/docs/api/type-aliases/RBACConfig.md +13 -0
- package/docs/api/type-aliases/RBACFunctionResponse.md +57 -0
- package/docs/api/type-aliases/Scope.md +41 -0
- package/docs/api/type-aliases/SessionType.md +11 -0
- package/docs/api/type-aliases/UUID.md +11 -0
- package/docs/api/type-aliases/UnifiedAuthContextType.md +13 -0
- package/docs/api/type-aliases/UseFileReferenceForRecordReturn.md +161 -0
- package/docs/api/type-aliases/UseFileReferenceOptions.md +35 -0
- package/docs/api/type-aliases/UseFileReferenceReturn.md +13 -0
- package/docs/api/variables/ALL_PERMISSIONS.md +281 -0
- package/docs/api/variables/APP_PATH_MAPPING.md +14 -0
- package/docs/api/variables/AddressField.md +41 -0
- package/docs/api/variables/Alert.md +11 -0
- package/docs/api/variables/AlertDescription.md +11 -0
- package/docs/api/variables/AlertTitle.md +11 -0
- package/docs/api/variables/Avatar.md +13 -0
- package/docs/api/variables/Button.md +31 -0
- package/docs/api/variables/CACHE_PATTERNS.md +89 -0
- package/docs/api/variables/Calendar.md +74 -0
- package/docs/api/variables/Card.md +11 -0
- package/docs/api/variables/CardActions.md +11 -0
- package/docs/api/variables/CardContent.md +11 -0
- package/docs/api/variables/CardDescription.md +11 -0
- package/docs/api/variables/CardFooter.md +11 -0
- package/docs/api/variables/CardHeader.md +11 -0
- package/docs/api/variables/CardTitle.md +11 -0
- package/docs/api/variables/Checkbox.md +11 -0
- package/docs/api/variables/DEFAULT_APP_PORT_MAP.md +14 -0
- package/docs/api/variables/DEFAULT_FILE_SIZE_LIMIT.md +13 -0
- package/docs/api/variables/Dialog.md +14 -0
- package/docs/api/variables/DialogClose.md +14 -0
- package/docs/api/variables/DialogContent.md +28 -0
- package/docs/api/variables/DialogDescription.md +14 -0
- package/docs/api/variables/DialogPortal.md +14 -0
- package/docs/api/variables/DialogTitle.md +14 -0
- package/docs/api/variables/DialogTrigger.md +14 -0
- package/docs/api/variables/EVENT_APP_PERMISSIONS.md +109 -0
- package/docs/api/variables/ErrorBoundary.md +15 -0
- package/docs/api/variables/FILE_SIZE_LIMITS.md +13 -0
- package/docs/api/variables/Footer.md +11 -0
- package/docs/api/variables/GLOBAL_PERMISSIONS.md +29 -0
- package/docs/api/variables/Label.md +34 -0
- package/docs/api/variables/LoadingSpinner.md +28 -0
- package/docs/api/variables/LoginForm.md +34 -0
- package/docs/api/variables/NavigationMenu.md +203 -0
- package/docs/api/variables/ORGANISATION_PERMISSIONS.md +89 -0
- package/docs/api/variables/PAGE_PERMISSIONS.md +93 -0
- package/docs/api/variables/PaceLoginPage.md +40 -0
- package/docs/api/variables/PagePermissionGuard.md +11 -0
- package/docs/api/variables/Progress.md +32 -0
- package/docs/api/variables/SECURE_CLIENT_SYMBOL.md +14 -0
- package/docs/api/variables/STORAGE_CONFIG.md +13 -0
- package/docs/api/variables/Select.md +26 -0
- package/docs/api/variables/SelectContent.md +26 -0
- package/docs/api/variables/SelectGroup.md +26 -0
- package/docs/api/variables/SelectItem.md +26 -0
- package/docs/api/variables/SelectLabel.md +26 -0
- package/docs/api/variables/SelectSeparator.md +26 -0
- package/docs/api/variables/SelectTrigger.md +26 -0
- package/docs/api/variables/SelectValue.md +26 -0
- package/docs/api/variables/SessionRestorationLoader.md +11 -0
- package/docs/api/variables/Switch.md +23 -0
- package/docs/api/variables/Table.md +35 -0
- package/docs/api/variables/TableBody.md +11 -0
- package/docs/api/variables/TableCaption.md +11 -0
- package/docs/api/variables/TableCell.md +11 -0
- package/docs/api/variables/TableFooter.md +11 -0
- package/docs/api/variables/TableHead.md +11 -0
- package/docs/api/variables/TableHeader.md +11 -0
- package/docs/api/variables/TableRow.md +11 -0
- package/docs/api/variables/Tabs.md +25 -0
- package/docs/api/variables/TabsContent.md +24 -0
- package/docs/api/variables/TabsList.md +25 -0
- package/docs/api/variables/TabsTrigger.md +34 -0
- package/docs/api/variables/Toast.md +36 -0
- package/docs/api/variables/ToastAction.md +32 -0
- package/docs/api/variables/ToastClose.md +32 -0
- package/docs/api/variables/ToastDescription.md +32 -0
- package/docs/api/variables/ToastProvider.md +11 -0
- package/docs/api/variables/ToastTitle.md +32 -0
- package/docs/api/variables/ToastViewport.md +26 -0
- package/docs/api/variables/Tooltip.md +34 -0
- package/docs/api/variables/TooltipContent.md +34 -0
- package/docs/api/variables/TooltipProvider.md +11 -0
- package/docs/api/variables/TooltipRoot.md +11 -0
- package/docs/api/variables/TooltipTrigger.md +11 -0
- package/docs/api/variables/UserMenu.md +11 -0
- package/docs/api/variables/emailSchema.md +13 -0
- package/docs/api/variables/logger.md +203 -0
- package/docs/api/variables/nameSchema.md +13 -0
- package/docs/api/variables/passwordSchema.md +13 -0
- package/docs/api/variables/phoneSchema.md +13 -0
- package/docs/api/variables/rbacCache.md +16 -0
- package/docs/api/variables/styleConfig.md +25 -0
- package/docs/api/variables/urlSchema.md +13 -0
- package/docs/api-reference/hooks.md +2 -0
- package/docs/api-reference/rpc-functions.md +12 -3
- package/docs/core-concepts/rbac-system.md +8 -0
- package/docs/getting-started/cursor-rules.md +17 -20
- package/docs/getting-started/dependencies.md +1 -1
- package/docs/getting-started/setup.md +235 -0
- package/docs/implementation-guides/authentication.md +27 -0
- package/docs/implementation-guides/data-tables.md +184 -3
- package/docs/migration/ApiResult-migration.md +25 -0
- package/docs/rbac/api-reference.md +33 -31
- package/docs/rbac/getting-started.md +7 -0
- package/docs/rbac/troubleshooting.md +5 -1
- package/docs/standards/0-standards-overview.md +50 -15
- package/docs/standards/1-pace-core-compliance-standards.md +62 -57
- package/docs/standards/2-project-structure-standards.md +33 -16
- package/docs/standards/3-architecture-standards.md +41 -1
- package/docs/standards/4-code-quality-standards.md +26 -6
- package/docs/standards/5-styling-standards.md +35 -1
- package/docs/standards/6-security-rbac-standards.md +66 -0
- package/docs/standards/7-api-tech-stack-standards.md +25 -14
- package/docs/standards/8-testing-documentation-standards.md +31 -0
- package/docs/standards/9-operations-standards.md +19 -0
- package/docs/standards/README.md +20 -201
- package/docs/testing/test-setup-for-consumers.md +2 -0
- package/docs/troubleshooting/common-issues.md +17 -1
- package/docs/troubleshooting/organisation-context-setup.md +8 -0
- package/docs/troubleshooting/print-event-name-css-variable-analysis.md +217 -0
- package/eslint-config-pace-core.cjs +20 -0
- package/package.json +16 -22
- package/scripts/build-docs.js +180 -0
- package/scripts/setup.cjs +536 -0
- package/scripts/validate.cjs +480 -0
- package/src/__tests__/helpers/component-test-utils.test.tsx +260 -0
- package/src/__tests__/helpers/optimized-test-setup.test.ts +224 -0
- package/src/__tests__/helpers/supabaseMock.test.ts +273 -0
- package/src/__tests__/helpers/test-providers.test.tsx +99 -0
- package/src/__tests__/helpers/test-providers.tsx +37 -39
- package/src/__tests__/helpers/test-utils.test.tsx +447 -0
- package/src/__tests__/helpers/timer-utils.test.ts +371 -0
- package/src/assets/app-icons/index.test.ts +304 -0
- package/src/components/AddressField/AddressField.test.tsx +1 -1
- package/src/components/AddressField/AddressField.tsx +238 -212
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Card/Card.test.tsx +172 -17
- package/src/components/Card/Card.tsx +19 -10
- package/src/components/ContextSelector/ContextSelector.internals.tsx +204 -0
- package/src/components/ContextSelector/ContextSelector.test.tsx +360 -0
- package/src/components/ContextSelector/ContextSelector.tsx +66 -280
- package/src/components/ContextSelector/ContextSelector.types.ts +35 -0
- package/src/components/ContextSelector/useContextSelectorState.tsx +195 -0
- package/src/components/DataTable/AUDIT_REPORT.md +59 -44
- package/src/components/DataTable/DataTable.comprehensive.test.tsx +759 -0
- package/src/components/DataTable/DataTable.default-state.test.tsx +524 -0
- package/src/components/DataTable/DataTable.export.test.tsx +705 -0
- package/src/components/DataTable/DataTable.grouping-aggregation.test.tsx +658 -0
- package/src/components/DataTable/DataTable.hooks.test.tsx +192 -0
- package/src/components/DataTable/DataTable.select-label-display.test.tsx +485 -0
- package/src/components/DataTable/DataTable.test.tsx +787 -416
- package/src/components/DataTable/DataTable.tsx +12 -12
- package/src/components/DataTable/DataTableCore.integration.test.tsx +458 -0
- package/src/components/DataTable/DataTableCore.test-setup.ts +221 -0
- package/src/components/DataTable/DataTableCore.test.tsx +970 -0
- package/src/components/DataTable/README.md +155 -0
- package/src/components/DataTable/TESTING.md +101 -0
- package/src/components/DataTable/a11y.basic.test.tsx +788 -0
- package/src/components/DataTable/components/DataTableCore.tsx +104 -864
- package/src/components/DataTable/components/GroupingDropdown.test.tsx +621 -0
- package/src/components/DataTable/components/GroupingDropdown.tsx +2 -2
- package/src/components/DataTable/components/ImportModal.tsx +61 -559
- package/src/components/DataTable/components/ImportModalFileSection.tsx +148 -0
- package/src/components/DataTable/context/DataTableContext.test.tsx +328 -0
- package/src/components/DataTable/context/DataTableContext.tsx +7 -6
- package/src/components/DataTable/core/ColumnFactory.test.ts +403 -0
- package/src/components/DataTable/hooks/useColumnOrderPersistence.test.ts +516 -0
- package/src/components/DataTable/hooks/useColumnVisibilityPersistence.test.ts +256 -0
- package/src/components/DataTable/hooks/useDataTableConfiguration.test.ts +297 -0
- package/src/components/DataTable/hooks/useDataTableConfiguration.ts +14 -2
- package/src/components/DataTable/hooks/useDataTableDataPipeline.test.ts +270 -0
- package/src/components/DataTable/hooks/useDataTableDeletionBatching.test.ts +127 -0
- package/src/components/DataTable/hooks/useDataTableDeletionBatching.ts +106 -0
- package/src/components/DataTable/hooks/useDataTableEffectiveActions.test.ts +461 -0
- package/src/components/DataTable/hooks/useDataTableEffectiveActions.ts +248 -0
- package/src/components/DataTable/hooks/useDataTableLayoutHandlers.test.ts +296 -0
- package/src/components/DataTable/hooks/useDataTableLayoutHandlers.ts +175 -0
- package/src/components/DataTable/hooks/useDataTablePaginationSync.test.ts +203 -0
- package/src/components/DataTable/hooks/useDataTablePaginationSync.ts +109 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.test.ts +280 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +79 -247
- package/src/components/DataTable/hooks/useDataTablePipeline.test.tsx +219 -0
- package/src/components/DataTable/hooks/useDataTablePipeline.tsx +239 -0
- package/src/components/DataTable/hooks/useDataTableRenderGuard.test.tsx +316 -0
- package/src/components/DataTable/hooks/useDataTableRenderGuard.tsx +195 -0
- package/src/components/DataTable/hooks/useDataTableScope.test.ts +102 -0
- package/src/components/DataTable/hooks/useDataTableScope.ts +125 -0
- package/src/components/DataTable/hooks/useDataTableState.test.ts +733 -0
- package/src/components/DataTable/hooks/useDataTableState.ts +145 -94
- package/src/components/DataTable/hooks/useDataTableStateAndPersistence.test.ts +277 -0
- package/src/components/DataTable/hooks/useDataTableStateAndPersistence.ts +222 -0
- package/src/components/DataTable/hooks/useDataTableSuperAdmin.test.ts +93 -0
- package/src/components/DataTable/hooks/useDataTableSuperAdmin.ts +86 -0
- package/src/components/DataTable/hooks/useDataTableTableInstance.test.ts +185 -0
- package/src/components/DataTable/hooks/useDataTableTableInstance.ts +178 -0
- package/src/components/DataTable/hooks/useEffectiveColumnOrder.test.ts +183 -0
- package/src/components/DataTable/hooks/useHierarchicalState.test.ts +294 -0
- package/src/components/DataTable/hooks/useImportModalFocus.test.ts +184 -0
- package/src/components/DataTable/hooks/useImportModalFocus.ts +53 -0
- package/src/components/DataTable/hooks/useImportModalState.test.ts +390 -0
- package/src/components/DataTable/hooks/useImportModalState.ts +345 -0
- package/src/components/DataTable/hooks/useKeyboardNavigation.test.ts +787 -0
- package/src/components/DataTable/hooks/useKeyboardNavigation.ts +309 -269
- package/src/components/DataTable/hooks/usePermissionTracking.test.ts +381 -0
- package/src/components/DataTable/hooks/usePermissionTracking.ts +122 -0
- package/src/components/DataTable/hooks/useServerSideDataEffect.test.ts +258 -0
- package/src/components/DataTable/hooks/useServerSideDataEffect.ts +14 -3
- package/src/components/DataTable/hooks/useTableColumns.test.ts +499 -0
- package/src/components/DataTable/hooks/useTableHandlers.test.ts +461 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +5 -2
- package/src/components/DataTable/index.ts +18 -17
- package/src/components/DataTable/keyboard.test.tsx +734 -0
- package/src/components/DataTable/mocks/MockRBACProvider.tsx +66 -0
- package/src/components/DataTable/pagination.modes.test.tsx +728 -0
- package/src/components/DataTable/ssr.strict-mode.test.tsx +319 -0
- package/src/components/DataTable/styles.test.ts +379 -0
- package/src/components/DataTable/styles.ts +0 -1
- package/src/components/DataTable/test-utils/MockDataTableComponents.tsx +55 -0
- package/src/components/DataTable/test-utils/dataFactories.ts +103 -0
- package/src/components/DataTable/test-utils/featureConfig.ts +10 -0
- package/src/components/DataTable/test-utils/sharedTestUtils.ts +419 -0
- package/src/components/DataTable/test-utils.ts +94 -0
- package/src/components/DataTable/types/actions.ts +71 -0
- package/src/components/DataTable/types/base.ts +39 -0
- package/src/components/DataTable/types/columns.ts +125 -0
- package/src/components/DataTable/types/export.ts +32 -0
- package/src/components/DataTable/types/features.ts +81 -0
- package/src/components/DataTable/types/hierarchical.ts +44 -0
- package/src/components/DataTable/types/index.ts +43 -0
- package/src/components/DataTable/types/pagination.ts +85 -0
- package/src/components/DataTable/types/performance.ts +47 -0
- package/src/components/DataTable/types/props.ts +62 -0
- package/src/components/DataTable/types/rbac.ts +45 -0
- package/src/components/DataTable/ui/layout/DataTableCore.test.tsx +1194 -0
- package/src/components/DataTable/ui/layout/DataTableCore.tsx +345 -0
- package/src/components/DataTable/ui/layout/DataTableErrorBoundary.test.tsx +438 -0
- package/src/components/DataTable/ui/layout/DataTableErrorBoundary.tsx +225 -0
- package/src/components/DataTable/ui/layout/DataTableLayout.test.tsx +1352 -0
- package/src/components/DataTable/ui/layout/DataTableLayout.tsx +661 -0
- package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.test.tsx +91 -0
- package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.tsx +43 -0
- package/src/components/DataTable/ui/modals/DataTableModals.test.tsx +749 -0
- package/src/components/DataTable/ui/modals/DataTableModals.tsx +341 -0
- package/src/components/DataTable/ui/modals/ImportModal.test.tsx +1834 -0
- package/src/components/DataTable/ui/modals/ImportModal.tsx +197 -0
- package/src/components/DataTable/ui/modals/ImportModalFailedRowsSection.tsx +60 -0
- package/src/components/DataTable/ui/modals/ImportModalFileSection.tsx +148 -0
- package/src/components/DataTable/ui/modals/ImportModalPreviewSection.tsx +60 -0
- package/src/components/DataTable/ui/modals/ImportModalSummarySection.tsx +59 -0
- package/src/components/DataTable/ui/modals/importModalPersistence.ts +73 -0
- package/src/components/DataTable/ui/shared/AccessDeniedPage.test.tsx +245 -0
- package/src/components/DataTable/ui/shared/AccessDeniedPage.tsx +159 -0
- package/src/components/DataTable/ui/shared/ActionButtons.test.tsx +921 -0
- package/src/components/DataTable/ui/shared/ActionButtons.tsx +195 -0
- package/src/components/DataTable/ui/shared/ColumnFilter.test.tsx +497 -0
- package/src/components/DataTable/ui/shared/ColumnFilter.tsx +113 -0
- package/src/components/DataTable/ui/shared/PaginationControls.test.tsx +451 -0
- package/src/components/DataTable/ui/shared/PaginationControls.tsx +291 -0
- package/src/components/DataTable/ui/shared/SortIndicator.test.tsx +135 -0
- package/src/components/DataTable/ui/shared/SortIndicator.tsx +50 -0
- package/src/components/DataTable/ui/table/EditFields.test.tsx +526 -0
- package/src/components/DataTable/ui/table/EditFields.tsx +355 -0
- package/src/components/DataTable/ui/table/EditableRow.test.tsx +1003 -0
- package/src/components/DataTable/ui/table/EditableRow.tsx +444 -0
- package/src/components/DataTable/ui/table/EmptyState.test.tsx +360 -0
- package/src/components/DataTable/ui/table/EmptyState.tsx +74 -0
- package/src/components/DataTable/ui/table/FilterRow.test.tsx +416 -0
- package/src/components/DataTable/ui/table/FilterRow.tsx +148 -0
- package/src/components/DataTable/ui/table/LoadingState.test.tsx +77 -0
- package/src/components/DataTable/ui/table/LoadingState.tsx +17 -0
- package/src/components/DataTable/ui/table/RowComponent.test.tsx +1024 -0
- package/src/components/DataTable/ui/table/RowComponent.tsx +429 -0
- package/src/components/DataTable/ui/table/UnifiedTableBody.test.tsx +1273 -0
- package/src/components/DataTable/ui/table/UnifiedTableBody.tsx +440 -0
- package/src/components/DataTable/ui/table/cellValueUtils.test.ts +453 -0
- package/src/components/DataTable/ui/table/cellValueUtils.ts +40 -0
- package/src/components/DataTable/ui/toolbar/BulkOperationsDropdown.test.tsx +551 -0
- package/src/components/DataTable/ui/toolbar/BulkOperationsDropdown.tsx +160 -0
- package/src/components/DataTable/ui/toolbar/ColumnVisibilityDropdown.test.tsx +751 -0
- package/src/components/DataTable/ui/toolbar/ColumnVisibilityDropdown.tsx +114 -0
- package/src/components/DataTable/ui/toolbar/DataTableToolbar.test.tsx +629 -0
- package/src/components/DataTable/ui/toolbar/DataTableToolbar.tsx +271 -0
- package/src/components/DataTable/ui/toolbar/GroupingDropdown.test.tsx +621 -0
- package/src/components/DataTable/ui/toolbar/GroupingDropdown.tsx +107 -0
- package/src/components/DataTable/utils/a11yUtils.test.ts +548 -0
- package/src/components/DataTable/utils/aggregationUtils.test.ts +288 -0
- package/src/components/DataTable/utils/columnUtils.test.ts +94 -0
- package/src/components/DataTable/utils/csvParse.test.ts +74 -0
- package/src/components/DataTable/utils/csvParse.ts +65 -0
- package/src/components/DataTable/utils/errorHandling.test.ts +209 -0
- package/src/components/DataTable/utils/exportUtils.test.ts +954 -0
- package/src/components/DataTable/utils/flexibleImport.test.ts +573 -0
- package/src/components/DataTable/utils/flexibleImport.ts +3 -186
- package/src/components/DataTable/utils/hierarchicalSorting.test.ts +235 -0
- package/src/components/DataTable/utils/hierarchicalUtils.test.ts +586 -0
- package/src/components/DataTable/utils/importDateParser.test.ts +162 -0
- package/src/components/DataTable/utils/importDateParser.ts +114 -0
- package/src/components/DataTable/utils/importValueParser.test.ts +138 -0
- package/src/components/DataTable/utils/importValueParser.ts +91 -0
- package/src/components/DataTable/utils/paginationUtils.test.ts +593 -0
- package/src/components/DataTable/utils/paginationUtils.ts +6 -3
- package/src/components/DataTable/utils/performanceUtils.test.ts +470 -0
- package/src/components/DataTable/utils/rowUtils.test.ts +235 -0
- package/src/components/DataTable/utils/selectFieldUtils.test.ts +271 -0
- package/src/components/DataTable/utils/selectFieldUtils.ts +97 -60
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -1
- package/src/components/DateTimeField/DateTimeField.test.tsx +1 -1
- package/src/components/Dialog/Dialog.test-utils.ts +49 -0
- package/src/components/Dialog/Dialog.test.tsx +896 -89
- package/src/components/Dialog/Dialog.tsx +174 -882
- package/src/components/Dialog/dialogLock.test.ts +238 -0
- package/src/components/Dialog/dialogLock.ts +98 -0
- package/src/components/Dialog/index.ts +2 -0
- package/src/components/Dialog/useDialogDimensions.test.ts +163 -0
- package/src/components/Dialog/useDialogDimensions.ts +140 -0
- package/src/components/Dialog/useDialogLifecycle.test.ts +361 -0
- package/src/components/Dialog/useDialogLifecycle.ts +135 -0
- package/src/components/Dialog/useDialogPersistence.test.ts +381 -0
- package/src/components/Dialog/useDialogPersistence.ts +357 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +40 -40
- package/src/components/FileDisplay/FileDisplay.tsx +24 -656
- package/src/components/FileDisplay/FileDisplayContent.test.tsx +395 -0
- package/src/components/FileDisplay/FileDisplayContent.tsx +242 -0
- package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.test.tsx +74 -0
- package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.tsx +38 -0
- package/src/components/FileDisplay/FileDisplayEmptyView.test.tsx +33 -0
- package/src/components/FileDisplay/FileDisplayEmptyView.tsx +33 -0
- package/src/components/FileDisplay/FileDisplayErrorView.test.tsx +71 -0
- package/src/components/FileDisplay/FileDisplayErrorView.tsx +50 -0
- package/src/components/FileDisplay/FileDisplayLoadingFallbackView.test.tsx +22 -0
- package/src/components/FileDisplay/FileDisplayLoadingFallbackView.tsx +22 -0
- package/src/components/FileDisplay/FileDisplayLoadingView.test.tsx +21 -0
- package/src/components/FileDisplay/FileDisplayLoadingView.tsx +23 -0
- package/src/components/FileDisplay/FileDisplayMultipleFilesView.test.tsx +101 -0
- package/src/components/FileDisplay/FileDisplayMultipleFilesView.tsx +109 -0
- package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.test.tsx +58 -0
- package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.tsx +48 -0
- package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.test.tsx +111 -0
- package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.tsx +270 -0
- package/src/components/FileDisplay/FileDisplaySingleImageView.test.tsx +78 -0
- package/src/components/FileDisplay/FileDisplaySingleImageView.tsx +67 -0
- package/src/components/FileDisplay/fallbackUtils.test.ts +50 -0
- package/src/components/FileDisplay/fallbackUtils.ts +44 -0
- package/src/components/FileDisplay/fetchFileDisplayData.ts +24 -0
- package/src/components/FileDisplay/fetchFileDisplayData.unit.test.ts +183 -0
- package/src/components/FileDisplay/fileDisplayUtils.test.ts +58 -0
- package/src/components/FileDisplay/fileDisplayUtils.ts +24 -0
- package/src/components/FileDisplay/useFileDisplay.test.ts +538 -0
- package/src/components/FileDisplay/useFileDisplay.ts +515 -0
- package/src/components/FileDisplay/useFileDisplay.unit.test.ts +1442 -0
- package/src/components/FileDisplay/useFileDisplayData.ts +126 -0
- package/src/components/FileDisplay/usePublicFileDisplay.test.ts +729 -0
- package/src/components/FileDisplay/usePublicFileDisplay.ts +579 -0
- package/src/components/FileUpload/FileUpload.test.tsx +16 -10
- package/src/components/FileUpload/FileUpload.tsx +107 -525
- package/src/components/FileUpload/FileUploadDropZone.tsx +112 -0
- package/src/components/FileUpload/FileUploadProgressItem.tsx +86 -0
- package/src/components/FileUpload/FileUploadProgressList.tsx +40 -0
- package/src/components/FileUpload/useFileUploadManager.test.ts +308 -0
- package/src/components/FileUpload/useFileUploadManager.ts +454 -0
- package/src/components/FileUpload/useResolvedAppId.test.ts +102 -0
- package/src/components/FileUpload/useResolvedAppId.ts +77 -0
- package/src/components/Footer/Footer.test.tsx +6 -292
- package/src/components/Footer/Footer.tsx +8 -125
- package/src/components/Form/Form.test.tsx +44 -27
- package/src/components/Form/Form.tsx +64 -287
- package/src/components/Form/useFormPersistence.ts +257 -0
- package/src/components/Header/Header.test.tsx +17 -18
- package/src/components/Header/Header.tsx +10 -1
- package/src/components/Input/Input.tsx +1 -1
- package/src/components/Label/Label.test.tsx +1 -1
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +1 -1
- package/src/components/NavigationMenu/HierarchicalNavItem.tsx +104 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +1029 -26
- package/src/components/NavigationMenu/NavigationMenu.tsx +61 -361
- package/src/components/NavigationMenu/index.ts +6 -1
- package/src/components/NavigationMenu/navigationPermissionHelper.ts +188 -0
- package/src/components/NavigationMenu/useNavigationFiltering.test.ts +1949 -0
- package/src/components/NavigationMenu/useNavigationFiltering.ts +197 -296
- package/src/components/NavigationMenu/useNavigationScope.ts +125 -0
- package/src/components/PaceAppLayout/PaceAppLayout.edge-cases.test.tsx +109 -70
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +3 -3
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +16 -19
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +529 -5
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +280 -756
- package/src/components/PaceAppLayout/useFilteredNavItems.ts +319 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutConfig.ts +163 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutGate.tsx +150 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutPermissions.ts +162 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutScope.ts +81 -0
- package/src/components/PaceAppLayout/useRoleBasedRouteAccess.ts +157 -0
- package/src/components/PaceAppLayout/useSuperAdminFallback.ts +58 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +31 -25
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +31 -122
- package/src/components/PaceLoginPage/useLoginAppAccess.ts +153 -0
- package/src/components/Progress/Progress.tsx +1 -2
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +29 -235
- package/src/components/ProtectedRoute/useProtectedRouteState.ts +128 -0
- package/src/components/ProtectedRoute/useVisibilityRedirectGrace.ts +89 -0
- package/src/components/PublicLayout/PublicLayout.test.tsx +217 -36
- package/src/components/PublicLayout/PublicPageLayout.tsx +132 -73
- package/src/components/PublicLayout/PublicPageProvider.tsx +5 -1
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/Select/Select.tsx +28 -18
- package/src/components/Select/context.test.tsx +56 -0
- package/src/components/Select/text.test.tsx +104 -0
- package/src/components/Select/text.ts +26 -0
- package/src/components/Select/useSelectEvents.test.ts +279 -0
- package/src/components/Select/useSelectEvents.ts +87 -0
- package/src/components/Select/useSelectSearch.test.tsx +295 -0
- package/src/components/Select/useSelectSearch.ts +91 -0
- package/src/components/Select/useSelectState.test.ts +268 -0
- package/src/components/Select/useSelectState.ts +104 -0
- package/src/components/Table/Table.test.tsx +348 -0
- package/src/components/Tabs/Tabs.test.tsx +270 -0
- package/src/components/Tabs/Tabs.tsx +1 -1
- package/src/components/Toast/Toast.test.tsx +420 -0
- package/src/components/index.test.ts +346 -0
- package/src/constants/performance.test.ts +91 -0
- package/src/hooks/ServiceHooks.test.tsx +725 -0
- package/src/hooks/hooks.integration.test.tsx +608 -0
- package/src/hooks/index.ts +7 -4
- package/src/hooks/index.unit.test.ts +220 -0
- package/src/hooks/public/usePublicEvent.test.ts +1 -1
- package/src/hooks/public/usePublicEventLogo.test.ts +1 -1
- package/src/hooks/public/usePublicRouteParams.test.ts +1 -1
- package/src/hooks/services/useAuth.ts +9 -7
- package/src/hooks/useAddressAutocomplete.test.ts +22 -22
- package/src/hooks/useAddressAutocomplete.ts +90 -75
- package/src/hooks/useAppConfig.unit.test.ts +720 -0
- package/src/hooks/useComponentPerformance.unit.test.tsx +316 -0
- package/src/hooks/useDataTablePerformance.ts +100 -120
- package/src/hooks/useDataTablePerformance.unit.test.ts +720 -0
- package/src/hooks/useDataTableState.test.ts +170 -0
- package/src/hooks/useDebounce.unit.test.ts +157 -0
- package/src/hooks/useEventTheme.test.ts +4 -1
- package/src/hooks/useEventTheme.ts +49 -21
- package/src/hooks/useEvents.ts +41 -1
- package/src/hooks/useEvents.unit.test.ts +227 -0
- package/src/hooks/useFileReference.test.ts +44 -41
- package/src/hooks/useFileReference.ts +182 -173
- package/src/hooks/useFileUrl.ts +1 -1
- package/src/hooks/useFileUrl.unit.test.ts +684 -0
- package/src/hooks/useFileUrlCache.test.ts +319 -0
- package/src/hooks/useFileUrlCache.ts +1 -1
- package/src/hooks/useFocusManagement.unit.test.ts +604 -0
- package/src/hooks/useFocusTrap.unit.test.tsx +613 -0
- package/src/hooks/useFormDialog.test.ts +307 -0
- package/src/hooks/useInactivityTracker.ts +138 -131
- package/src/hooks/useInactivityTracker.unit.test.ts +450 -0
- package/src/hooks/useIsMobile.unit.test.ts +317 -0
- package/src/hooks/useIsPrint.ts +62 -0
- package/src/hooks/useIsPrint.unit.test.ts +545 -0
- package/src/hooks/useKeyboardShortcuts.unit.test.ts +907 -0
- package/src/hooks/useOrganisationPermissions.unit.test.tsx +293 -0
- package/src/hooks/useOrganisationSecurity.test.ts +3 -3
- package/src/hooks/useOrganisationSecurity.ts +190 -201
- package/src/hooks/useOrganisationSecurity.unit.test.tsx +959 -0
- package/src/hooks/useOrganisations.unit.test.ts +369 -0
- package/src/hooks/usePerformanceMonitor.unit.test.ts +683 -0
- package/src/hooks/usePermissionCache.test.ts +505 -0
- package/src/hooks/usePermissionCache.ts +276 -271
- package/src/hooks/usePreventTabReload.test.ts +307 -0
- package/src/hooks/usePublicEvent.simple.test.ts +779 -0
- package/src/hooks/usePublicEvent.test.ts +664 -0
- package/src/hooks/usePublicEvent.unit.test.ts +638 -0
- package/src/hooks/usePublicFileDisplay.test.ts +948 -0
- package/src/hooks/usePublicRouteParams.unit.test.ts +442 -0
- package/src/hooks/useQueryCache.test.ts +391 -0
- package/src/hooks/useQueryCache.ts +0 -2
- package/src/hooks/useRBAC.unit.test.ts +253 -0
- package/src/hooks/useSessionDraft.test.ts +556 -0
- package/src/hooks/useSessionRestoration.unit.test.tsx +381 -0
- package/src/hooks/useStorage.ts +21 -16
- package/src/hooks/useStorage.unit.test.ts +684 -0
- package/src/hooks/useToast.test.ts +413 -0
- package/src/hooks/useToast.unit.test.tsx +481 -0
- package/src/hooks/useZodForm.unit.test.tsx +191 -0
- package/src/icons/index.test.ts +133 -0
- package/src/icons/index.ts +2 -0
- package/src/index.test.ts +528 -0
- package/src/index.ts +15 -7
- package/src/providers/AuthProvider.test.tsx +218 -0
- package/src/providers/EventProvider.test.tsx +487 -0
- package/src/providers/InactivityProvider.test-helper.tsx +40 -0
- package/src/providers/InactivityProvider.test.tsx +421 -0
- package/src/providers/ProviderLifecycle.test.tsx +308 -0
- package/src/providers/UnifiedAuthProvider.test.tsx +503 -0
- package/src/providers/index.test.ts +138 -0
- package/src/providers/services/AuthServiceProvider.integration.test.tsx +229 -0
- package/src/providers/services/AuthServiceProvider.test.tsx +643 -0
- package/src/providers/services/EventServiceProvider.test.tsx +844 -0
- package/src/providers/services/InactivityServiceProvider.test.tsx +667 -0
- package/src/providers/services/OrganisationServiceProvider.test.tsx +445 -0
- package/src/providers/services/UnifiedAuthContext.ts +30 -27
- package/src/providers/services/UnifiedAuthProvider.advanced.test.tsx +434 -0
- package/src/providers/services/UnifiedAuthProvider.appId.test.tsx +408 -0
- package/src/providers/services/UnifiedAuthProvider.integration.test.tsx +304 -0
- package/src/providers/services/UnifiedAuthProvider.tsx +115 -360
- package/src/providers/services/contexts.test.tsx +281 -0
- package/src/providers/services/useUnifiedAuth.test.tsx +251 -0
- package/src/providers/services/useUnifiedAuthContextValue.ts +279 -0
- package/src/providers/useInactivity.test-helper.ts +27 -0
- package/src/rbac/README.md +7 -5
- package/src/rbac/adapters.comprehensive.test.tsx +429 -0
- package/src/rbac/adapters.test.tsx +22 -22
- package/src/rbac/adapters.tsx +29 -29
- package/src/rbac/api.test.ts +1075 -87
- package/src/rbac/api.ts +300 -255
- package/src/rbac/audit-batched.test.ts +550 -0
- package/src/rbac/audit.ts +4 -1
- package/src/rbac/auth-rbac-security.integration.test.tsx +300 -0
- package/src/rbac/auth-rbac.e2e.test.tsx +510 -0
- package/src/rbac/cache-invalidation.test.ts +715 -0
- package/src/rbac/components/AccessDenied.test.tsx +324 -0
- package/src/rbac/components/NavigationGuard.test.tsx +1148 -0
- package/src/rbac/components/NavigationGuard.tsx +2 -1
- package/src/rbac/components/PagePermissionGuard.guard.test.tsx +236 -0
- package/src/rbac/components/PagePermissionGuard.performance.test.tsx +252 -0
- package/src/rbac/components/PagePermissionGuard.race-condition.test.tsx +243 -0
- package/src/rbac/components/PagePermissionGuard.test.tsx +1443 -0
- package/src/rbac/components/PagePermissionGuard.tsx +177 -372
- package/src/rbac/components/PagePermissionGuard.verification.test.tsx +185 -0
- package/src/rbac/config.ts +58 -18
- package/src/rbac/engine.comprehensive.test.ts +808 -0
- package/src/rbac/engine.test.ts +494 -0
- package/src/rbac/engine.ts +23 -1
- package/src/rbac/errors.ts +89 -55
- package/src/rbac/hooks/permissions/runPermissionCheck.ts +91 -0
- package/src/rbac/hooks/permissions/useAccessLevel.test.ts +622 -0
- package/src/rbac/hooks/permissions/useAccessLevel.ts +16 -6
- package/src/rbac/hooks/permissions/useCan.test.ts +837 -0
- package/src/rbac/hooks/permissions/useCan.ts +177 -255
- package/src/rbac/hooks/permissions/useMultiplePermissions.test.ts +843 -0
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +24 -11
- package/src/rbac/hooks/permissions/usePermissions.test.ts +543 -0
- package/src/rbac/hooks/permissions/usePermissions.ts +36 -65
- package/src/rbac/hooks/useCan.test.ts +44 -45
- package/src/rbac/hooks/usePageAccessLogging.ts +160 -0
- package/src/rbac/hooks/usePageGuardScope.ts +119 -0
- package/src/rbac/hooks/usePagePermissionCheck.ts +67 -0
- package/src/rbac/hooks/usePermissions.integration.test.ts +427 -0
- package/src/rbac/hooks/usePermissions.stability.test.ts +268 -0
- package/src/rbac/hooks/usePermissions.test.ts +54 -54
- package/src/rbac/hooks/useRBAC.test.ts +313 -217
- package/src/rbac/hooks/useRBAC.ts +145 -81
- package/src/rbac/hooks/useResolvedScope.ts +16 -10
- package/src/rbac/hooks/useResourcePermissions.test.ts +73 -83
- package/src/rbac/hooks/useResourcePermissions.ts +77 -152
- package/src/rbac/hooks/useResourcePermissionsSuperAdmin.ts +67 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +27 -112
- package/src/rbac/hooks/useRoleManagement.ts +153 -585
- package/src/rbac/hooks/useSecureSupabase.test.ts +1179 -0
- package/src/rbac/hooks/useSecureSupabase.ts +10 -2
- package/src/rbac/hooks/useSuperAdminCheck.ts +80 -0
- package/src/rbac/performance.test.ts +451 -0
- package/src/rbac/rbac-core.test.tsx +276 -0
- package/src/rbac/rbac-engine-core-logic.test.ts +387 -0
- package/src/rbac/rbac-engine-simplified.test.ts +252 -0
- package/src/rbac/rbac-functions.test.ts +703 -0
- package/src/rbac/rbac-role-isolation.test.ts +456 -0
- package/src/rbac/request-deduplication.test.ts +14 -9
- package/src/rbac/request-deduplication.ts +5 -4
- package/src/rbac/scenarios.user-role.test.tsx +271 -0
- package/src/rbac/secureClient.test.ts +514 -83
- package/src/rbac/secureClient.ts +8 -2
- package/src/rbac/security.test.ts +323 -0
- package/src/rbac/types/roleManagement.ts +66 -0
- package/src/rbac/types.ts +3 -0
- package/src/rbac/utils/clientSecurity.test.ts +192 -0
- package/src/rbac/utils/contextValidator.test.ts +126 -0
- package/src/rbac/utils/contextValidator.ts +5 -1
- package/src/rbac/utils/deep-equal.test.ts +76 -0
- package/src/rbac/utils/eventContext.test.ts +401 -0
- package/src/rbac/utils/eventContext.ts +37 -33
- package/src/rbac/utils/fetchPermissionMap.ts +13 -0
- package/src/rbac/utils/permissionMapHelpers.ts +34 -0
- package/src/rbac/utils/roleManagementRpc.ts +303 -0
- package/src/services/AuthService.edge-cases.test.ts +746 -0
- package/src/services/AuthService.restoreSession.test.ts +59 -0
- package/src/services/AuthService.test.ts +1362 -0
- package/src/services/AuthService.ts +184 -205
- package/src/services/BaseService.edge-cases.test.ts +506 -0
- package/src/services/BaseService.test.ts +363 -0
- package/src/services/EventService.edge-cases.test.ts +636 -0
- package/src/services/EventService.eventColours.test.ts +64 -0
- package/src/services/EventService.test.ts +1250 -0
- package/src/services/EventService.ts +251 -316
- package/src/services/InactivityService.edge-cases.test.ts +492 -0
- package/src/services/InactivityService.lifecycle.test.ts +406 -0
- package/src/services/InactivityService.test.ts +829 -0
- package/src/services/InactivityService.ts +172 -213
- package/src/services/OrganisationService.edge-cases.test.ts +633 -0
- package/src/services/OrganisationService.pagination.test.ts +409 -0
- package/src/services/OrganisationService.test.ts +1579 -0
- package/src/services/OrganisationService.ts +184 -238
- package/src/services/base/BaseService.test.ts +1 -1
- package/src/services/interfaces/IAuthService.test.ts +184 -0
- package/src/services/interfaces/IAuthService.ts +10 -9
- package/src/services/interfaces/IEventService.test.ts +176 -0
- package/src/services/interfaces/IInactivityService.test.ts +183 -0
- package/src/services/interfaces/IOrganisationService.test.ts +207 -0
- package/src/styles/core.css +243 -12
- package/src/theming/parseEventColours.test.ts +321 -0
- package/src/theming/runtime.test.ts +495 -0
- package/src/theming/runtime.ts +71 -2
- package/src/types/api-result.ts +53 -0
- package/src/types/core.test.ts +397 -0
- package/src/types/database-generated.test.ts +78 -0
- package/src/types/database.generated.ts +45 -10
- package/src/types/event.ts +38 -18
- package/src/types/file-reference.test.ts +351 -0
- package/src/types/file-reference.ts +37 -12
- package/src/types/guards.test.ts +246 -0
- package/src/types/index.test.ts +265 -0
- package/src/types/index.ts +3 -0
- package/src/types/organisation.roles.test.ts +55 -0
- package/src/types/organisation.test.ts +1105 -0
- package/src/types/organisation.ts +15 -15
- package/src/types/supabase.ts +13 -4
- package/src/types/theme.test.ts +830 -0
- package/src/types/type-validation.test.ts +526 -0
- package/src/types/validation.test.ts +729 -0
- package/src/utils/app/appIdResolver.test.ts +98 -71
- package/src/utils/app/appIdResolver.ts +31 -20
- package/src/utils/appConfig.unit.test.ts +55 -0
- package/src/utils/audit.unit.test.ts +69 -0
- package/src/utils/auth-utils.unit.test.ts +69 -0
- package/src/utils/bundleAnalysis.unit.test.ts +326 -0
- package/src/utils/cn.unit.test.ts +34 -0
- package/src/utils/context/organisationContext.test.ts +105 -91
- package/src/utils/context/organisationContext.ts +29 -40
- package/src/utils/core/cn.test.ts +66 -0
- package/src/utils/core/debugLogger.test.ts +113 -0
- package/src/utils/core/logger.test.ts +217 -0
- package/src/utils/core/mergeRefs.ts +24 -0
- package/src/utils/debugLogger.test.ts +417 -0
- package/src/utils/deviceFingerprint.unit.test.ts +818 -0
- package/src/utils/dynamic/createLazyComponent.tsx +9 -1
- package/src/utils/dynamic/dynamicUtils.test.ts +185 -0
- package/src/utils/dynamic/lazyLoad.test.tsx +156 -0
- package/src/utils/dynamicUtils.unit.test.ts +331 -0
- package/src/utils/file-reference/file-reference.test.ts +1249 -0
- package/src/utils/file-reference/index.ts +330 -347
- package/src/utils/formatDate.unit.test.ts +109 -0
- package/src/utils/formatting/formatDateTimeTimezone.test.ts +1 -1
- package/src/utils/formatting/formatNumber.test.ts +1 -1
- package/src/utils/formatting.unit.test.ts +99 -0
- package/src/utils/google-places/googlePlacesUtils.test.ts +70 -48
- package/src/utils/google-places/googlePlacesUtils.ts +67 -99
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +25 -22
- package/src/utils/google-places/loadGoogleMapsScript.ts +138 -117
- package/src/utils/index.unit.test.ts +251 -0
- package/src/utils/lazyLoad.unit.test.tsx +319 -0
- package/src/utils/location/location.test.ts +1 -1
- package/src/utils/logger.unit.test.ts +398 -0
- package/src/utils/organisationContext.unit.test.ts +180 -0
- package/src/utils/performance/bundleAnalysis.test.ts +148 -0
- package/src/utils/performance/performanceBenchmark.test.ts +251 -0
- package/src/utils/performance/performanceBudgets.test.ts +241 -0
- package/src/utils/performanceBenchmark.test.ts +174 -0
- package/src/utils/performanceBudgets.unit.test.ts +288 -0
- package/src/utils/permissionTypes.unit.test.ts +250 -0
- package/src/utils/permissionUtils.unit.test.ts +362 -0
- package/src/utils/permissions/permissionTypes.test.ts +149 -0
- package/src/utils/persistence/keyDerivation.test.ts +306 -0
- package/src/utils/persistence/sensitiveFieldDetection.test.ts +271 -0
- package/src/utils/request-deduplication.test.ts +349 -0
- package/src/utils/sanitization.unit.test.ts +346 -0
- package/src/utils/schemaUtils.unit.test.ts +441 -0
- package/src/utils/secureDataAccess.unit.test.ts +334 -0
- package/src/utils/secureErrors.unit.test.ts +390 -0
- package/src/utils/secureStorage.unit.test.ts +289 -0
- package/src/utils/security/auth-utils.ts +34 -23
- package/src/utils/security/secureDataAccess.ts +241 -281
- package/src/utils/security/secureErrors.test.ts +1 -1
- package/src/utils/security/secureStorage.test.ts +1 -1
- package/src/utils/security/security.test.ts +25 -17
- package/src/utils/security/security.ts +15 -18
- package/src/utils/security/securityMonitor.test.ts +1 -1
- package/src/utils/security.unit.test.ts +155 -0
- package/src/utils/securityMonitor.unit.test.ts +276 -0
- package/src/utils/sessionTracking.unit.test.ts +218 -0
- package/src/utils/storage/config.unit.test.ts +239 -0
- package/src/utils/storage/helpers.test.ts +88 -102
- package/src/utils/storage/helpers.ts +173 -251
- package/src/utils/storage/index.unit.test.ts +68 -0
- package/src/utils/storage/types.ts +7 -0
- package/src/utils/supabase/createBaseClient.test.ts +31 -14
- package/src/utils/timezone/timezone.test.ts +1 -1
- package/src/utils/timezone.test.ts +345 -0
- package/src/utils/validation/common.test.ts +115 -0
- package/src/utils/validation/csrf.test.ts +198 -0
- package/src/utils/validation/csrf.ts +42 -41
- package/src/utils/validation/htmlSanitization.unit.test.ts +618 -0
- package/src/utils/validation/passwordSchema.test.ts +164 -0
- package/src/utils/validation/schema.test.ts +127 -0
- package/src/utils/validation/sqlInjectionProtection.test.ts +165 -0
- package/src/utils/validation/user.test.ts +173 -0
- package/src/utils/validation/validation.test.ts +197 -0
- package/src/utils/validation/validationUtils.test.ts +294 -0
- package/src/utils/validation.unit.test.ts +307 -0
- package/src/utils/validationUtils.unit.test.ts +558 -0
- package/dist/DataTable-SAXFG4XI.js +0 -13
- package/dist/InactivityServiceProvider-DHryoh6K.d.ts +0 -299
- package/dist/UnifiedAuthProvider-BBD2PS3Q.js +0 -7
- package/dist/UnifiedAuthProvider-CiBAl9-s.d.ts +0 -151
- package/dist/api-F47QJ7FX.js +0 -4
- package/dist/audit-Z6ZZBWLU.js +0 -3
- package/dist/auth-BZOJqrdd.d.ts +0 -49
- package/dist/chunk-3GWSPISD.js +0 -61
- package/dist/chunk-66R6RLUZ.js +0 -529
- package/dist/chunk-7YDC7LMU.js +0 -487
- package/dist/chunk-BCTXBU6U.js +0 -704
- package/dist/chunk-FBZ7U3ID.js +0 -2209
- package/dist/chunk-FN52B75D.js +0 -246
- package/dist/chunk-JJEYZ3DX.js +0 -165
- package/dist/chunk-KPYQWGFQ.js +0 -183
- package/dist/chunk-KSNLMI7N.js +0 -481
- package/dist/chunk-KYURMOQM.js +0 -977
- package/dist/chunk-LNHFAF4X.js +0 -2279
- package/dist/chunk-MPY44PWB.js +0 -8510
- package/dist/chunk-NIU6DPQV.js +0 -427
- package/dist/chunk-TFIPNIPE.js +0 -5379
- package/dist/chunk-UZNAFKGW.js +0 -125
- package/dist/chunk-W46INAVW.js +0 -1216
- package/dist/chunk-X5EAU5G7.js +0 -793
- package/dist/chunk-Y4PF6HIM.js +0 -2862
- package/dist/database.generated-DT8JTZiP.d.ts +0 -9406
- package/dist/event-WTAQuGcq.d.ts +0 -239
- package/dist/file-reference-BavO2eQj.d.ts +0 -148
- package/dist/functions-DH45k8ec.d.ts +0 -208
- package/dist/timezone-K-ptz3HO.d.ts +0 -696
- package/dist/types-BE2sEHKd.d.ts +0 -55
- package/dist/types-CvOPXWWZ.d.ts +0 -111
- package/dist/types-D05dCGma.d.ts +0 -521
- package/dist/usePublicPageContext-vxBlEHO9.d.ts +0 -4290
- package/dist/usePublicRouteParams-G3Ks53mk.d.ts +0 -877
- package/docs/api/modules.md +0 -9889
- package/scripts/build-docs-incremental.js +0 -179
- package/scripts/eslint-audit.cjs +0 -222
- package/scripts/generate-docs.js +0 -157
- package/scripts/install-cursor-rules.cjs +0 -255
- package/scripts/install-eslint-config.cjs +0 -349
- package/scripts/setup-build-cache.js +0 -73
- package/scripts/validate-pre-publish.js +0 -145
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +0 -260
- package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +0 -224
- package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +0 -273
- package/src/__tests__/helpers/__tests__/test-providers.test.tsx +0 -99
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +0 -446
- package/src/__tests__/helpers/__tests__/timer-utils.test.ts +0 -371
- package/src/__tests__/hooks/usePermissions.test.ts +0 -268
- package/src/__tests__/index.test.ts +0 -532
- package/src/__tests__/integration/UserProfile.test.tsx +0 -124
- package/src/__tests__/public-recipe-view.test.ts +0 -228
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +0 -222
- package/src/__tests__/rls-policies.test.ts +0 -472
- package/src/components/ContextSelector/__tests__/ContextSelector.test.tsx +0 -360
- package/src/components/DataTable/__tests__/DataTable.comprehensive.test.tsx +0 -759
- package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +0 -524
- package/src/components/DataTable/__tests__/DataTable.export.test.tsx +0 -705
- package/src/components/DataTable/__tests__/DataTable.grouping-aggregation.test.tsx +0 -658
- package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +0 -192
- package/src/components/DataTable/__tests__/DataTable.select-label-display.test.tsx +0 -485
- package/src/components/DataTable/__tests__/DataTable.test.tsx +0 -876
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +0 -220
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +0 -970
- package/src/components/DataTable/__tests__/README.md +0 -145
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +0 -788
- package/src/components/DataTable/__tests__/keyboard.test.tsx +0 -756
- package/src/components/DataTable/__tests__/mocks/MockRBACProvider.tsx +0 -66
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +0 -728
- package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx +0 -319
- package/src/components/DataTable/__tests__/styles.test.ts +0 -382
- package/src/components/DataTable/__tests__/test-utils/dataFactories.ts +0 -103
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +0 -388
- package/src/components/DataTable/__tests__/test-utils.ts +0 -94
- package/src/components/DataTable/components/AccessDeniedPage.tsx +0 -159
- package/src/components/DataTable/components/ActionButtons.tsx +0 -195
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +0 -160
- package/src/components/DataTable/components/ColumnFilter.tsx +0 -113
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +0 -114
- package/src/components/DataTable/components/DataTableErrorBoundary.tsx +0 -225
- package/src/components/DataTable/components/DataTableLayout.tsx +0 -584
- package/src/components/DataTable/components/DataTableModals.tsx +0 -333
- package/src/components/DataTable/components/DataTableToolbar.tsx +0 -271
- package/src/components/DataTable/components/EditFields.tsx +0 -286
- package/src/components/DataTable/components/EditableRow.tsx +0 -462
- package/src/components/DataTable/components/EmptyState.tsx +0 -82
- package/src/components/DataTable/components/FilterRow.tsx +0 -148
- package/src/components/DataTable/components/LoadingState.tsx +0 -17
- package/src/components/DataTable/components/PaginationControls.tsx +0 -285
- package/src/components/DataTable/components/RowComponent.tsx +0 -423
- package/src/components/DataTable/components/SortIndicator.tsx +0 -50
- package/src/components/DataTable/components/UnifiedTableBody.tsx +0 -395
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +0 -245
- package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +0 -919
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +0 -544
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +0 -484
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +0 -748
- package/src/components/DataTable/components/__tests__/DataTableCore.test.tsx +0 -792
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +0 -438
- package/src/components/DataTable/components/__tests__/DataTableLayout.test.tsx +0 -467
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +0 -358
- package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +0 -629
- package/src/components/DataTable/components/__tests__/EditFields.test.tsx +0 -526
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +0 -994
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +0 -420
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +0 -415
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +0 -612
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +0 -957
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +0 -81
- package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +0 -429
- package/src/components/DataTable/components/__tests__/RowComponent.test.tsx +0 -629
- package/src/components/DataTable/components/__tests__/SortIndicator.test.tsx +0 -135
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +0 -864
- package/src/components/DataTable/components/__tests__/cellValueUtils.test.ts +0 -453
- package/src/components/DataTable/components/cellValueUtils.ts +0 -40
- package/src/components/DataTable/components/hooks/useImportModalFocus.test.ts +0 -184
- package/src/components/DataTable/components/hooks/useImportModalFocus.ts +0 -53
- package/src/components/DataTable/components/hooks/usePermissionTracking.test.ts +0 -381
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -122
- package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +0 -328
- package/src/components/DataTable/core/ActionManager.ts +0 -235
- package/src/components/DataTable/core/ColumnManager.ts +0 -204
- package/src/components/DataTable/core/DataManager.ts +0 -190
- package/src/components/DataTable/core/LocalDataAdapter.ts +0 -274
- package/src/components/DataTable/core/PluginRegistry.ts +0 -229
- package/src/components/DataTable/core/StateManager.ts +0 -312
- package/src/components/DataTable/core/__tests__/ActionManager.test.ts +0 -235
- package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +0 -403
- package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +0 -141
- package/src/components/DataTable/core/__tests__/DataManager.test.ts +0 -178
- package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +0 -133
- package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +0 -142
- package/src/components/DataTable/core/__tests__/StateManager.test.ts +0 -158
- package/src/components/DataTable/core/interfaces.ts +0 -338
- package/src/components/DataTable/hooks/__tests__/useColumnOrderPersistence.test.ts +0 -516
- package/src/components/DataTable/hooks/__tests__/useColumnVisibilityPersistence.test.ts +0 -256
- package/src/components/DataTable/hooks/__tests__/useDataTableConfiguration.test.ts +0 -297
- package/src/components/DataTable/hooks/__tests__/useDataTableDataPipeline.test.ts +0 -270
- package/src/components/DataTable/hooks/__tests__/useDataTablePermissions.test.ts +0 -280
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +0 -691
- package/src/components/DataTable/hooks/__tests__/useEffectiveColumnOrder.test.ts +0 -183
- package/src/components/DataTable/hooks/__tests__/useHierarchicalState.test.ts +0 -294
- package/src/components/DataTable/hooks/__tests__/useKeyboardNavigation.test.ts +0 -787
- package/src/components/DataTable/hooks/__tests__/useServerSideDataEffect.test.ts +0 -258
- package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +0 -499
- package/src/components/DataTable/hooks/__tests__/useTableHandlers.test.ts +0 -440
- package/src/components/DataTable/types.ts +0 -764
- package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +0 -548
- package/src/components/DataTable/utils/__tests__/aggregationUtils.test.ts +0 -288
- package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +0 -94
- package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +0 -209
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +0 -954
- package/src/components/DataTable/utils/__tests__/flexibleImport.test.ts +0 -573
- package/src/components/DataTable/utils/__tests__/hierarchicalSorting.test.ts +0 -235
- package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +0 -586
- package/src/components/DataTable/utils/__tests__/paginationUtils.test.ts +0 -593
- package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +0 -470
- package/src/components/DataTable/utils/__tests__/rowUtils.test.ts +0 -235
- package/src/components/DataTable/utils/__tests__/selectFieldUtils.test.ts +0 -208
- package/src/components/NavigationMenu/__tests__/useNavigationFiltering.test.ts +0 -1934
- package/src/components/Select/__tests__/context.test.tsx +0 -56
- package/src/components/Select/hooks/__tests__/useSelectEvents.test.ts +0 -279
- package/src/components/Select/hooks/__tests__/useSelectSearch.test.tsx +0 -295
- package/src/components/Select/hooks/__tests__/useSelectState.test.ts +0 -254
- package/src/components/Select/hooks/useSelectEvents.ts +0 -87
- package/src/components/Select/hooks/useSelectSearch.ts +0 -91
- package/src/components/Select/hooks/useSelectState.ts +0 -104
- package/src/components/Select/utils/__tests__/text.test.tsx +0 -104
- package/src/components/Select/utils/text.ts +0 -26
- package/src/components/__tests__/index.test.ts +0 -346
- package/src/constants/__tests__/performance.test.ts +0 -91
- package/src/hooks/__tests__/ServiceHooks.test.tsx +0 -725
- package/src/hooks/__tests__/hooks.integration.test.tsx +0 -608
- package/src/hooks/__tests__/index.unit.test.ts +0 -220
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +0 -406
- package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +0 -328
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +0 -720
- package/src/hooks/__tests__/useDataTableState.test.ts +0 -170
- package/src/hooks/__tests__/useDebounce.unit.test.ts +0 -157
- package/src/hooks/__tests__/useEvents.unit.test.ts +0 -227
- package/src/hooks/__tests__/useFileDisplay.test.ts +0 -540
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +0 -1109
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +0 -696
- package/src/hooks/__tests__/useFileUrlCache.test.ts +0 -319
- package/src/hooks/__tests__/useFocusManagement.unit.test.ts +0 -604
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +0 -613
- package/src/hooks/__tests__/useFormDialog.test.ts +0 -307
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +0 -446
- package/src/hooks/__tests__/useIsMobile.unit.test.ts +0 -317
- package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +0 -907
- package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +0 -293
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +0 -961
- package/src/hooks/__tests__/useOrganisations.unit.test.ts +0 -369
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +0 -694
- package/src/hooks/__tests__/usePermissionCache.test.ts +0 -506
- package/src/hooks/__tests__/usePreventTabReload.test.ts +0 -307
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -794
- package/src/hooks/__tests__/usePublicEvent.test.ts +0 -670
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +0 -638
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +0 -948
- package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +0 -442
- package/src/hooks/__tests__/useQueryCache.test.ts +0 -391
- package/src/hooks/__tests__/useRBAC.unit.test.ts +0 -236
- package/src/hooks/__tests__/useSessionDraft.test.ts +0 -556
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +0 -390
- package/src/hooks/__tests__/useStorage.unit.test.ts +0 -721
- package/src/hooks/__tests__/useToast.test.ts +0 -413
- package/src/hooks/__tests__/useToast.unit.test.tsx +0 -481
- package/src/hooks/__tests__/useZodForm.unit.test.tsx +0 -191
- package/src/hooks/public/usePublicFileDisplay.test.ts +0 -723
- package/src/hooks/public/usePublicFileDisplay.ts +0 -534
- package/src/hooks/useFileDisplay.ts +0 -748
- package/src/icons/__tests__/index.test.ts +0 -133
- package/src/providers/OrganisationProvider.test.tsx +0 -40
- package/src/providers/OrganisationProvider.tsx +0 -92
- package/src/providers/__tests__/AuthProvider.test.tsx +0 -218
- package/src/providers/__tests__/EventProvider.test.tsx +0 -487
- package/src/providers/__tests__/InactivityProvider.test-helper.tsx +0 -65
- package/src/providers/__tests__/InactivityProvider.test.tsx +0 -428
- package/src/providers/__tests__/OrganisationProvider.test.tsx +0 -616
- package/src/providers/__tests__/OrganisationProvider.wrapper.test.tsx +0 -591
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +0 -308
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +0 -503
- package/src/providers/__tests__/index.test.ts +0 -138
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +0 -229
- package/src/providers/services/__tests__/AuthServiceProvider.test.tsx +0 -638
- package/src/providers/services/__tests__/EventServiceProvider.test.tsx +0 -839
- package/src/providers/services/__tests__/InactivityServiceProvider.test.tsx +0 -662
- package/src/providers/services/__tests__/OrganisationServiceProvider.test.tsx +0 -440
- package/src/providers/services/__tests__/UnifiedAuthProvider.advanced.test.tsx +0 -435
- package/src/providers/services/__tests__/UnifiedAuthProvider.appId.test.tsx +0 -408
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +0 -301
- package/src/providers/services/__tests__/contexts.test.tsx +0 -281
- package/src/providers/services/__tests__/useUnifiedAuth.test.tsx +0 -251
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +0 -429
- package/src/rbac/__tests__/audit-batched.test.ts +0 -550
- package/src/rbac/__tests__/auth-rbac-security.integration.test.tsx +0 -300
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +0 -517
- package/src/rbac/__tests__/cache-invalidation.test.ts +0 -393
- package/src/rbac/__tests__/engine.comprehensive.test.ts +0 -808
- package/src/rbac/__tests__/performance.test.ts +0 -451
- package/src/rbac/__tests__/rbac-core.test.tsx +0 -276
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +0 -387
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +0 -252
- package/src/rbac/__tests__/rbac-functions.test.ts +0 -646
- package/src/rbac/__tests__/rbac-role-isolation.test.ts +0 -456
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +0 -271
- package/src/rbac/components/__tests__/AccessDenied.test.tsx +0 -324
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +0 -1146
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +0 -231
- package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +0 -243
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +0 -1430
- package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +0 -185
- package/src/rbac/hooks/__tests__/usePermissions.integration.test.ts +0 -427
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +0 -1179
- package/src/rbac/hooks/permissions/__tests__/useAccessLevel.test.ts +0 -622
- package/src/rbac/hooks/permissions/__tests__/useCan.test.ts +0 -798
- package/src/rbac/hooks/permissions/__tests__/useMultiplePermissions.test.ts +0 -843
- package/src/rbac/hooks/permissions/__tests__/usePermissions.test.ts +0 -545
- package/src/rbac/utils/__tests__/clientSecurity.test.ts +0 -192
- package/src/rbac/utils/__tests__/contextValidator.test.ts +0 -126
- package/src/rbac/utils/__tests__/deep-equal.test.ts +0 -76
- package/src/rbac/utils/__tests__/eventContext.test.ts +0 -386
- package/src/services/__tests__/AuthService.edge-cases.test.ts +0 -746
- package/src/services/__tests__/AuthService.restoreSession.test.ts +0 -59
- package/src/services/__tests__/AuthService.test.ts +0 -1328
- package/src/services/__tests__/BaseService.edge-cases.test.ts +0 -506
- package/src/services/__tests__/BaseService.test.ts +0 -363
- package/src/services/__tests__/EventService.edge-cases.test.ts +0 -633
- package/src/services/__tests__/EventService.eventColours.test.ts +0 -64
- package/src/services/__tests__/EventService.test.ts +0 -1018
- package/src/services/__tests__/InactivityService.edge-cases.test.ts +0 -492
- package/src/services/__tests__/InactivityService.lifecycle.test.ts +0 -406
- package/src/services/__tests__/InactivityService.test.ts +0 -654
- package/src/services/__tests__/OrganisationService.edge-cases.test.ts +0 -633
- package/src/services/__tests__/OrganisationService.pagination.test.ts +0 -409
- package/src/services/__tests__/OrganisationService.test.ts +0 -1176
- package/src/services/interfaces/__tests__/IAuthService.test.ts +0 -190
- package/src/services/interfaces/__tests__/IEventService.test.ts +0 -176
- package/src/services/interfaces/__tests__/IInactivityService.test.ts +0 -183
- package/src/services/interfaces/__tests__/IOrganisationService.test.ts +0 -207
- package/src/theming/__tests__/parseEventColours.test.ts +0 -321
- package/src/theming/__tests__/runtime.test.ts +0 -504
- package/src/types/__tests__/core.test.ts +0 -397
- package/src/types/__tests__/database-generated.test.ts +0 -78
- package/src/types/__tests__/file-reference.test.ts +0 -351
- package/src/types/__tests__/guards.test.ts +0 -246
- package/src/types/__tests__/index.test.ts +0 -265
- package/src/types/__tests__/organisation.roles.test.ts +0 -55
- package/src/types/__tests__/organisation.test.ts +0 -1133
- package/src/types/__tests__/theme.test.ts +0 -830
- package/src/types/__tests__/type-validation.test.ts +0 -526
- package/src/types/__tests__/validation.test.ts +0 -729
- package/src/utils/__tests__/appConfig.unit.test.ts +0 -55
- package/src/utils/__tests__/audit.unit.test.ts +0 -69
- package/src/utils/__tests__/auth-utils.unit.test.ts +0 -70
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +0 -326
- package/src/utils/__tests__/cn.unit.test.ts +0 -34
- package/src/utils/__tests__/debugLogger.test.ts +0 -417
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +0 -818
- package/src/utils/__tests__/dynamicUtils.unit.test.ts +0 -331
- package/src/utils/__tests__/formatDate.unit.test.ts +0 -109
- package/src/utils/__tests__/formatting.unit.test.ts +0 -99
- package/src/utils/__tests__/index.unit.test.ts +0 -251
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +0 -320
- package/src/utils/__tests__/logger.unit.test.ts +0 -398
- package/src/utils/__tests__/organisationContext.unit.test.ts +0 -191
- package/src/utils/__tests__/performanceBenchmark.test.ts +0 -174
- package/src/utils/__tests__/performanceBudgets.unit.test.ts +0 -288
- package/src/utils/__tests__/permissionTypes.unit.test.ts +0 -250
- package/src/utils/__tests__/permissionUtils.unit.test.ts +0 -362
- package/src/utils/__tests__/request-deduplication.test.ts +0 -349
- package/src/utils/__tests__/sanitization.unit.test.ts +0 -346
- package/src/utils/__tests__/schemaUtils.unit.test.ts +0 -441
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +0 -334
- package/src/utils/__tests__/secureErrors.unit.test.ts +0 -390
- package/src/utils/__tests__/secureStorage.unit.test.ts +0 -289
- package/src/utils/__tests__/security.unit.test.ts +0 -149
- package/src/utils/__tests__/securityMonitor.unit.test.ts +0 -276
- package/src/utils/__tests__/sessionTracking.unit.test.ts +0 -218
- package/src/utils/__tests__/timezone.test.ts +0 -345
- package/src/utils/__tests__/validation.unit.test.ts +0 -307
- package/src/utils/__tests__/validationUtils.unit.test.ts +0 -555
- package/src/utils/core/__tests__/cn.test.ts +0 -66
- package/src/utils/core/__tests__/debugLogger.test.ts +0 -113
- package/src/utils/core/__tests__/logger.test.ts +0 -217
- package/src/utils/dynamic/__tests__/dynamicUtils.test.ts +0 -185
- package/src/utils/dynamic/__tests__/lazyLoad.test.tsx +0 -156
- package/src/utils/file-reference/__tests__/file-reference.test.ts +0 -1313
- package/src/utils/performance/__tests__/bundleAnalysis.test.ts +0 -148
- package/src/utils/performance/__tests__/performanceBenchmark.test.ts +0 -251
- package/src/utils/performance/__tests__/performanceBudgets.test.ts +0 -241
- package/src/utils/permissions/__tests__/permissionTypes.test.ts +0 -149
- package/src/utils/persistence/__tests__/keyDerivation.test.ts +0 -306
- package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +0 -271
- package/src/utils/storage/__tests__/config.unit.test.ts +0 -239
- package/src/utils/storage/__tests__/index.unit.test.ts +0 -68
- package/src/utils/validation/__tests__/common.test.ts +0 -115
- package/src/utils/validation/__tests__/csrf.test.ts +0 -170
- package/src/utils/validation/__tests__/htmlSanitization.unit.test.ts +0 -618
- package/src/utils/validation/__tests__/passwordSchema.test.ts +0 -164
- package/src/utils/validation/__tests__/schema.test.ts +0 -127
- package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +0 -165
- package/src/utils/validation/__tests__/user.test.ts +0 -173
- package/src/utils/validation/__tests__/validation.test.ts +0 -197
- package/src/utils/validation/__tests__/validationUtils.test.ts +0 -294
- /package/src/components/DataTable/{components/__tests__ → ui}/COVERAGE_NOTE.md +0 -0
- /package/src/components/DataTable/utils/{__tests__/COVERAGE_NOTE.md → COVERAGE_NOTE.md} +0 -0
- /package/src/hooks/{__tests__/useApiFetch.unit.test.ts → useApiFetch.unit.test.ts} +0 -0
- /package/src/providers/{__tests__/README.md → README.md} +0 -0
- /package/src/rbac/{__tests__/index.test.ts → index.test.ts} +0 -0
- /package/src/rbac/{__tests__/rbac-integration.test.ts → rbac-integration.test.ts} +0 -0
- /package/src/types/{__tests__/README.md → README.md} +0 -0
|
@@ -0,0 +1,1834 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Import Modal Component Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DataTable/Components
|
|
5
|
+
* @since 0.4.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive test suite for ImportModal component following testing guidelines.
|
|
8
|
+
* Tests cover all major functionality, edge cases, and user interactions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { render, screen, waitFor, cleanup } from '@testing-library/react';
|
|
13
|
+
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
15
|
+
import { ImportModal } from './ImportModal';
|
|
16
|
+
|
|
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 ensure fresh modal state (clears any persistent state from previous tests)
|
|
60
|
+
// This works by rendering a modal, opening it, clicking close button to trigger handleClose,
|
|
61
|
+
// then closing it programmatically to ensure cleanup
|
|
62
|
+
const ensureFreshModalState = async () => {
|
|
63
|
+
const onClose = vi.fn();
|
|
64
|
+
const user = userEvent.setup();
|
|
65
|
+
|
|
66
|
+
// Render a modal and open it
|
|
67
|
+
const { rerender, unmount } = render(
|
|
68
|
+
<ImportModal
|
|
69
|
+
isOpen={true}
|
|
70
|
+
onClose={onClose}
|
|
71
|
+
onImport={vi.fn()}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Wait for modal to initialize
|
|
76
|
+
try {
|
|
77
|
+
await waitFor(() => {
|
|
78
|
+
const dialog = document.querySelector('dialog[role="dialog"]');
|
|
79
|
+
if (!dialog) throw new Error('Dialog not found');
|
|
80
|
+
}, { timeout: 1000 });
|
|
81
|
+
} catch (_e) {
|
|
82
|
+
// If modal didn't render, that's okay, we'll still close it
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if there's a summary - if so, click close button to trigger handleClose
|
|
86
|
+
await waitForDialog();
|
|
87
|
+
const hasSummary = screen.queryByText(/import completed/i);
|
|
88
|
+
if (hasSummary) {
|
|
89
|
+
const closeButton = findButtonByText(/^close$/i) || findButtonByText(/cancel/i);
|
|
90
|
+
if (closeButton) {
|
|
91
|
+
await user.click(closeButton);
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Close the modal by setting isOpen to false
|
|
97
|
+
// This triggers the useEffect that clears persistent storage when there's no importSummary
|
|
98
|
+
rerender(
|
|
99
|
+
<ImportModal
|
|
100
|
+
isOpen={false}
|
|
101
|
+
onClose={onClose}
|
|
102
|
+
onImport={vi.fn() as (data: Array<Record<string, unknown>>) => void | Promise<void>}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Wait for React to process the state change and run cleanup effects
|
|
107
|
+
// The component's useEffect (lines 152-173) clears persistent storage when isOpen becomes false
|
|
108
|
+
// We need to wait for this effect to complete
|
|
109
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
110
|
+
|
|
111
|
+
// Unmount to ensure cleanup
|
|
112
|
+
unmount();
|
|
113
|
+
cleanup();
|
|
114
|
+
|
|
115
|
+
// Additional wait to ensure module-level state is cleared
|
|
116
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Helper function to wait for file input to be available
|
|
120
|
+
// This function handles the case where persistent state might have leaked from previous tests
|
|
121
|
+
const waitForFileInput = async (
|
|
122
|
+
rerender?: (element: React.ReactElement) => void,
|
|
123
|
+
props?: { isOpen?: boolean; onClose: () => void; onImport: (data: Array<Record<string, unknown>>) => void | Promise<void>; config?: any }
|
|
124
|
+
): Promise<HTMLInputElement> => {
|
|
125
|
+
// First wait for dialog to be accessible
|
|
126
|
+
await waitForDialog();
|
|
127
|
+
|
|
128
|
+
// Then wait for dialog content to be rendered (check for title)
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
try {
|
|
131
|
+
expect(screen.getByText('Import Data')).toBeInTheDocument();
|
|
132
|
+
} catch (_e) {
|
|
133
|
+
// Fallback: check if any element with "Import Data" exists
|
|
134
|
+
const elements = screen.getAllByText('Import Data');
|
|
135
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
136
|
+
}
|
|
137
|
+
}, { timeout: 5000 });
|
|
138
|
+
|
|
139
|
+
// Check if a file is already selected OR if there's a summary (from persistent state leakage)
|
|
140
|
+
const selectedFileText = screen.queryByText(/selected:/i);
|
|
141
|
+
const hasSummary = screen.queryByText(/import completed/i);
|
|
142
|
+
|
|
143
|
+
if ((selectedFileText || hasSummary) && rerender) {
|
|
144
|
+
// Persistent state leaked from a previous test - clear it by closing and reopening the modal
|
|
145
|
+
const closeProps = props || getBaseProps();
|
|
146
|
+
|
|
147
|
+
// Strategy: Always click cancel/close button to trigger handleClose which clears everything
|
|
148
|
+
// This is more reliable than relying on useEffect cleanup
|
|
149
|
+
const closeButton = findButtonByText(/^close$/i) || findButtonByText(/cancel/i);
|
|
150
|
+
if (closeButton) {
|
|
151
|
+
const user = userEvent.setup();
|
|
152
|
+
await user.click(closeButton);
|
|
153
|
+
// Wait for handleClose to execute (clears all persistent storage)
|
|
154
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Close the modal programmatically (onClose is just a mock, so we need to set isOpen=false)
|
|
158
|
+
rerender(<ImportModal {...closeProps} isOpen={false} />);
|
|
159
|
+
|
|
160
|
+
// Wait for modal to close
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
163
|
+
}, { timeout: 2000 });
|
|
164
|
+
|
|
165
|
+
// Wait for cleanup effect to run (clears persistent storage)
|
|
166
|
+
// The useEffect at line 152-173 clears persistentFile when isOpen=false and !importSummary
|
|
167
|
+
// Since handleClose already cleared persistentSummary, the cleanup should run
|
|
168
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
169
|
+
|
|
170
|
+
// Reopen the modal with fresh state
|
|
171
|
+
const reopenProps = props || getBaseProps();
|
|
172
|
+
rerender(<ImportModal {...reopenProps} isOpen={true} />);
|
|
173
|
+
|
|
174
|
+
// Wait for dialog to be accessible again
|
|
175
|
+
await waitForDialog();
|
|
176
|
+
|
|
177
|
+
// Wait for dialog content to be rendered
|
|
178
|
+
await waitFor(() => {
|
|
179
|
+
expect(screen.getByText('Import Data')).toBeInTheDocument();
|
|
180
|
+
}, { timeout: 5000 });
|
|
181
|
+
|
|
182
|
+
// Wait for any state restoration to complete (useEffect at line 134-149)
|
|
183
|
+
// But since we cleared persistent storage, nothing should be restored
|
|
184
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
185
|
+
|
|
186
|
+
// Verify state was cleared - check multiple times
|
|
187
|
+
for (let i = 0; i < 5; i++) {
|
|
188
|
+
const stillSelected = screen.queryByText(/selected:/i);
|
|
189
|
+
const stillHasSummary = screen.queryByText(/import completed/i);
|
|
190
|
+
if (!stillSelected && !stillHasSummary) {
|
|
191
|
+
break; // State is cleared
|
|
192
|
+
}
|
|
193
|
+
if (i === 4) {
|
|
194
|
+
// Last attempt - if still not cleared, we have a problem
|
|
195
|
+
throw new Error('Persistent state was not cleared after close/reopen. Still seeing selected file or summary.');
|
|
196
|
+
}
|
|
197
|
+
// Wait a bit more and check again
|
|
198
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
199
|
+
// Re-render to force state refresh
|
|
200
|
+
rerender(<ImportModal {...reopenProps} isOpen={true} />);
|
|
201
|
+
await waitForDialog();
|
|
202
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
203
|
+
}
|
|
204
|
+
} else if (selectedFileText || hasSummary) {
|
|
205
|
+
// Can't clear it without rerender function
|
|
206
|
+
throw new Error('File input not available because a file is already selected or summary exists. The test needs to pass rerender (and optionally props) to waitForFileInput to clear persistent state.');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Wait for file input to be in the DOM (it's only rendered when !file)
|
|
210
|
+
// After clearing state, wait a bit more to ensure component is fully initialized
|
|
211
|
+
if ((selectedFileText || hasSummary) && rerender) {
|
|
212
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return await waitFor(() => {
|
|
216
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
217
|
+
if (!fileInput) {
|
|
218
|
+
// Double-check if file is still selected
|
|
219
|
+
const stillSelected = screen.queryByText(/selected:/i);
|
|
220
|
+
if (stillSelected) {
|
|
221
|
+
throw new Error('File input not found because a file is already selected. Persistent state may not have been cleared.');
|
|
222
|
+
}
|
|
223
|
+
throw new Error('File input not found in DOM. The dialog may not be fully rendered yet.');
|
|
224
|
+
}
|
|
225
|
+
// Ensure file input is enabled and ready
|
|
226
|
+
if (fileInput.disabled) {
|
|
227
|
+
throw new Error('File input is disabled. Component may not be fully initialized.');
|
|
228
|
+
}
|
|
229
|
+
return fileInput;
|
|
230
|
+
}, { timeout: 5000 });
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Helper function to upload a file and wait for preview to appear
|
|
234
|
+
const uploadFileAndWaitForPreview = async (user: ReturnType<typeof userEvent.setup>, file: File) => {
|
|
235
|
+
// Wait for dialog and file input
|
|
236
|
+
const fileInput = await waitForFileInput();
|
|
237
|
+
|
|
238
|
+
// Upload the file
|
|
239
|
+
await user.upload(fileInput, file);
|
|
240
|
+
|
|
241
|
+
// Wait for preview table to appear (file processing is async)
|
|
242
|
+
// Try multiple ways to find the table since queryByRole may not work in all test environments
|
|
243
|
+
await waitFor(() => {
|
|
244
|
+
const table = screen.queryByRole('table') ||
|
|
245
|
+
document.querySelector('table') ||
|
|
246
|
+
document.querySelector('table.min-w-full');
|
|
247
|
+
expect(table).toBeInTheDocument();
|
|
248
|
+
}, { timeout: 10000 });
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Mock Button component (Dialog uses IconButton from same module - must export both)
|
|
252
|
+
vi.mock('../../Button/Button', () => ({
|
|
253
|
+
Button: ({ children, onClick, variant, size, disabled, className }: any) => (
|
|
254
|
+
<button
|
|
255
|
+
onClick={onClick}
|
|
256
|
+
disabled={disabled}
|
|
257
|
+
data-variant={variant}
|
|
258
|
+
data-size={size}
|
|
259
|
+
className={className}
|
|
260
|
+
>
|
|
261
|
+
{children}
|
|
262
|
+
</button>
|
|
263
|
+
),
|
|
264
|
+
IconButton: React.forwardRef(({ onClick, icon, 'aria-label': ariaLabel, className, ...props }: any, ref: any) => (
|
|
265
|
+
<button ref={ref} onClick={onClick} aria-label={ariaLabel} className={className} {...props}>
|
|
266
|
+
{icon}
|
|
267
|
+
</button>
|
|
268
|
+
)),
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
// Mock Input component
|
|
272
|
+
vi.mock('../../Input/Input', () => ({
|
|
273
|
+
Input: React.forwardRef(({ type, accept, onChange, className, ...props }: any, ref: any) => (
|
|
274
|
+
<input
|
|
275
|
+
ref={ref}
|
|
276
|
+
type={type}
|
|
277
|
+
accept={accept}
|
|
278
|
+
onChange={onChange}
|
|
279
|
+
className={className}
|
|
280
|
+
{...props}
|
|
281
|
+
/>
|
|
282
|
+
)),
|
|
283
|
+
}));
|
|
284
|
+
|
|
285
|
+
// Mock lucide-react icons
|
|
286
|
+
vi.mock('lucide-react', () => ({
|
|
287
|
+
Upload: ({ className }: { className?: string }) => (
|
|
288
|
+
<span data-testid="upload-icon" className={className}>Upload</span>
|
|
289
|
+
),
|
|
290
|
+
FileText: ({ className }: { className?: string }) => (
|
|
291
|
+
<span data-testid="file-text-icon" className={className}>File</span>
|
|
292
|
+
),
|
|
293
|
+
AlertCircle: ({ className }: { className?: string }) => (
|
|
294
|
+
<span data-testid="alert-circle-icon" className={className}>Alert</span>
|
|
295
|
+
),
|
|
296
|
+
CheckCircle2: ({ className }: { className?: string }) => (
|
|
297
|
+
<span data-testid="check-circle-icon" className={className}>Check</span>
|
|
298
|
+
),
|
|
299
|
+
X: ({ className }: { className?: string }) => (
|
|
300
|
+
<span data-testid="x-icon" className={className}>X</span>
|
|
301
|
+
),
|
|
302
|
+
}));
|
|
303
|
+
|
|
304
|
+
// Mock logger
|
|
305
|
+
vi.mock('../../../utils/core/logger', () => ({
|
|
306
|
+
createLogger: () => ({
|
|
307
|
+
debug: vi.fn(),
|
|
308
|
+
info: vi.fn(),
|
|
309
|
+
warn: vi.fn(),
|
|
310
|
+
error: vi.fn(),
|
|
311
|
+
}),
|
|
312
|
+
}));
|
|
313
|
+
|
|
314
|
+
// Base props for tests - defined outside describe so helpers can access it
|
|
315
|
+
const getBaseProps = () => ({
|
|
316
|
+
isOpen: true,
|
|
317
|
+
onClose: vi.fn(),
|
|
318
|
+
onImport: vi.fn(),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('[component] ImportModal', () => {
|
|
322
|
+
const baseProps = getBaseProps();
|
|
323
|
+
|
|
324
|
+
const createCSVFile = (content: string, filename = 'test.csv'): File => {
|
|
325
|
+
const blob = new Blob([content], { type: 'text/csv' });
|
|
326
|
+
const file = new File([blob], filename, { type: 'text/csv' });
|
|
327
|
+
// Store content for File.text() mock to access
|
|
328
|
+
(file as any)._content = content;
|
|
329
|
+
return file;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
beforeEach(async () => {
|
|
333
|
+
vi.clearAllMocks();
|
|
334
|
+
|
|
335
|
+
// Clear any rendered components from previous tests
|
|
336
|
+
cleanup();
|
|
337
|
+
|
|
338
|
+
// Mock showModal for dialog elements (needed for test environments)
|
|
339
|
+
// MUST be set up before any components are rendered
|
|
340
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function(this: HTMLDialogElement) {
|
|
341
|
+
this.setAttribute('open', '');
|
|
342
|
+
this.dispatchEvent(new Event('show', { bubbles: true }));
|
|
343
|
+
});
|
|
344
|
+
HTMLDialogElement.prototype.close = vi.fn(function(this: HTMLDialogElement) {
|
|
345
|
+
this.removeAttribute('open');
|
|
346
|
+
this.dispatchEvent(new Event('close', { bubbles: true }));
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Mock File.text() method for jsdom compatibility
|
|
350
|
+
// File.text() reads the file content asynchronously
|
|
351
|
+
// In tests, we read directly from the Blob content synchronously
|
|
352
|
+
if (!File.prototype.text) {
|
|
353
|
+
Object.defineProperty(File.prototype, 'text', {
|
|
354
|
+
writable: true,
|
|
355
|
+
configurable: true,
|
|
356
|
+
value: async function(this: File) {
|
|
357
|
+
// For test files, read from the stored content or from the Blob
|
|
358
|
+
const file = this as any;
|
|
359
|
+
if (file._content) {
|
|
360
|
+
// Use stored content if available
|
|
361
|
+
return Promise.resolve(file._content);
|
|
362
|
+
}
|
|
363
|
+
// Otherwise, try to read from Blob using FileReader
|
|
364
|
+
return new Promise((resolve, reject) => {
|
|
365
|
+
const reader = new FileReader();
|
|
366
|
+
reader.onload = (e) => {
|
|
367
|
+
resolve(e.target?.result as string);
|
|
368
|
+
};
|
|
369
|
+
reader.onerror = () => {
|
|
370
|
+
reject(new Error('Failed to read file'));
|
|
371
|
+
};
|
|
372
|
+
// Read the file as text
|
|
373
|
+
reader.readAsText(this);
|
|
374
|
+
});
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Clear ImportModal's persistent state by opening and closing a modal
|
|
380
|
+
// The ImportModal uses module-level persistent storage that can leak between tests
|
|
381
|
+
// We need to ensure it's cleared by closing a modal (which calls handleClose and clears persistent storage)
|
|
382
|
+
await ensureFreshModalState();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
afterEach(() => {
|
|
386
|
+
// Clean up all rendered components to ensure state doesn't leak
|
|
387
|
+
cleanup();
|
|
388
|
+
vi.clearAllMocks();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('Rendering', () => {
|
|
392
|
+
it('returns null when modal is closed', () => {
|
|
393
|
+
const { container } = render(
|
|
394
|
+
<ImportModal {...baseProps} isOpen={false} />
|
|
395
|
+
);
|
|
396
|
+
expect(container.firstChild).toBeNull();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('renders modal when open', async () => {
|
|
400
|
+
render(<ImportModal {...baseProps} />);
|
|
401
|
+
|
|
402
|
+
// Wait for dialog to be accessible
|
|
403
|
+
await waitForDialog();
|
|
404
|
+
// Check for content instead of testids
|
|
405
|
+
expect(screen.getByText('Import Data')).toBeInTheDocument();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('renders default title', async () => {
|
|
409
|
+
render(<ImportModal {...baseProps} />);
|
|
410
|
+
|
|
411
|
+
// Wait for dialog content to be rendered (showModal is async via requestAnimationFrame)
|
|
412
|
+
await waitFor(() => {
|
|
413
|
+
// Try by role first, fallback to text content
|
|
414
|
+
try {
|
|
415
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
416
|
+
} catch (_e) {
|
|
417
|
+
expect(screen.getByText('Import Data')).toBeInTheDocument();
|
|
418
|
+
}
|
|
419
|
+
}, { timeout: 5000 });
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('renders custom title from config', async () => {
|
|
423
|
+
render(
|
|
424
|
+
<ImportModal
|
|
425
|
+
{...baseProps}
|
|
426
|
+
config={{ title: 'Custom Import Title' }}
|
|
427
|
+
/>
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// Wait for dialog content to be rendered (showModal is async via requestAnimationFrame)
|
|
431
|
+
await waitFor(() => {
|
|
432
|
+
// Try by role first, fallback to text content
|
|
433
|
+
try {
|
|
434
|
+
expect(screen.getByRole('heading', { name: 'Custom Import Title' })).toBeInTheDocument();
|
|
435
|
+
} catch (_e) {
|
|
436
|
+
expect(screen.getByText('Custom Import Title')).toBeInTheDocument();
|
|
437
|
+
}
|
|
438
|
+
}, { timeout: 5000 });
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('renders default description', async () => {
|
|
442
|
+
render(<ImportModal {...baseProps} />);
|
|
443
|
+
|
|
444
|
+
// Wait for dialog to be accessible
|
|
445
|
+
await waitForDialog();
|
|
446
|
+
// Description is rendered as p in DialogHeader
|
|
447
|
+
expect(screen.getByText('Upload a CSV file to import multiple records at once.')).toBeInTheDocument();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('renders custom description from config', async () => {
|
|
451
|
+
render(
|
|
452
|
+
<ImportModal
|
|
453
|
+
{...baseProps}
|
|
454
|
+
config={{ description: 'Custom description' }}
|
|
455
|
+
/>
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// Wait for dialog to be accessible
|
|
459
|
+
await waitForDialog();
|
|
460
|
+
// Description is rendered as p in DialogHeader
|
|
461
|
+
expect(screen.getByText('Custom description')).toBeInTheDocument();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('renders file upload area', async () => {
|
|
465
|
+
render(<ImportModal {...baseProps} />);
|
|
466
|
+
|
|
467
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
468
|
+
// Use getByRole for heading to avoid multiple matches (h2 and button both have "Import Data")
|
|
469
|
+
await waitFor(() => {
|
|
470
|
+
try {
|
|
471
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
472
|
+
} catch (_e) {
|
|
473
|
+
// Fallback: check if any element with "Import Data" exists
|
|
474
|
+
const elements = screen.getAllByText('Import Data');
|
|
475
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
476
|
+
}
|
|
477
|
+
}, { timeout: 5000 });
|
|
478
|
+
|
|
479
|
+
// Then check for upload area text and button (use querySelector as fallback for buttons in dialogs)
|
|
480
|
+
await waitFor(() => {
|
|
481
|
+
expect(screen.getByText(/choose a csv file/i)).toBeInTheDocument();
|
|
482
|
+
const selectFileButton = findButtonByText(/select file/i);
|
|
483
|
+
expect(selectFileButton).toBeInTheDocument();
|
|
484
|
+
}, { timeout: 5000 });
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('renders cancel button', async () => {
|
|
488
|
+
render(<ImportModal {...baseProps} />);
|
|
489
|
+
|
|
490
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
491
|
+
// Use getByRole for heading to avoid multiple matches (h2 and button both have "Import Data")
|
|
492
|
+
await waitFor(() => {
|
|
493
|
+
try {
|
|
494
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
495
|
+
} catch (_e) {
|
|
496
|
+
// Fallback: check if any element with "Import Data" exists
|
|
497
|
+
const elements = screen.getAllByText('Import Data');
|
|
498
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
499
|
+
}
|
|
500
|
+
}, { timeout: 5000 });
|
|
501
|
+
|
|
502
|
+
// Then check for cancel button (use querySelector as fallback for buttons in dialogs)
|
|
503
|
+
await waitFor(() => {
|
|
504
|
+
const cancelButton = screen.queryByRole('button', { name: /cancel/i })
|
|
505
|
+
|| Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.match(/cancel/i));
|
|
506
|
+
expect(cancelButton).toBeInTheDocument();
|
|
507
|
+
}, { timeout: 5000 });
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('renders import button', async () => {
|
|
511
|
+
render(<ImportModal {...baseProps} />);
|
|
512
|
+
|
|
513
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
514
|
+
// Use getByRole for heading to avoid multiple matches (h2 and button both have "Import Data")
|
|
515
|
+
await waitFor(() => {
|
|
516
|
+
try {
|
|
517
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
518
|
+
} catch (_e) {
|
|
519
|
+
// Fallback: check if any element with "Import Data" exists
|
|
520
|
+
const elements = screen.getAllByText('Import Data');
|
|
521
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
522
|
+
}
|
|
523
|
+
}, { timeout: 5000 });
|
|
524
|
+
|
|
525
|
+
// Then check for import button (use querySelector as fallback for buttons in dialogs)
|
|
526
|
+
await waitFor(() => {
|
|
527
|
+
const importButton = screen.queryByRole('button', { name: /import/i })
|
|
528
|
+
|| Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.match(/^import$/i));
|
|
529
|
+
expect(importButton).toBeInTheDocument();
|
|
530
|
+
}, { timeout: 5000 });
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe('File Selection', () => {
|
|
535
|
+
it('displays selected file name', async () => {
|
|
536
|
+
const user = userEvent.setup();
|
|
537
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
538
|
+
const file = createCSVFile(csvContent);
|
|
539
|
+
|
|
540
|
+
render(<ImportModal {...baseProps} />);
|
|
541
|
+
|
|
542
|
+
// Wait for file input to be available
|
|
543
|
+
const fileInput = await waitForFileInput();
|
|
544
|
+
await user.upload(fileInput, file);
|
|
545
|
+
|
|
546
|
+
await waitFor(() => {
|
|
547
|
+
// Text is split across elements with <br/>, so use getAllByText and check first match
|
|
548
|
+
const elements = screen.getAllByText((content, element) => {
|
|
549
|
+
return element?.textContent?.includes(`Selected: ${file.name}`) ?? false;
|
|
550
|
+
});
|
|
551
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('resets file when modal closes and reopens', async () => {
|
|
556
|
+
const user = userEvent.setup();
|
|
557
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
558
|
+
const file = createCSVFile(csvContent);
|
|
559
|
+
|
|
560
|
+
const { rerender } = render(<ImportModal {...baseProps} />);
|
|
561
|
+
|
|
562
|
+
// Wait for file input to be available (pass rerender and props to handle persistent state)
|
|
563
|
+
const fileInput = await waitForFileInput(rerender, baseProps);
|
|
564
|
+
await user.upload(fileInput, file);
|
|
565
|
+
|
|
566
|
+
await waitFor(() => {
|
|
567
|
+
// Text is split across elements with <br/>, so use getAllByText and check first match
|
|
568
|
+
const elements = screen.getAllByText((content, element) => {
|
|
569
|
+
return element?.textContent?.includes(`Selected: ${file.name}`) ?? false;
|
|
570
|
+
});
|
|
571
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
rerender(<ImportModal {...baseProps} isOpen={false} />);
|
|
575
|
+
|
|
576
|
+
// Wait for dialog to close
|
|
577
|
+
await waitFor(() => {
|
|
578
|
+
const dialog = document.querySelector('dialog[role="dialog"]');
|
|
579
|
+
expect(dialog).not.toBeInTheDocument();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
rerender(<ImportModal {...baseProps} isOpen={true} />);
|
|
583
|
+
|
|
584
|
+
// Wait for dialog to reopen
|
|
585
|
+
await waitForDialog();
|
|
586
|
+
|
|
587
|
+
// File should be reset - use queryAllByText since text may be split
|
|
588
|
+
const elements = screen.queryAllByText((content, element) => {
|
|
589
|
+
return element?.textContent?.includes(`Selected: ${file.name}`) ?? false;
|
|
590
|
+
});
|
|
591
|
+
expect(elements.length).toBe(0);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
describe('CSV Parsing', () => {
|
|
596
|
+
it('parses valid CSV and shows preview with data and row count', async () => {
|
|
597
|
+
const user = userEvent.setup();
|
|
598
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com\nBob,bob@example.com';
|
|
599
|
+
const file = createCSVFile(csvContent);
|
|
600
|
+
|
|
601
|
+
render(<ImportModal {...baseProps} />);
|
|
602
|
+
|
|
603
|
+
await uploadFileAndWaitForPreview(user, file);
|
|
604
|
+
|
|
605
|
+
await waitFor(() => {
|
|
606
|
+
expect(screen.getByText(/name/i)).toBeInTheDocument();
|
|
607
|
+
expect(screen.getByText(/email/i)).toBeInTheDocument();
|
|
608
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
609
|
+
expect(screen.getByText('john@example.com')).toBeInTheDocument();
|
|
610
|
+
expect(screen.getByText(/total rows to import: 3/i)).toBeInTheDocument();
|
|
611
|
+
}, { timeout: 5000 });
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('handles CSV with quoted values and commas in values', async () => {
|
|
615
|
+
const user = userEvent.setup();
|
|
616
|
+
const csvContent = 'name,description\n"John Doe","Description, with comma"\n"Jane Smith","Another, description"';
|
|
617
|
+
const file = createCSVFile(csvContent);
|
|
618
|
+
|
|
619
|
+
render(<ImportModal {...baseProps} />);
|
|
620
|
+
|
|
621
|
+
await waitForDialog();
|
|
622
|
+
await waitFor(() => {
|
|
623
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
624
|
+
expect(fileInput).toBeInTheDocument();
|
|
625
|
+
}, { timeout: 5000 });
|
|
626
|
+
|
|
627
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
628
|
+
await user.upload(fileInput, file);
|
|
629
|
+
|
|
630
|
+
await waitFor(() => {
|
|
631
|
+
const table = screen.queryByRole('table') ||
|
|
632
|
+
document.querySelector('table') ||
|
|
633
|
+
document.querySelector('table.min-w-full');
|
|
634
|
+
expect(table).toBeInTheDocument();
|
|
635
|
+
}, { timeout: 10000 });
|
|
636
|
+
|
|
637
|
+
await waitFor(() => {
|
|
638
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
639
|
+
expect(screen.getByText(/description, with comma/i)).toBeInTheDocument();
|
|
640
|
+
}, { timeout: 2000 });
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
describe('Error Handling', () => {
|
|
645
|
+
it('displays error for invalid CSV files (empty, only header, whitespace)', async () => {
|
|
646
|
+
// Clear any persistent state from previous tests first
|
|
647
|
+
await ensureFreshModalState();
|
|
648
|
+
|
|
649
|
+
const user = userEvent.setup();
|
|
650
|
+
const onClose = vi.fn();
|
|
651
|
+
|
|
652
|
+
// Test empty file
|
|
653
|
+
const emptyFile = createCSVFile('');
|
|
654
|
+
const testProps = { ...baseProps, onClose };
|
|
655
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
656
|
+
|
|
657
|
+
// Wait for file input to be available (pass rerender and props to handle persistent state)
|
|
658
|
+
let fileInput = await waitForFileInput(rerender, testProps);
|
|
659
|
+
|
|
660
|
+
// Ensure we have a fresh file input reference (in case it was recreated after state clear)
|
|
661
|
+
fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
662
|
+
expect(fileInput).toBeInTheDocument();
|
|
663
|
+
expect(fileInput.value).toBe(''); // Ensure file input is cleared
|
|
664
|
+
|
|
665
|
+
await user.upload(fileInput, emptyFile);
|
|
666
|
+
|
|
667
|
+
// When a file is selected but invalid, the error is set but file is also set
|
|
668
|
+
// The error should be displayed - wait for it to appear
|
|
669
|
+
// Note: The error might be in the CardContent section even when file is selected
|
|
670
|
+
await waitFor(() => {
|
|
671
|
+
// Check for error text or alert icon - error should be displayed somewhere
|
|
672
|
+
const errorText = screen.queryByText(/failed to preview file|CSV must have at least/i);
|
|
673
|
+
const alertIcon = screen.queryByTestId('alert-circle-icon');
|
|
674
|
+
if (!errorText && !alertIcon) {
|
|
675
|
+
throw new Error('Error message or alert icon not found after uploading invalid file');
|
|
676
|
+
}
|
|
677
|
+
}, { timeout: 3000 });
|
|
678
|
+
|
|
679
|
+
// Test CSV with only header
|
|
680
|
+
// Clear the file input value first to allow new file selection
|
|
681
|
+
fileInput.value = '';
|
|
682
|
+
const headerOnlyFile = createCSVFile('name,email');
|
|
683
|
+
await user.upload(fileInput, headerOnlyFile);
|
|
684
|
+
|
|
685
|
+
await waitFor(() => {
|
|
686
|
+
expect(screen.getByTestId('alert-circle-icon')).toBeInTheDocument();
|
|
687
|
+
}, { timeout: 3000 });
|
|
688
|
+
|
|
689
|
+
// Test CSV with only whitespace
|
|
690
|
+
fileInput.value = '';
|
|
691
|
+
const whitespaceFile = createCSVFile(' \n \n ');
|
|
692
|
+
await user.upload(fileInput, whitespaceFile);
|
|
693
|
+
|
|
694
|
+
await waitFor(() => {
|
|
695
|
+
expect(screen.getByTestId('alert-circle-icon')).toBeInTheDocument();
|
|
696
|
+
}, { timeout: 3000 });
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it('clears error when new file is selected', async () => {
|
|
700
|
+
// Clear any persistent state from previous tests first
|
|
701
|
+
await ensureFreshModalState();
|
|
702
|
+
|
|
703
|
+
const user = userEvent.setup();
|
|
704
|
+
const onClose = vi.fn();
|
|
705
|
+
const invalidFile = createCSVFile('invalid');
|
|
706
|
+
const validFile = createCSVFile('name,email\nJohn,john@example.com');
|
|
707
|
+
|
|
708
|
+
const testProps = { ...baseProps, onClose };
|
|
709
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
710
|
+
|
|
711
|
+
// Wait for file input to be available (pass rerender and props to handle persistent state)
|
|
712
|
+
let fileInput = await waitForFileInput(rerender, testProps);
|
|
713
|
+
|
|
714
|
+
// Ensure we have a fresh file input reference (in case it was recreated after state clear)
|
|
715
|
+
fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
716
|
+
expect(fileInput).toBeInTheDocument();
|
|
717
|
+
expect(fileInput.value).toBe(''); // Ensure file input is cleared
|
|
718
|
+
|
|
719
|
+
await user.upload(fileInput, invalidFile);
|
|
720
|
+
|
|
721
|
+
await waitFor(() => {
|
|
722
|
+
expect(screen.getByTestId('alert-circle-icon')).toBeInTheDocument();
|
|
723
|
+
}, { timeout: 3000 });
|
|
724
|
+
|
|
725
|
+
// Clear the file input value first to allow new file selection
|
|
726
|
+
fileInput.value = '';
|
|
727
|
+
|
|
728
|
+
// Get a fresh file input reference (it might have been recreated)
|
|
729
|
+
await waitFor(() => {
|
|
730
|
+
const freshInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
731
|
+
expect(freshInput).toBeInTheDocument();
|
|
732
|
+
expect(freshInput.value).toBe('');
|
|
733
|
+
}, { timeout: 1000 });
|
|
734
|
+
|
|
735
|
+
const freshFileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
736
|
+
await user.upload(freshFileInput, validFile);
|
|
737
|
+
|
|
738
|
+
// Wait for error to be cleared and preview to appear
|
|
739
|
+
await waitFor(() => {
|
|
740
|
+
expect(screen.queryByTestId('alert-circle-icon')).not.toBeInTheDocument();
|
|
741
|
+
// Also verify that preview appears (indicating valid file was processed)
|
|
742
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
743
|
+
expect(table).toBeInTheDocument();
|
|
744
|
+
}, { timeout: 3000 });
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
describe('Import Action', () => {
|
|
749
|
+
it('calls onImport with parsed data when import button is clicked', async () => {
|
|
750
|
+
const user = userEvent.setup();
|
|
751
|
+
const onImport = vi.fn();
|
|
752
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
753
|
+
const file = createCSVFile(csvContent);
|
|
754
|
+
|
|
755
|
+
render(<ImportModal {...baseProps} onImport={onImport} />);
|
|
756
|
+
|
|
757
|
+
// Wait for file input to be available
|
|
758
|
+
const fileInput = await waitForFileInput();
|
|
759
|
+
await user.upload(fileInput, file);
|
|
760
|
+
|
|
761
|
+
// Wait for preview to appear
|
|
762
|
+
await waitFor(() => {
|
|
763
|
+
const table = screen.queryByRole('table') ||
|
|
764
|
+
document.querySelector('table') ||
|
|
765
|
+
document.querySelector('table.min-w-full');
|
|
766
|
+
expect(table).toBeInTheDocument();
|
|
767
|
+
}, { timeout: 10000 });
|
|
768
|
+
|
|
769
|
+
await waitFor(() => {
|
|
770
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
771
|
+
}, { timeout: 2000 });
|
|
772
|
+
|
|
773
|
+
const importButton = findButtonByText(/^import$/i);
|
|
774
|
+
expect(importButton).toBeInTheDocument();
|
|
775
|
+
if (importButton) {
|
|
776
|
+
await user.click(importButton);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
await waitFor(() => {
|
|
780
|
+
expect(onImport).toHaveBeenCalledTimes(1);
|
|
781
|
+
expect(onImport).toHaveBeenCalledWith(
|
|
782
|
+
expect.arrayContaining([
|
|
783
|
+
expect.objectContaining({
|
|
784
|
+
name: 'John',
|
|
785
|
+
email: 'john@example.com',
|
|
786
|
+
}),
|
|
787
|
+
])
|
|
788
|
+
);
|
|
789
|
+
}, { timeout: 3000 });
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('shows app-returned ImportSummary counts in summary section', async () => {
|
|
793
|
+
const user = userEvent.setup();
|
|
794
|
+
const onImport = vi.fn().mockResolvedValue({
|
|
795
|
+
successCount: 2,
|
|
796
|
+
totalCount: 3,
|
|
797
|
+
failedCount: 1,
|
|
798
|
+
failedRows: [{ row: 3, reason: 'Invalid meal type' }],
|
|
799
|
+
});
|
|
800
|
+
const csvContent = 'name,email\nAlice,a@x.com\nBob,b@x.com\nCarol,c@x.com';
|
|
801
|
+
const file = createCSVFile(csvContent);
|
|
802
|
+
|
|
803
|
+
render(<ImportModal {...baseProps} onImport={onImport} />);
|
|
804
|
+
|
|
805
|
+
const fileInput = await waitForFileInput();
|
|
806
|
+
await user.upload(fileInput, file);
|
|
807
|
+
|
|
808
|
+
await waitFor(() => {
|
|
809
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
810
|
+
expect(table).toBeInTheDocument();
|
|
811
|
+
}, { timeout: 10000 });
|
|
812
|
+
|
|
813
|
+
const importButton = findButtonByText(/^import$/i);
|
|
814
|
+
expect(importButton).toBeInTheDocument();
|
|
815
|
+
if (importButton) {
|
|
816
|
+
await user.click(importButton);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
await waitFor(() => {
|
|
820
|
+
expect(screen.getByText(/2 of 3.*imported successfully/i)).toBeInTheDocument();
|
|
821
|
+
}, { timeout: 5000 });
|
|
822
|
+
await waitFor(() => {
|
|
823
|
+
const failedTexts = screen.getAllByText(/1.*row.*failed to import/i);
|
|
824
|
+
expect(failedTexts.length).toBeGreaterThanOrEqual(1);
|
|
825
|
+
}, { timeout: 2000 });
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('disables import button when no file is selected', async () => {
|
|
829
|
+
const { rerender } = render(<ImportModal {...baseProps} />);
|
|
830
|
+
|
|
831
|
+
// Check if there's persistent state (file selected or summary) - if so, clear it first
|
|
832
|
+
await waitForDialog();
|
|
833
|
+
const selectedFileText = screen.queryByText(/selected:/i);
|
|
834
|
+
const hasSummary = screen.queryByText(/import completed/i);
|
|
835
|
+
|
|
836
|
+
if (selectedFileText || hasSummary) {
|
|
837
|
+
// Clear persistent state by closing and reopening
|
|
838
|
+
if (hasSummary) {
|
|
839
|
+
const closeButton = findButtonByText(/^close$/i);
|
|
840
|
+
if (closeButton) {
|
|
841
|
+
const user = userEvent.setup();
|
|
842
|
+
await user.click(closeButton);
|
|
843
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
// Close programmatically (onClose is just a mock, so we need to set isOpen=false)
|
|
847
|
+
rerender(<ImportModal {...baseProps} isOpen={false} />);
|
|
848
|
+
await waitFor(() => {
|
|
849
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
850
|
+
}, { timeout: 2000 });
|
|
851
|
+
|
|
852
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
853
|
+
rerender(<ImportModal {...baseProps} isOpen={true} />);
|
|
854
|
+
await waitForDialog();
|
|
855
|
+
// Verify state was cleared
|
|
856
|
+
await waitFor(() => {
|
|
857
|
+
const stillSelected = screen.queryByText(/selected:/i);
|
|
858
|
+
const stillHasSummary = screen.queryByText(/import completed/i);
|
|
859
|
+
if (stillSelected || stillHasSummary) {
|
|
860
|
+
throw new Error('State was not cleared');
|
|
861
|
+
}
|
|
862
|
+
}, { timeout: 2000 });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
866
|
+
// Use getByRole for heading to avoid multiple matches (h2 and button both have "Import Data")
|
|
867
|
+
await waitFor(() => {
|
|
868
|
+
try {
|
|
869
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
870
|
+
} catch (_e) {
|
|
871
|
+
// Fallback: check if any element with "Import Data" exists
|
|
872
|
+
const elements = screen.getAllByText('Import Data');
|
|
873
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
874
|
+
}
|
|
875
|
+
}, { timeout: 5000 });
|
|
876
|
+
|
|
877
|
+
// Then check for import button and its disabled state
|
|
878
|
+
await waitFor(() => {
|
|
879
|
+
const importButton = findButtonByText(/^import$/i);
|
|
880
|
+
expect(importButton).toBeInTheDocument();
|
|
881
|
+
expect(importButton).toBeDisabled();
|
|
882
|
+
}, { timeout: 5000 });
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('disables import button while processing', async () => {
|
|
886
|
+
const user = userEvent.setup();
|
|
887
|
+
const onImport = vi.fn(async () => {
|
|
888
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
889
|
+
});
|
|
890
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
891
|
+
const file = createCSVFile(csvContent);
|
|
892
|
+
|
|
893
|
+
const testProps = { ...baseProps, onImport };
|
|
894
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
895
|
+
|
|
896
|
+
// Wait for file input to be available (pass rerender and props to handle persistent state)
|
|
897
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
898
|
+
await user.upload(fileInput, file);
|
|
899
|
+
|
|
900
|
+
// Wait for preview to appear
|
|
901
|
+
await waitFor(() => {
|
|
902
|
+
const table = screen.queryByRole('table') ||
|
|
903
|
+
document.querySelector('table') ||
|
|
904
|
+
document.querySelector('table.min-w-full');
|
|
905
|
+
expect(table).toBeInTheDocument();
|
|
906
|
+
}, { timeout: 10000 });
|
|
907
|
+
|
|
908
|
+
await waitFor(() => {
|
|
909
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
910
|
+
}, { timeout: 2000 });
|
|
911
|
+
|
|
912
|
+
const importButton = findButtonByText(/^import$/i);
|
|
913
|
+
expect(importButton).toBeInTheDocument();
|
|
914
|
+
if (importButton) {
|
|
915
|
+
await user.click(importButton);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
await waitFor(() => {
|
|
919
|
+
expect(importButton).toBeDisabled();
|
|
920
|
+
}, { timeout: 1000 });
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it('shows processing text while importing', async () => {
|
|
924
|
+
const user = userEvent.setup();
|
|
925
|
+
const onImport = vi.fn(async () => {
|
|
926
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
927
|
+
});
|
|
928
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
929
|
+
const file = createCSVFile(csvContent);
|
|
930
|
+
|
|
931
|
+
const testProps = { ...baseProps, onImport };
|
|
932
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
933
|
+
|
|
934
|
+
// Wait for file input to be available (pass rerender and props to handle persistent state)
|
|
935
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
936
|
+
await user.upload(fileInput, file);
|
|
937
|
+
|
|
938
|
+
// Wait for preview to appear
|
|
939
|
+
await waitFor(() => {
|
|
940
|
+
const table = screen.queryByRole('table') ||
|
|
941
|
+
document.querySelector('table') ||
|
|
942
|
+
document.querySelector('table.min-w-full');
|
|
943
|
+
expect(table).toBeInTheDocument();
|
|
944
|
+
}, { timeout: 10000 });
|
|
945
|
+
|
|
946
|
+
await waitFor(() => {
|
|
947
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
948
|
+
}, { timeout: 2000 });
|
|
949
|
+
|
|
950
|
+
const importButton = findButtonByText(/^import$/i);
|
|
951
|
+
expect(importButton).toBeInTheDocument();
|
|
952
|
+
if (importButton) {
|
|
953
|
+
await user.click(importButton);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Button text changes to "Processing..." when isProcessing is true
|
|
957
|
+
await waitFor(() => {
|
|
958
|
+
const processingButton = findButtonByText(/processing/i);
|
|
959
|
+
expect(processingButton).toBeInTheDocument();
|
|
960
|
+
}, { timeout: 1000 });
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
it('calls onClose after successful import', async () => {
|
|
964
|
+
const user = userEvent.setup();
|
|
965
|
+
const onClose = vi.fn();
|
|
966
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
967
|
+
const file = createCSVFile(csvContent);
|
|
968
|
+
const onImport = vi.fn().mockResolvedValue({ successCount: 1, totalCount: 1, failedCount: 0 });
|
|
969
|
+
|
|
970
|
+
const testProps = { ...baseProps, onClose, onImport };
|
|
971
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
972
|
+
|
|
973
|
+
// Wait for file input to be available (pass rerender and props to handle persistent state)
|
|
974
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
975
|
+
await user.upload(fileInput, file);
|
|
976
|
+
|
|
977
|
+
// Wait for preview to appear
|
|
978
|
+
await waitFor(() => {
|
|
979
|
+
const table = screen.queryByRole('table') ||
|
|
980
|
+
document.querySelector('table') ||
|
|
981
|
+
document.querySelector('table.min-w-full');
|
|
982
|
+
expect(table).toBeInTheDocument();
|
|
983
|
+
}, { timeout: 10000 });
|
|
984
|
+
|
|
985
|
+
await waitFor(() => {
|
|
986
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
987
|
+
}, { timeout: 2000 });
|
|
988
|
+
|
|
989
|
+
const importButton = findButtonByText(/^import$/i);
|
|
990
|
+
expect(importButton).toBeInTheDocument();
|
|
991
|
+
if (importButton) {
|
|
992
|
+
await user.click(importButton);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Wait for import to complete and summary to appear
|
|
996
|
+
await waitFor(() => {
|
|
997
|
+
expect(screen.getByText(/import completed successfully/i)).toBeInTheDocument();
|
|
998
|
+
}, { timeout: 5000 });
|
|
999
|
+
|
|
1000
|
+
// Click the Close button to trigger onClose
|
|
1001
|
+
const closeButton = findButtonByText(/^close$/i);
|
|
1002
|
+
expect(closeButton).toBeInTheDocument();
|
|
1003
|
+
if (closeButton) {
|
|
1004
|
+
await user.click(closeButton);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Verify onClose was called
|
|
1008
|
+
expect(onClose).toHaveBeenCalled();
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
describe('Close Action', () => {
|
|
1013
|
+
it('calls onClose when cancel button is clicked', async () => {
|
|
1014
|
+
const user = userEvent.setup();
|
|
1015
|
+
const onClose = vi.fn();
|
|
1016
|
+
const { rerender } = render(<ImportModal {...baseProps} onClose={onClose} />);
|
|
1017
|
+
|
|
1018
|
+
// Wait for dialog title first (most reliable indicator dialog is rendered)
|
|
1019
|
+
await waitFor(() => {
|
|
1020
|
+
try {
|
|
1021
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
1022
|
+
} catch (_e) {
|
|
1023
|
+
const elements = screen.getAllByText('Import Data');
|
|
1024
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
1025
|
+
}
|
|
1026
|
+
}, { timeout: 5000 });
|
|
1027
|
+
|
|
1028
|
+
// Check if there's persistent state (summary or selected file) - if so, clear it first
|
|
1029
|
+
const hasSummary = screen.queryByText(/import completed/i);
|
|
1030
|
+
const hasSelectedFile = screen.queryByText(/selected:/i);
|
|
1031
|
+
if (hasSummary || hasSelectedFile) {
|
|
1032
|
+
// If there's a summary, click close button to trigger handleClose
|
|
1033
|
+
if (hasSummary) {
|
|
1034
|
+
const closeButton = findButtonByText(/^close$/i);
|
|
1035
|
+
if (closeButton) {
|
|
1036
|
+
await user.click(closeButton);
|
|
1037
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
// Close and reopen to clear state
|
|
1041
|
+
rerender(<ImportModal {...baseProps} isOpen={false} onClose={onClose} />);
|
|
1042
|
+
await waitFor(() => {
|
|
1043
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
1044
|
+
}, { timeout: 2000 });
|
|
1045
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1046
|
+
// Reset the mock call count since we called onClose when clearing state
|
|
1047
|
+
onClose.mockClear();
|
|
1048
|
+
rerender(<ImportModal {...baseProps} isOpen={true} onClose={onClose} />);
|
|
1049
|
+
await waitForDialog();
|
|
1050
|
+
await waitFor(() => {
|
|
1051
|
+
expect(screen.getByText('Import Data')).toBeInTheDocument();
|
|
1052
|
+
}, { timeout: 5000 });
|
|
1053
|
+
// Verify state was cleared
|
|
1054
|
+
await waitFor(() => {
|
|
1055
|
+
const stillHasSummary = screen.queryByText(/import completed/i);
|
|
1056
|
+
const stillHasSelected = screen.queryByText(/selected:/i);
|
|
1057
|
+
if (stillHasSummary || stillHasSelected) {
|
|
1058
|
+
throw new Error('State was not cleared');
|
|
1059
|
+
}
|
|
1060
|
+
}, { timeout: 2000 });
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Then wait for cancel button (should exist when no summary)
|
|
1064
|
+
await waitFor(() => {
|
|
1065
|
+
const cancelButton = findButtonByText(/cancel/i);
|
|
1066
|
+
expect(cancelButton).toBeInTheDocument();
|
|
1067
|
+
}, { timeout: 5000 });
|
|
1068
|
+
|
|
1069
|
+
const cancelButton = findButtonByText(/cancel/i);
|
|
1070
|
+
expect(cancelButton).toBeInTheDocument();
|
|
1071
|
+
if (cancelButton) {
|
|
1072
|
+
await user.click(cancelButton);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it('resets state when modal closes', async () => {
|
|
1079
|
+
const user = userEvent.setup();
|
|
1080
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
1081
|
+
const file = createCSVFile(csvContent);
|
|
1082
|
+
|
|
1083
|
+
const { rerender } = render(<ImportModal {...baseProps} />);
|
|
1084
|
+
|
|
1085
|
+
// Wait for file input to be available (pass rerender to handle persistent state)
|
|
1086
|
+
const fileInput = await waitForFileInput(rerender, baseProps);
|
|
1087
|
+
await user.upload(fileInput, file);
|
|
1088
|
+
|
|
1089
|
+
// Wait for preview to appear
|
|
1090
|
+
await waitFor(() => {
|
|
1091
|
+
const table = screen.queryByRole('table') ||
|
|
1092
|
+
document.querySelector('table') ||
|
|
1093
|
+
document.querySelector('table.min-w-full');
|
|
1094
|
+
expect(table).toBeInTheDocument();
|
|
1095
|
+
}, { timeout: 10000 });
|
|
1096
|
+
|
|
1097
|
+
await waitFor(() => {
|
|
1098
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
1099
|
+
}, { timeout: 2000 });
|
|
1100
|
+
|
|
1101
|
+
rerender(<ImportModal {...baseProps} isOpen={false} />);
|
|
1102
|
+
rerender(<ImportModal {...baseProps} isOpen={true} />);
|
|
1103
|
+
|
|
1104
|
+
// State should be reset - wait for modal to reopen
|
|
1105
|
+
await waitFor(() => {
|
|
1106
|
+
expect(screen.queryByText('John')).not.toBeInTheDocument();
|
|
1107
|
+
}, { timeout: 1000 });
|
|
1108
|
+
});
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
describe('Custom Configuration', () => {
|
|
1112
|
+
it('uses custom button texts from config', async () => {
|
|
1113
|
+
const testProps = {
|
|
1114
|
+
...baseProps,
|
|
1115
|
+
config: {
|
|
1116
|
+
selectFileButtonText: 'Browse Files',
|
|
1117
|
+
importButtonText: 'Import Data',
|
|
1118
|
+
cancelButtonText: 'Close',
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1122
|
+
|
|
1123
|
+
// Wait for dialog title first
|
|
1124
|
+
await waitFor(() => {
|
|
1125
|
+
try {
|
|
1126
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
1127
|
+
} catch (_e) {
|
|
1128
|
+
const elements = screen.getAllByText('Import Data');
|
|
1129
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
1130
|
+
}
|
|
1131
|
+
}, { timeout: 5000 });
|
|
1132
|
+
|
|
1133
|
+
// Check if there's a summary - if so, clear it first
|
|
1134
|
+
const hasSummary = screen.queryByText(/import completed/i);
|
|
1135
|
+
if (hasSummary) {
|
|
1136
|
+
rerender(
|
|
1137
|
+
<ImportModal
|
|
1138
|
+
{...baseProps}
|
|
1139
|
+
isOpen={false}
|
|
1140
|
+
config={{
|
|
1141
|
+
selectFileButtonText: 'Browse Files',
|
|
1142
|
+
importButtonText: 'Import Data',
|
|
1143
|
+
cancelButtonText: 'Close',
|
|
1144
|
+
}}
|
|
1145
|
+
/>
|
|
1146
|
+
);
|
|
1147
|
+
await waitFor(() => {
|
|
1148
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
1149
|
+
}, { timeout: 2000 });
|
|
1150
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1151
|
+
rerender(
|
|
1152
|
+
<ImportModal
|
|
1153
|
+
{...baseProps}
|
|
1154
|
+
isOpen={true}
|
|
1155
|
+
config={{
|
|
1156
|
+
selectFileButtonText: 'Browse Files',
|
|
1157
|
+
importButtonText: 'Import Data',
|
|
1158
|
+
cancelButtonText: 'Close',
|
|
1159
|
+
}}
|
|
1160
|
+
/>
|
|
1161
|
+
);
|
|
1162
|
+
await waitForDialog();
|
|
1163
|
+
await waitFor(() => {
|
|
1164
|
+
expect(screen.getByText('Import Data')).toBeInTheDocument();
|
|
1165
|
+
}, { timeout: 5000 });
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Then wait for buttons (should exist when no summary)
|
|
1169
|
+
await waitFor(() => {
|
|
1170
|
+
const browseButton = findButtonByText(/browse files/i);
|
|
1171
|
+
expect(browseButton).toBeInTheDocument();
|
|
1172
|
+
}, { timeout: 5000 });
|
|
1173
|
+
|
|
1174
|
+
await waitFor(() => {
|
|
1175
|
+
const importDataButton = findButtonByText(/import data/i);
|
|
1176
|
+
expect(importDataButton).toBeInTheDocument();
|
|
1177
|
+
}, { timeout: 5000 });
|
|
1178
|
+
// Dialog has a close button too, so find the Cancel one by text
|
|
1179
|
+
const cancelButton = findButtonByText(/^close$/i);
|
|
1180
|
+
expect(cancelButton).toBeInTheDocument();
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
it('uses custom preview header text from config', async () => {
|
|
1184
|
+
const user = userEvent.setup();
|
|
1185
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
1186
|
+
const file = createCSVFile(csvContent);
|
|
1187
|
+
|
|
1188
|
+
const testProps = { ...baseProps, config: { previewHeaderText: 'Data Preview' } };
|
|
1189
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1190
|
+
|
|
1191
|
+
// Wait for file input to be available (pass rerender and props to handle persistent state)
|
|
1192
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1193
|
+
await user.upload(fileInput, file);
|
|
1194
|
+
|
|
1195
|
+
// Wait for preview to appear
|
|
1196
|
+
await waitFor(() => {
|
|
1197
|
+
expect(screen.getByText('Data Preview')).toBeInTheDocument();
|
|
1198
|
+
}, { timeout: 5000 });
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
it('uses custom total rows text from config', async () => {
|
|
1202
|
+
const user = userEvent.setup();
|
|
1203
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com';
|
|
1204
|
+
const file = createCSVFile(csvContent);
|
|
1205
|
+
|
|
1206
|
+
const testProps = { ...baseProps, config: { totalRowsText: 'Found {count} records' } };
|
|
1207
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1208
|
+
|
|
1209
|
+
// Wait for file input to be available (pass rerender and props to handle persistent state)
|
|
1210
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1211
|
+
await user.upload(fileInput, file);
|
|
1212
|
+
|
|
1213
|
+
// Wait for preview to appear
|
|
1214
|
+
await waitFor(() => {
|
|
1215
|
+
const table = screen.queryByRole('table') ||
|
|
1216
|
+
document.querySelector('table') ||
|
|
1217
|
+
document.querySelector('table.min-w-full');
|
|
1218
|
+
expect(table).toBeInTheDocument();
|
|
1219
|
+
}, { timeout: 10000 });
|
|
1220
|
+
|
|
1221
|
+
await waitFor(() => {
|
|
1222
|
+
expect(screen.getByText(/found 2 records/i)).toBeInTheDocument();
|
|
1223
|
+
}, { timeout: 2000 });
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
describe('Import Progress Tracking', () => {
|
|
1228
|
+
it('shows parsing progress during CSV parsing', async () => {
|
|
1229
|
+
const user = userEvent.setup();
|
|
1230
|
+
const headers = 'name,email\n';
|
|
1231
|
+
const rows = Array.from({ length: 200 }, (_, i) => `User${i},user${i}@example.com`).join('\n');
|
|
1232
|
+
const csvContent = headers + rows;
|
|
1233
|
+
const file = createCSVFile(csvContent);
|
|
1234
|
+
const onImport = vi.fn(async () => {
|
|
1235
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
const testProps = { ...baseProps, onImport };
|
|
1239
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1240
|
+
|
|
1241
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1242
|
+
await user.upload(fileInput, file);
|
|
1243
|
+
|
|
1244
|
+
await waitFor(() => {
|
|
1245
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1246
|
+
expect(table).toBeInTheDocument();
|
|
1247
|
+
}, { timeout: 10000 });
|
|
1248
|
+
|
|
1249
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1250
|
+
expect(importButton).toBeInTheDocument();
|
|
1251
|
+
if (importButton && !importButton.disabled) {
|
|
1252
|
+
await user.click(importButton);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Should show parsing progress (may be very brief)
|
|
1256
|
+
await waitFor(() => {
|
|
1257
|
+
const parsingText = screen.queryByText(/parsing csv file|importing data/i);
|
|
1258
|
+
if (parsingText) {
|
|
1259
|
+
expect(parsingText).toBeInTheDocument();
|
|
1260
|
+
}
|
|
1261
|
+
}, { timeout: 5000 });
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it('shows importing progress during data import', async () => {
|
|
1265
|
+
const user = userEvent.setup();
|
|
1266
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com';
|
|
1267
|
+
const file = createCSVFile(csvContent);
|
|
1268
|
+
const onImport = vi.fn(async () => {
|
|
1269
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
const testProps = { ...baseProps, onImport };
|
|
1273
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1274
|
+
|
|
1275
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1276
|
+
await user.upload(fileInput, file);
|
|
1277
|
+
|
|
1278
|
+
await waitFor(() => {
|
|
1279
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1280
|
+
expect(table).toBeInTheDocument();
|
|
1281
|
+
}, { timeout: 10000 });
|
|
1282
|
+
|
|
1283
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1284
|
+
if (importButton) {
|
|
1285
|
+
await user.click(importButton);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Should show importing progress
|
|
1289
|
+
await waitFor(() => {
|
|
1290
|
+
expect(screen.getByText(/importing data/i)).toBeInTheDocument();
|
|
1291
|
+
}, { timeout: 2000 });
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
it('shows chunk progress when processing large datasets', async () => {
|
|
1295
|
+
const user = userEvent.setup();
|
|
1296
|
+
const headers = 'name,email\n';
|
|
1297
|
+
const rows = Array.from({ length: 250 }, (_, i) => `User${i},user${i}@example.com`).join('\n');
|
|
1298
|
+
const csvContent = headers + rows;
|
|
1299
|
+
const file = createCSVFile(csvContent);
|
|
1300
|
+
const onImport = vi.fn(async () => {
|
|
1301
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
const testProps = { ...baseProps, onImport };
|
|
1305
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1306
|
+
|
|
1307
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1308
|
+
await user.upload(fileInput, file);
|
|
1309
|
+
|
|
1310
|
+
await waitFor(() => {
|
|
1311
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1312
|
+
expect(table).toBeInTheDocument();
|
|
1313
|
+
}, { timeout: 10000 });
|
|
1314
|
+
|
|
1315
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1316
|
+
if (importButton) {
|
|
1317
|
+
await user.click(importButton);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Should show chunk progress
|
|
1321
|
+
await waitFor(() => {
|
|
1322
|
+
const progressText = screen.queryByText(/chunk \d+ of \d+/i);
|
|
1323
|
+
if (progressText) {
|
|
1324
|
+
expect(progressText).toBeInTheDocument();
|
|
1325
|
+
}
|
|
1326
|
+
}, { timeout: 3000 });
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
describe('Import Summary', () => {
|
|
1331
|
+
it('displays success summary when all rows imported successfully', async () => {
|
|
1332
|
+
const user = userEvent.setup();
|
|
1333
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com';
|
|
1334
|
+
const file = createCSVFile(csvContent);
|
|
1335
|
+
const onImport = vi.fn().mockResolvedValue({ successCount: 2, totalCount: 2, failedCount: 0 });
|
|
1336
|
+
|
|
1337
|
+
const testProps = { ...baseProps, onImport };
|
|
1338
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1339
|
+
|
|
1340
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1341
|
+
await user.upload(fileInput, file);
|
|
1342
|
+
|
|
1343
|
+
await waitFor(() => {
|
|
1344
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1345
|
+
expect(table).toBeInTheDocument();
|
|
1346
|
+
}, { timeout: 10000 });
|
|
1347
|
+
|
|
1348
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1349
|
+
if (importButton) {
|
|
1350
|
+
await user.click(importButton);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
await waitFor(() => {
|
|
1354
|
+
expect(screen.getByText(/import completed successfully/i)).toBeInTheDocument();
|
|
1355
|
+
expect(screen.getByText(/2 of 2.*imported/i)).toBeInTheDocument();
|
|
1356
|
+
}, { timeout: 5000 });
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
it('displays error summary when some rows fail', async () => {
|
|
1360
|
+
const user = userEvent.setup();
|
|
1361
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com';
|
|
1362
|
+
const file = createCSVFile(csvContent);
|
|
1363
|
+
let callCount = 0;
|
|
1364
|
+
const onImport = vi.fn(async () => {
|
|
1365
|
+
callCount++;
|
|
1366
|
+
if (callCount === 1) {
|
|
1367
|
+
throw new Error('Import failed for chunk');
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
const testProps = { ...baseProps, onImport };
|
|
1372
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1373
|
+
|
|
1374
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1375
|
+
await user.upload(fileInput, file);
|
|
1376
|
+
|
|
1377
|
+
await waitFor(() => {
|
|
1378
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1379
|
+
expect(table).toBeInTheDocument();
|
|
1380
|
+
}, { timeout: 10000 });
|
|
1381
|
+
|
|
1382
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1383
|
+
if (importButton) {
|
|
1384
|
+
await user.click(importButton);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
await waitFor(() => {
|
|
1388
|
+
expect(screen.getByText(/import completed with errors/i)).toBeInTheDocument();
|
|
1389
|
+
}, { timeout: 5000 });
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
it('displays failed rows table when failures occur', async () => {
|
|
1393
|
+
const user = userEvent.setup();
|
|
1394
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com';
|
|
1395
|
+
const file = createCSVFile(csvContent);
|
|
1396
|
+
const onImport = vi.fn().mockRejectedValue(new Error('Import failed'));
|
|
1397
|
+
|
|
1398
|
+
const testProps = { ...baseProps, onImport };
|
|
1399
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1400
|
+
|
|
1401
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1402
|
+
await user.upload(fileInput, file);
|
|
1403
|
+
|
|
1404
|
+
await waitFor(() => {
|
|
1405
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1406
|
+
expect(table).toBeInTheDocument();
|
|
1407
|
+
}, { timeout: 10000 });
|
|
1408
|
+
|
|
1409
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1410
|
+
if (importButton) {
|
|
1411
|
+
await user.click(importButton);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
await waitFor(() => {
|
|
1415
|
+
const failedRowsTable = screen.queryByText(/failed rows/i);
|
|
1416
|
+
if (failedRowsTable) {
|
|
1417
|
+
expect(failedRowsTable).toBeInTheDocument();
|
|
1418
|
+
}
|
|
1419
|
+
}, { timeout: 5000 });
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
it('limits failed rows display to 50 rows', async () => {
|
|
1423
|
+
const user = userEvent.setup();
|
|
1424
|
+
const headers = 'name,email\n';
|
|
1425
|
+
const rows = Array.from({ length: 100 }, (_, i) => `User${i},user${i}@example.com`).join('\n');
|
|
1426
|
+
const csvContent = headers + rows;
|
|
1427
|
+
const file = createCSVFile(csvContent);
|
|
1428
|
+
const onImport = vi.fn().mockRejectedValue(new Error('Import failed'));
|
|
1429
|
+
|
|
1430
|
+
const testProps = { ...baseProps, onImport };
|
|
1431
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1432
|
+
|
|
1433
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1434
|
+
await user.upload(fileInput, file);
|
|
1435
|
+
|
|
1436
|
+
await waitFor(() => {
|
|
1437
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1438
|
+
expect(table).toBeInTheDocument();
|
|
1439
|
+
}, { timeout: 10000 });
|
|
1440
|
+
|
|
1441
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1442
|
+
if (importButton) {
|
|
1443
|
+
await user.click(importButton);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
await waitFor(() => {
|
|
1447
|
+
const failedRowsText = screen.queryByText(/showing first \d+ of \d+ failed/i);
|
|
1448
|
+
if (failedRowsText) {
|
|
1449
|
+
expect(failedRowsText).toBeInTheDocument();
|
|
1450
|
+
}
|
|
1451
|
+
}, { timeout: 5000 });
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
describe('Chunk Processing', () => {
|
|
1456
|
+
it('processes data in chunks for large datasets', async () => {
|
|
1457
|
+
const user = userEvent.setup();
|
|
1458
|
+
const headers = 'name,email\n';
|
|
1459
|
+
const rows = Array.from({ length: 150 }, (_, i) => `User${i},user${i}@example.com`).join('\n');
|
|
1460
|
+
const csvContent = headers + rows;
|
|
1461
|
+
const file = createCSVFile(csvContent);
|
|
1462
|
+
const onImport = vi.fn().mockResolvedValue(undefined);
|
|
1463
|
+
|
|
1464
|
+
const testProps = { ...baseProps, onImport };
|
|
1465
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1466
|
+
|
|
1467
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1468
|
+
await user.upload(fileInput, file);
|
|
1469
|
+
|
|
1470
|
+
await waitFor(() => {
|
|
1471
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1472
|
+
expect(table).toBeInTheDocument();
|
|
1473
|
+
}, { timeout: 10000 });
|
|
1474
|
+
|
|
1475
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1476
|
+
if (importButton) {
|
|
1477
|
+
await user.click(importButton);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
await waitFor(() => {
|
|
1481
|
+
// Should be called multiple times for chunks
|
|
1482
|
+
expect(onImport).toHaveBeenCalled();
|
|
1483
|
+
}, { timeout: 5000 });
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
it('handles chunk processing errors gracefully', async () => {
|
|
1487
|
+
const user = userEvent.setup();
|
|
1488
|
+
const headers = 'name,email\n';
|
|
1489
|
+
const rows = Array.from({ length: 150 }, (_, i) => `User${i},user${i}@example.com`).join('\n');
|
|
1490
|
+
const csvContent = headers + rows;
|
|
1491
|
+
const file = createCSVFile(csvContent);
|
|
1492
|
+
let callCount = 0;
|
|
1493
|
+
const onImport = vi.fn(async () => {
|
|
1494
|
+
callCount++;
|
|
1495
|
+
if (callCount === 2) {
|
|
1496
|
+
throw new Error('Chunk processing failed');
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
const testProps = { ...baseProps, onImport };
|
|
1501
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1502
|
+
|
|
1503
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1504
|
+
await user.upload(fileInput, file);
|
|
1505
|
+
|
|
1506
|
+
await waitFor(() => {
|
|
1507
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1508
|
+
expect(table).toBeInTheDocument();
|
|
1509
|
+
}, { timeout: 10000 });
|
|
1510
|
+
|
|
1511
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1512
|
+
if (importButton) {
|
|
1513
|
+
await user.click(importButton);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
await waitFor(() => {
|
|
1517
|
+
// Should show summary with failures
|
|
1518
|
+
const summary = screen.queryByText(/import completed/i);
|
|
1519
|
+
expect(summary).toBeInTheDocument();
|
|
1520
|
+
}, { timeout: 5000 });
|
|
1521
|
+
});
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
describe('Edge Cases', () => {
|
|
1525
|
+
it('handles very large CSV files', async () => {
|
|
1526
|
+
const user = userEvent.setup();
|
|
1527
|
+
const headers = 'name,email\n';
|
|
1528
|
+
const rows = Array.from({ length: 1000 }, (_, i) => `User${i},user${i}@example.com`).join('\n');
|
|
1529
|
+
const csvContent = headers + rows;
|
|
1530
|
+
const file = createCSVFile(csvContent);
|
|
1531
|
+
|
|
1532
|
+
const { rerender } = render(<ImportModal {...baseProps} />);
|
|
1533
|
+
|
|
1534
|
+
// Wait for file input to be available (pass rerender to handle persistent state)
|
|
1535
|
+
const fileInput = await waitForFileInput(rerender, baseProps);
|
|
1536
|
+
await user.upload(fileInput, file);
|
|
1537
|
+
|
|
1538
|
+
await waitFor(() => {
|
|
1539
|
+
expect(screen.getByText(/total rows to import: 1000/i)).toBeInTheDocument();
|
|
1540
|
+
}, { timeout: 5000 });
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
it('handles CSV with only one data row', async () => {
|
|
1544
|
+
const user = userEvent.setup();
|
|
1545
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
1546
|
+
const file = createCSVFile(csvContent);
|
|
1547
|
+
|
|
1548
|
+
const { rerender } = render(<ImportModal {...baseProps} />);
|
|
1549
|
+
|
|
1550
|
+
const fileInput = await waitForFileInput(rerender, baseProps);
|
|
1551
|
+
await user.upload(fileInput, file);
|
|
1552
|
+
|
|
1553
|
+
await waitFor(() => {
|
|
1554
|
+
expect(screen.getByText(/total rows to import: 1/i)).toBeInTheDocument();
|
|
1555
|
+
}, { timeout: 5000 });
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
it('handles CSV with empty values', async () => {
|
|
1559
|
+
const user = userEvent.setup();
|
|
1560
|
+
const csvContent = 'name,email\nJohn,\n,jane@example.com';
|
|
1561
|
+
const file = createCSVFile(csvContent);
|
|
1562
|
+
|
|
1563
|
+
const { rerender } = render(<ImportModal {...baseProps} />);
|
|
1564
|
+
|
|
1565
|
+
const fileInput = await waitForFileInput(rerender, baseProps);
|
|
1566
|
+
await user.upload(fileInput, file);
|
|
1567
|
+
|
|
1568
|
+
await waitFor(() => {
|
|
1569
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1570
|
+
expect(table).toBeInTheDocument();
|
|
1571
|
+
}, { timeout: 10000 });
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
it('handles CSV with special characters in values', async () => {
|
|
1575
|
+
const user = userEvent.setup();
|
|
1576
|
+
const csvContent = 'name,email\n"John, Doe","john@example.com"\n"Jane; Smith","jane@example.com"';
|
|
1577
|
+
const file = createCSVFile(csvContent);
|
|
1578
|
+
|
|
1579
|
+
const { rerender } = render(<ImportModal {...baseProps} />);
|
|
1580
|
+
|
|
1581
|
+
const fileInput = await waitForFileInput(rerender, baseProps);
|
|
1582
|
+
await user.upload(fileInput, file);
|
|
1583
|
+
|
|
1584
|
+
await waitFor(() => {
|
|
1585
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1586
|
+
expect(table).toBeInTheDocument();
|
|
1587
|
+
}, { timeout: 10000 });
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
it('handles async onImport that returns Promise', async () => {
|
|
1591
|
+
const user = userEvent.setup();
|
|
1592
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
1593
|
+
const file = createCSVFile(csvContent);
|
|
1594
|
+
const onImport = vi.fn(() => Promise.resolve());
|
|
1595
|
+
|
|
1596
|
+
const testProps = { ...baseProps, onImport };
|
|
1597
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1598
|
+
|
|
1599
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1600
|
+
await user.upload(fileInput, file);
|
|
1601
|
+
|
|
1602
|
+
await waitFor(() => {
|
|
1603
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1604
|
+
expect(table).toBeInTheDocument();
|
|
1605
|
+
}, { timeout: 10000 });
|
|
1606
|
+
|
|
1607
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1608
|
+
if (importButton) {
|
|
1609
|
+
await user.click(importButton);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
await waitFor(() => {
|
|
1613
|
+
expect(onImport).toHaveBeenCalled();
|
|
1614
|
+
}, { timeout: 5000 });
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
it('prevents closing modal during processing', async () => {
|
|
1618
|
+
const user = userEvent.setup();
|
|
1619
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
1620
|
+
const file = createCSVFile(csvContent);
|
|
1621
|
+
const onImport = vi.fn(async () => {
|
|
1622
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
1623
|
+
});
|
|
1624
|
+
const onClose = vi.fn();
|
|
1625
|
+
|
|
1626
|
+
const testProps = { ...baseProps, onImport, onClose };
|
|
1627
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1628
|
+
|
|
1629
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1630
|
+
await user.upload(fileInput, file);
|
|
1631
|
+
|
|
1632
|
+
await waitFor(() => {
|
|
1633
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1634
|
+
expect(table).toBeInTheDocument();
|
|
1635
|
+
}, { timeout: 10000 });
|
|
1636
|
+
|
|
1637
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1638
|
+
if (importButton) {
|
|
1639
|
+
await user.click(importButton);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// Try to close modal during processing
|
|
1643
|
+
await waitFor(() => {
|
|
1644
|
+
const dialog = document.querySelector('dialog[role="dialog"]');
|
|
1645
|
+
if (dialog) {
|
|
1646
|
+
// Simulate escape key or outside click
|
|
1647
|
+
const event = new KeyboardEvent('keydown', { key: 'Escape' });
|
|
1648
|
+
dialog.dispatchEvent(event);
|
|
1649
|
+
}
|
|
1650
|
+
}, { timeout: 1000 });
|
|
1651
|
+
|
|
1652
|
+
// Modal should still be open
|
|
1653
|
+
await waitFor(() => {
|
|
1654
|
+
expect(screen.queryByText('Import Data')).toBeInTheDocument();
|
|
1655
|
+
}, { timeout: 1000 });
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
it('prevents closing modal when summary exists', async () => {
|
|
1659
|
+
const user = userEvent.setup();
|
|
1660
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
1661
|
+
const file = createCSVFile(csvContent);
|
|
1662
|
+
const onImport = vi.fn().mockResolvedValue(undefined);
|
|
1663
|
+
const onClose = vi.fn();
|
|
1664
|
+
|
|
1665
|
+
const testProps = { ...baseProps, onImport, onClose };
|
|
1666
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1667
|
+
|
|
1668
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1669
|
+
await user.upload(fileInput, file);
|
|
1670
|
+
|
|
1671
|
+
await waitFor(() => {
|
|
1672
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1673
|
+
expect(table).toBeInTheDocument();
|
|
1674
|
+
}, { timeout: 10000 });
|
|
1675
|
+
|
|
1676
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1677
|
+
if (importButton) {
|
|
1678
|
+
await user.click(importButton);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// Wait for summary to appear
|
|
1682
|
+
await waitFor(() => {
|
|
1683
|
+
expect(screen.getByText(/import completed/i)).toBeInTheDocument();
|
|
1684
|
+
}, { timeout: 5000 });
|
|
1685
|
+
|
|
1686
|
+
// Try to close modal - should be prevented
|
|
1687
|
+
const dialog = document.querySelector('dialog[role="dialog"]');
|
|
1688
|
+
if (dialog) {
|
|
1689
|
+
const event = new KeyboardEvent('keydown', { key: 'Escape' });
|
|
1690
|
+
dialog.dispatchEvent(event);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Modal should still be open
|
|
1694
|
+
await waitFor(() => {
|
|
1695
|
+
expect(screen.queryByText('Import Data')).toBeInTheDocument();
|
|
1696
|
+
}, { timeout: 1000 });
|
|
1697
|
+
});
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
describe('Full import flow and section content', () => {
|
|
1701
|
+
it('full flow renders and asserts File, Preview, Summary section content', async () => {
|
|
1702
|
+
const user = userEvent.setup();
|
|
1703
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com';
|
|
1704
|
+
const file = createCSVFile(csvContent);
|
|
1705
|
+
const onImport = vi.fn().mockResolvedValue({ successCount: 2, totalCount: 2, failedCount: 0 });
|
|
1706
|
+
|
|
1707
|
+
const testProps = { ...baseProps, onImport };
|
|
1708
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1709
|
+
|
|
1710
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1711
|
+
await user.upload(fileInput, file);
|
|
1712
|
+
|
|
1713
|
+
await waitFor(() => {
|
|
1714
|
+
expect(screen.getByText(/selected:/i)).toBeInTheDocument();
|
|
1715
|
+
}, { timeout: 3000 });
|
|
1716
|
+
const fileSection = document.body;
|
|
1717
|
+
expect(fileSection.textContent).toMatch(new RegExp(file.name, 'i'));
|
|
1718
|
+
|
|
1719
|
+
await waitFor(() => {
|
|
1720
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1721
|
+
expect(table).toBeInTheDocument();
|
|
1722
|
+
}, { timeout: 10000 });
|
|
1723
|
+
expect(screen.getByText(/total rows to import: 2/i)).toBeInTheDocument();
|
|
1724
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
1725
|
+
expect(screen.getByText('Jane')).toBeInTheDocument();
|
|
1726
|
+
|
|
1727
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1728
|
+
if (importButton) {
|
|
1729
|
+
await user.click(importButton);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
await waitFor(() => {
|
|
1733
|
+
expect(screen.getByText(/import completed successfully/i)).toBeInTheDocument();
|
|
1734
|
+
expect(screen.getByText(/2 of 2.*imported/i)).toBeInTheDocument();
|
|
1735
|
+
}, { timeout: 5000 });
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
it('full flow with import error renders Summary and FailedRows section content', async () => {
|
|
1739
|
+
const user = userEvent.setup();
|
|
1740
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com';
|
|
1741
|
+
const file = createCSVFile(csvContent);
|
|
1742
|
+
const onImport = vi.fn().mockRejectedValue(new Error('Import failed'));
|
|
1743
|
+
|
|
1744
|
+
const testProps = { ...baseProps, onImport };
|
|
1745
|
+
const { rerender } = render(<ImportModal {...testProps} />);
|
|
1746
|
+
|
|
1747
|
+
const fileInput = await waitForFileInput(rerender, testProps);
|
|
1748
|
+
await user.upload(fileInput, file);
|
|
1749
|
+
|
|
1750
|
+
await waitFor(() => {
|
|
1751
|
+
const table = screen.queryByRole('table') || document.querySelector('table');
|
|
1752
|
+
expect(table).toBeInTheDocument();
|
|
1753
|
+
}, { timeout: 10000 });
|
|
1754
|
+
|
|
1755
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1756
|
+
if (importButton) {
|
|
1757
|
+
await user.click(importButton);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
await waitFor(() => {
|
|
1761
|
+
expect(screen.getByText(/import completed with errors/i)).toBeInTheDocument();
|
|
1762
|
+
}, { timeout: 5000 });
|
|
1763
|
+
const failedRowsHeading = screen.queryByText(/failed rows/i);
|
|
1764
|
+
if (failedRowsHeading) {
|
|
1765
|
+
expect(failedRowsHeading).toBeInTheDocument();
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
describe('Accessibility', () => {
|
|
1771
|
+
it('provides accessible file input', async () => {
|
|
1772
|
+
const { rerender } = render(<ImportModal {...baseProps} />);
|
|
1773
|
+
|
|
1774
|
+
// Wait for file input to be available (pass rerender to handle persistent state)
|
|
1775
|
+
const fileInput = await waitForFileInput(rerender, baseProps);
|
|
1776
|
+
expect(fileInput).toBeInTheDocument();
|
|
1777
|
+
expect(fileInput).toHaveAttribute('type', 'file');
|
|
1778
|
+
expect(fileInput).toHaveAttribute('accept', '.csv');
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
it('provides accessible button labels', async () => {
|
|
1782
|
+
const { rerender } = render(<ImportModal {...baseProps} />);
|
|
1783
|
+
|
|
1784
|
+
// Wait for dialog title first
|
|
1785
|
+
await waitFor(() => {
|
|
1786
|
+
try {
|
|
1787
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
1788
|
+
} catch (_e) {
|
|
1789
|
+
const elements = screen.getAllByText('Import Data');
|
|
1790
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
1791
|
+
}
|
|
1792
|
+
}, { timeout: 5000 });
|
|
1793
|
+
|
|
1794
|
+
// Check if there's a summary or file already selected - if so, clear it first
|
|
1795
|
+
const hasSummary = screen.queryByText(/import completed/i);
|
|
1796
|
+
const hasSelectedFile = screen.queryByText(/selected:/i);
|
|
1797
|
+
if (hasSummary || hasSelectedFile) {
|
|
1798
|
+
rerender(<ImportModal {...baseProps} isOpen={false} />);
|
|
1799
|
+
await waitFor(() => {
|
|
1800
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
1801
|
+
}, { timeout: 2000 });
|
|
1802
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1803
|
+
rerender(<ImportModal {...baseProps} isOpen={true} />);
|
|
1804
|
+
await waitForDialog();
|
|
1805
|
+
await waitFor(() => {
|
|
1806
|
+
expect(screen.getByText('Import Data')).toBeInTheDocument();
|
|
1807
|
+
}, { timeout: 5000 });
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// Wait for file selection area to be rendered (default uploadText is "Choose a CSV file to upload")
|
|
1811
|
+
await waitFor(() => {
|
|
1812
|
+
const uploadText = screen.queryByText(/choose a csv file/i);
|
|
1813
|
+
expect(uploadText).toBeInTheDocument();
|
|
1814
|
+
}, { timeout: 5000 });
|
|
1815
|
+
|
|
1816
|
+
// Then wait for buttons (should exist when no summary and no file selected)
|
|
1817
|
+
await waitFor(() => {
|
|
1818
|
+
const selectFileButton = findButtonByText(/select file/i);
|
|
1819
|
+
expect(selectFileButton).toBeInTheDocument();
|
|
1820
|
+
}, { timeout: 5000 });
|
|
1821
|
+
|
|
1822
|
+
await waitFor(() => {
|
|
1823
|
+
const importButton = findButtonByText(/^import$/i);
|
|
1824
|
+
expect(importButton).toBeInTheDocument();
|
|
1825
|
+
}, { timeout: 5000 });
|
|
1826
|
+
|
|
1827
|
+
await waitFor(() => {
|
|
1828
|
+
const cancelButton = findButtonByText(/cancel/i);
|
|
1829
|
+
expect(cancelButton).toBeInTheDocument();
|
|
1830
|
+
}, { timeout: 5000 });
|
|
1831
|
+
});
|
|
1832
|
+
});
|
|
1833
|
+
});
|
|
1834
|
+
|