@jmruthers/pace-core 0.6.7 → 0.6.8

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.
Files changed (100) hide show
  1. package/audit-tool/00-dependencies.cjs +215 -9
  2. package/audit-tool/audits/02-project-structure.cjs +3 -18
  3. package/audit-tool/audits/03-architecture.cjs +34 -6
  4. package/audit-tool/audits/06-security-rbac.cjs +10 -0
  5. package/audit-tool/audits/07-api-tech-stack.cjs +55 -1
  6. package/audit-tool/index.cjs +23 -19
  7. package/audit-tool/utils/report-utils.cjs +141 -2
  8. package/dist/{DataTable-7PMH7XN7.js → DataTable-6RMSCQJ6.js} +5 -5
  9. package/dist/{PublicPageProvider-DlsCaR5v.d.ts → PublicPageProvider-CIGSujI2.d.ts} +14 -8
  10. package/dist/{UnifiedAuthProvider-ZT6TIGM7.js → UnifiedAuthProvider-7SNDOWYD.js} +2 -2
  11. package/dist/{api-Y4MQWOFW.js → api-7P7DI652.js} +1 -1
  12. package/dist/{chunk-L4XMVJKY.js → chunk-4DDCYDQ3.js} +8 -7
  13. package/dist/{chunk-ZKAWKYT4.js → chunk-5W2A3DRC.js} +2 -1
  14. package/dist/{chunk-VBCS3DUA.js → chunk-EF2UGZWY.js} +3 -3
  15. package/dist/{chunk-JGWDVX64.js → chunk-EURB7QFZ.js} +123 -53
  16. package/dist/{chunk-BM4CQ5P3.js → chunk-GS5672WG.js} +6 -6
  17. package/dist/{chunk-ZFYPMX46.js → chunk-LX6U42O3.js} +1 -1
  18. package/dist/{chunk-5X4QLXRG.js → chunk-MPBLMWVR.js} +5 -3
  19. package/dist/{chunk-Q7Q7V5NV.js → chunk-NKHKXPI4.js} +7 -7
  20. package/dist/{chunk-6F3IILHI.js → chunk-S6ZQKDY6.js} +1 -1
  21. package/dist/{chunk-FTCRZOG2.js → chunk-T5CVK4R3.js} +5 -5
  22. package/dist/{chunk-GHYHJTYV.js → chunk-Z2FNRKF3.js} +13 -13
  23. package/dist/components.d.ts +1 -1
  24. package/dist/components.js +12 -12
  25. package/dist/eslint-rules/rules/04-code-quality.cjs +66 -10
  26. package/dist/eslint-rules/rules/06-security-rbac.cjs +8 -3
  27. package/dist/eslint-rules/rules/07-api-tech-stack.cjs +190 -68
  28. package/dist/{functions-DHebl8-F.d.ts → functions-lBy5L2ry.d.ts} +1 -1
  29. package/dist/hooks.js +7 -7
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +15 -15
  32. package/dist/providers.js +2 -2
  33. package/dist/rbac/index.d.ts +1 -1
  34. package/dist/rbac/index.js +6 -6
  35. package/dist/theming/runtime.d.ts +48 -1
  36. package/dist/theming/runtime.js +1 -1
  37. package/dist/types.d.ts +2 -2
  38. package/dist/utils.js +1 -1
  39. package/docs/api/modules.md +63 -14
  40. package/docs/getting-started/dependencies.md +23 -0
  41. package/docs/implementation-guides/app-layout.md +1 -1
  42. package/docs/implementation-guides/data-tables.md +1 -1
  43. package/docs/standards/1-pace-core-compliance-standards.md +38 -1
  44. package/eslint-config-pace-core.cjs +30 -11
  45. package/package.json +45 -15
  46. package/scripts/eslint-audit.cjs +123 -0
  47. package/scripts/install-eslint-config.cjs +67 -2
  48. package/scripts/validate-dependencies.cjs +248 -0
  49. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +20 -8
  50. package/src/__tests__/templates/accessibility.test.template.tsx +1 -0
  51. package/src/components/AddressField/AddressField.tsx +26 -1
  52. package/src/components/Alert/Alert.test.tsx +86 -22
  53. package/src/components/Alert/Alert.tsx +19 -11
  54. package/src/components/Badge/Badge.tsx +1 -1
  55. package/src/components/Checkbox/Checkbox.test.tsx +2 -1
  56. package/src/components/ContextSelector/ContextSelector.tsx +39 -41
  57. package/src/components/DataTable/DataTable.tsx +1 -19
  58. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +6 -10
  59. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +18 -9
  60. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +3 -2
  61. package/src/components/DataTable/components/EmptyState.tsx +1 -1
  62. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +1 -1
  63. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +3 -3
  64. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +33 -29
  65. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -2
  66. package/src/components/FileUpload/FileUpload.test.tsx +22 -31
  67. package/src/components/FileUpload/FileUpload.tsx +29 -0
  68. package/src/components/NavigationMenu/NavigationMenu.test.tsx +48 -12
  69. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +9 -9
  70. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +30 -30
  71. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +4 -4
  72. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +7 -1
  73. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +8 -5
  74. package/src/hooks/__tests__/useFileUrl.unit.test.ts +4 -0
  75. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +3 -3
  76. package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +45 -8
  77. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +22 -2
  78. package/src/hooks/public/usePublicRouteParams.ts +8 -4
  79. package/src/hooks/useAddressAutocomplete.test.ts +18 -18
  80. package/src/hooks/useEventTheme.ts +5 -1
  81. package/src/hooks/useFileUrl.ts +52 -8
  82. package/src/hooks/useOrganisationSecurity.test.ts +2 -1
  83. package/src/providers/__tests__/ProviderLifecycle.test.tsx +1 -1
  84. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +15 -6
  85. package/src/rbac/__tests__/rbac-functions.test.ts +3 -3
  86. package/src/rbac/api.test.ts +104 -0
  87. package/src/rbac/engine.ts +1 -1
  88. package/src/rbac/hooks/useCan.test.ts +2 -2
  89. package/src/rbac/secureClient.ts +1 -1
  90. package/src/rbac/types/functions.ts +1 -1
  91. package/src/theming/__tests__/parseEventColours.test.ts +117 -8
  92. package/src/theming/parseEventColours.ts +56 -2
  93. package/src/types/supabase.ts +2 -3
  94. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +9 -9
  95. package/src/utils/file-reference/__tests__/file-reference.test.ts +4 -0
  96. package/src/utils/formatting/formatDate.test.ts +3 -2
  97. package/src/utils/formatting/formatDateTime.test.ts +2 -2
  98. package/src/utils/google-places/googlePlacesUtils.test.ts +36 -24
  99. package/src/utils/storage/__tests__/helpers.unit.test.ts +19 -12
  100. package/src/utils/storage/helpers.test.ts +69 -3
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ESLint Audit Script
5
+ * @package pace-core
6
+ * @module Scripts/eslint-audit
7
+ *
8
+ * Runs ESLint across the entire repository and outputs results to a timestamped file
9
+ * in the audit directory.
10
+ *
11
+ * Usage:
12
+ * npm run audit:eslint
13
+ */
14
+
15
+ const { execSync } = require('child_process');
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ // ANSI color codes for terminal output
20
+ const colors = {
21
+ reset: '\x1b[0m',
22
+ green: '\x1b[32m',
23
+ yellow: '\x1b[33m',
24
+ blue: '\x1b[34m',
25
+ cyan: '\x1b[36m',
26
+ bold: '\x1b[1m',
27
+ red: '\x1b[31m'
28
+ };
29
+
30
+ // Generate timestamp in format yyyymmddHHMM
31
+ function generateTimestamp() {
32
+ const now = new Date();
33
+ const year = now.getFullYear();
34
+ const month = String(now.getMonth() + 1).padStart(2, '0');
35
+ const day = String(now.getDate()).padStart(2, '0');
36
+ const hours = String(now.getHours()).padStart(2, '0');
37
+ const minutes = String(now.getMinutes()).padStart(2, '0');
38
+ return `${year}${month}${day}${hours}${minutes}`;
39
+ }
40
+
41
+ // Main execution
42
+ function main() {
43
+ const cwd = process.cwd();
44
+ const auditDir = path.join(cwd, 'audit');
45
+ const timestamp = generateTimestamp();
46
+ const outputFile = path.join(auditDir, `${timestamp}-eslint-report.txt`);
47
+
48
+ // Create audit directory if it doesn't exist
49
+ if (!fs.existsSync(auditDir)) {
50
+ fs.mkdirSync(auditDir, { recursive: true });
51
+ console.log(`${colors.green}Created audit/ directory${colors.reset}`);
52
+ }
53
+
54
+ console.log(`${colors.cyan}Running ESLint audit across the repository...${colors.reset}\n`);
55
+
56
+ try {
57
+ // Run ESLint and capture output
58
+ const eslintCommand = 'npm run lint 2>&1';
59
+ const output = execSync(eslintCommand, {
60
+ encoding: 'utf8',
61
+ cwd,
62
+ stdio: 'pipe'
63
+ });
64
+
65
+ // Write output to file
66
+ const reportContent = `ESLint Audit Report
67
+ Generated: ${new Date().toISOString()}
68
+ Timestamp: ${timestamp}
69
+
70
+ ${output}
71
+ `;
72
+
73
+ fs.writeFileSync(outputFile, reportContent, 'utf8');
74
+
75
+ // Check if there are any errors
76
+ const hasErrors = output.includes('error') || output.includes('✖');
77
+ const hasWarnings = output.includes('warning') || output.includes('⚠');
78
+
79
+ console.log(`${colors.bold}Audit Summary:${colors.reset}`);
80
+ if (hasErrors) {
81
+ console.log(` ${colors.red}Errors found${colors.reset} - See report for details`);
82
+ } else if (hasWarnings) {
83
+ console.log(` ${colors.yellow}Warnings found${colors.reset} - See report for details`);
84
+ } else {
85
+ console.log(` ${colors.green}No errors or warnings${colors.reset}`);
86
+ }
87
+
88
+ console.log(`\n${colors.cyan}Report saved to:${colors.reset} ${outputFile}`);
89
+ console.log(`\n${colors.cyan}To view the report:${colors.reset}`);
90
+ console.log(` ${colors.bold}cat ${outputFile}${colors.reset}`);
91
+
92
+ } catch (error) {
93
+ // ESLint may exit with non-zero code if errors are found
94
+ // Capture the output anyway
95
+ const output = error.stdout || error.message || String(error);
96
+
97
+ const reportContent = `ESLint Audit Report
98
+ Generated: ${new Date().toISOString()}
99
+ Timestamp: ${timestamp}
100
+
101
+ ${output}
102
+
103
+ Exit Code: ${error.status || 1}
104
+ `;
105
+
106
+ fs.writeFileSync(outputFile, reportContent, 'utf8');
107
+
108
+ console.log(`${colors.yellow}ESLint found issues (exit code: ${error.status || 1})${colors.reset}`);
109
+ console.log(`\n${colors.cyan}Report saved to:${colors.reset} ${outputFile}`);
110
+ console.log(`\n${colors.cyan}To view the report:${colors.reset}`);
111
+ console.log(` ${colors.bold}cat ${outputFile}${colors.reset}`);
112
+
113
+ // Don't exit with error - we've captured the report
114
+ process.exit(0);
115
+ }
116
+ }
117
+
118
+ // Run if called directly
119
+ if (require.main === module) {
120
+ main();
121
+ }
122
+
123
+ module.exports = { main, generateTimestamp };
@@ -96,6 +96,43 @@ function isESModule(filePath, content) {
96
96
  return false;
97
97
  }
98
98
 
99
+ // Detect if config uses tseslint.config() wrapper
100
+ function usesTseslintConfig(content) {
101
+ return /tseslint\.config\s*\(/.test(content);
102
+ }
103
+
104
+ // Validate ESLint config structure
105
+ function validateConfig(configPath, content) {
106
+ try {
107
+ // Basic validation - check for common syntax errors
108
+ // Check for balanced brackets
109
+ const openBrackets = (content.match(/\[/g) || []).length;
110
+ const closeBrackets = (content.match(/\]/g) || []).length;
111
+ if (openBrackets !== closeBrackets) {
112
+ return { valid: false, error: `Unbalanced brackets: ${openBrackets} open, ${closeBrackets} close` };
113
+ }
114
+
115
+ // Check for balanced parentheses
116
+ const openParens = (content.match(/\(/g) || []).length;
117
+ const closeParens = (content.match(/\)/g) || []).length;
118
+ if (openParens !== closeParens) {
119
+ return { valid: false, error: `Unbalanced parentheses: ${openParens} open, ${closeParens} close` };
120
+ }
121
+
122
+ // Check that paceCoreConfig is spread correctly
123
+ if (content.includes('...paceCoreConfig')) {
124
+ // Should be spread in an array or tseslint.config()
125
+ if (!content.includes('[...paceCoreConfig') && !content.includes('tseslint.config(...paceCoreConfig')) {
126
+ return { valid: false, error: 'paceCoreConfig must be spread in an array or tseslint.config()' };
127
+ }
128
+ }
129
+
130
+ return { valid: true };
131
+ } catch (error) {
132
+ return { valid: false, error: error.message };
133
+ }
134
+ }
135
+
99
136
  // Backup file before modification
100
137
  function backupFile(filePath) {
101
138
  const backupPath = `${filePath}.backup.${Date.now()}`;
@@ -151,8 +188,27 @@ function setupESLintConfig(force = false) {
151
188
  }
152
189
  }
153
190
 
154
- // Add to export default array
155
- if (content.includes('export default [')) {
191
+ // Handle tseslint.config() wrapper
192
+ if (usesTseslintConfig(content)) {
193
+ // Config uses tseslint.config() - spread paceCoreConfig as first argument
194
+ if (!content.includes('...paceCoreConfig')) {
195
+ // Find tseslint.config( and add paceCoreConfig as first argument
196
+ // Handle both cases: tseslint.config(...) and tseslint.config(\n...)
197
+ if (content.match(/tseslint\.config\s*\(\s*[^\n]/)) {
198
+ // Has content on same line, add after opening paren
199
+ content = content.replace(
200
+ /(tseslint\.config\s*\()\s*/,
201
+ '$1\n ...paceCoreConfig,\n '
202
+ );
203
+ } else {
204
+ // Has content on new line, add as first line
205
+ content = content.replace(
206
+ /(tseslint\.config\s*\(\s*\n)\s*/,
207
+ '$1 ...paceCoreConfig,\n '
208
+ );
209
+ }
210
+ }
211
+ } else if (content.includes('export default [')) {
156
212
  // Already an array, add paceCoreConfig at the beginning
157
213
  if (!content.includes('...paceCoreConfig')) {
158
214
  content = content.replace(
@@ -206,6 +262,15 @@ function setupESLintConfig(force = false) {
206
262
  }
207
263
  }
208
264
 
265
+ // Validate config before writing
266
+ const validation = validateConfig(existingConfig.path, content);
267
+ if (!validation.valid) {
268
+ console.error(`${colors.red}✗${colors.reset} Config validation failed: ${validation.error}`);
269
+ console.error(`${colors.yellow} Restoring backup...${colors.reset}`);
270
+ fs.copyFileSync(backupPath, existingConfig.path);
271
+ throw new Error(`Invalid ESLint config structure: ${validation.error}`);
272
+ }
273
+
209
274
  // Write updated config
210
275
  fs.writeFileSync(existingConfig.path, content, 'utf8');
211
276
  console.log(`${colors.green}✓${colors.reset} Updated ${existingConfig.name}`);
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Dependency Validation Script
5
+ * @package @jmruthers/pace-core
6
+ * @module Scripts/ValidateDependencies
7
+ *
8
+ * Validates consistency between package.json, audit tool, and documentation.
9
+ * Ensures package.json is the single source of truth for dependencies.
10
+ *
11
+ * Checks:
12
+ * - All peerDependencies have corresponding entries in peerDependenciesMeta
13
+ * - No peer dependency is also in dependencies
14
+ * - Required peers (not optional) are correctly marked
15
+ * - Audit tool can read package.json correctly
16
+ * - No hardcoded dependency lists exist in audit tool
17
+ *
18
+ * Usage:
19
+ * node scripts/validate-dependencies.cjs
20
+ * npm run validate:dependencies
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+
26
+ // Colors for terminal output
27
+ const colors = {
28
+ reset: '\x1b[0m',
29
+ red: '\x1b[31m',
30
+ green: '\x1b[32m',
31
+ yellow: '\x1b[33m',
32
+ blue: '\x1b[34m',
33
+ cyan: '\x1b[36m',
34
+ bold: '\x1b[1m',
35
+ };
36
+
37
+ // Get package.json path
38
+ const packageJsonPath = path.resolve(__dirname, '../package.json');
39
+ const auditToolPath = path.resolve(__dirname, '../audit-tool/00-dependencies.cjs');
40
+
41
+ let hasErrors = false;
42
+ let hasWarnings = false;
43
+
44
+ /**
45
+ * Check package.json structure
46
+ */
47
+ function validatePackageJson() {
48
+ console.log(`${colors.blue}Validating package.json structure...${colors.reset}`);
49
+
50
+ if (!fs.existsSync(packageJsonPath)) {
51
+ console.error(`${colors.red}Error: package.json not found at ${packageJsonPath}${colors.reset}`);
52
+ return false;
53
+ }
54
+
55
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
56
+ const peerDeps = pkg.peerDependencies || {};
57
+ const peerMeta = pkg.peerDependenciesMeta || {};
58
+ const dependencies = pkg.dependencies || {};
59
+
60
+ let valid = true;
61
+
62
+ // Check 1: All OPTIONAL peerDependencies must have entries in peerDependenciesMeta
63
+ // Required peers (not optional) should NOT be in peerDependenciesMeta
64
+ // Only optional peers need to be explicitly marked
65
+ const optionalPeersInMeta = Object.keys(peerMeta).filter(dep => peerMeta[dep]?.optional);
66
+ const optionalPeersNotInMeta = Object.keys(peerDeps).filter(dep => {
67
+ // If it's in dependencies, it shouldn't be a peer at all (handled by check 2)
68
+ if (dependencies[dep]) return false;
69
+ // If it's marked as optional in meta, it's fine
70
+ if (peerMeta[dep]?.optional) return false;
71
+ // If it's not in meta and not in dependencies, we need to check if it should be optional
72
+ // For now, we'll only warn if a peer is in meta but not marked as optional
73
+ return false;
74
+ });
75
+
76
+ // Check for peers that are marked as optional in meta but should be required
77
+ // This is a warning, not an error, as the audit tool will treat them correctly
78
+ const requiredPeersInMeta = Object.keys(peerMeta).filter(
79
+ dep => peerDeps[dep] && !peerMeta[dep]?.optional
80
+ );
81
+ if (requiredPeersInMeta.length > 0) {
82
+ console.warn(`${colors.yellow}⚠️ Warning: The following required peers are in peerDependenciesMeta (they don't need to be):${colors.reset}`);
83
+ requiredPeersInMeta.forEach(dep => {
84
+ console.warn(` - ${colors.yellow}${dep}${colors.reset}`);
85
+ });
86
+ console.warn(`${colors.yellow} → Required peers don't need to be in peerDependenciesMeta (only optional ones do)${colors.reset}`);
87
+ hasWarnings = true;
88
+ }
89
+
90
+ // Check 2: No peer dependency is also in dependencies
91
+ const duplicateDeps = Object.keys(peerDeps).filter(dep => dependencies[dep]);
92
+ if (duplicateDeps.length > 0) {
93
+ console.error(`${colors.red}❌ Error: The following packages appear in both peerDependencies and dependencies:${colors.reset}`);
94
+ duplicateDeps.forEach(dep => {
95
+ console.error(` - ${colors.red}${dep}${colors.reset}`);
96
+ });
97
+ console.error(`${colors.yellow} → Remove them from peerDependencies (they're already included)${colors.reset}`);
98
+ valid = false;
99
+ hasErrors = true;
100
+ }
101
+
102
+ // Check 3: Verify no required peers are marked as optional
103
+ // Required peers are those NOT in peerDependenciesMeta (or marked as optional: false)
104
+ const requiredPeers = Object.keys(peerDeps).filter(dep => {
105
+ // If it's in dependencies, it's not a peer (handled by check 2)
106
+ if (dependencies[dep]) return false;
107
+ // If it's not in meta, it's required by default
108
+ if (!peerMeta[dep]) return true;
109
+ // If it's in meta and marked as optional: false, it's required
110
+ if (peerMeta[dep]?.optional === false) return true;
111
+ // If it's in meta and marked as optional: true, it's optional
112
+ return false;
113
+ });
114
+
115
+ const incorrectlyOptional = requiredPeers.filter(dep => peerMeta[dep]?.optional === true);
116
+ if (incorrectlyOptional.length > 0) {
117
+ console.error(`${colors.red}❌ Error: The following required peers are incorrectly marked as optional:${colors.reset}`);
118
+ incorrectlyOptional.forEach(dep => {
119
+ console.error(` - ${colors.red}${dep}${colors.reset}`);
120
+ });
121
+ console.error(`${colors.yellow} → Remove "optional": true from peerDependenciesMeta for these packages (or remove them from meta entirely)${colors.reset}`);
122
+ valid = false;
123
+ hasErrors = true;
124
+ }
125
+
126
+ // Check 4: Warn about extra entries in peerDependenciesMeta that don't exist in peerDependencies
127
+ const extraMeta = Object.keys(peerMeta).filter(dep => !peerDeps[dep]);
128
+ if (extraMeta.length > 0) {
129
+ console.warn(`${colors.yellow}⚠️ Warning: The following entries in peerDependenciesMeta don't exist in peerDependencies:${colors.reset}`);
130
+ extraMeta.forEach(dep => {
131
+ console.warn(` - ${colors.yellow}${dep}${colors.reset}`);
132
+ });
133
+ console.warn(`${colors.yellow} → Remove them from peerDependenciesMeta or add them to peerDependencies${colors.reset}`);
134
+ hasWarnings = true;
135
+ }
136
+
137
+ if (valid && duplicateDeps.length === 0) {
138
+ console.log(`${colors.green}✅ package.json structure is valid${colors.reset}`);
139
+ }
140
+
141
+ return valid;
142
+ }
143
+
144
+ /**
145
+ * Check audit tool consistency
146
+ */
147
+ function validateAuditTool() {
148
+ console.log(`${colors.blue}Validating audit tool consistency...${colors.reset}`);
149
+
150
+ if (!fs.existsSync(auditToolPath)) {
151
+ console.error(`${colors.red}Error: Audit tool not found at ${auditToolPath}${colors.reset}`);
152
+ return false;
153
+ }
154
+
155
+ const auditToolContent = fs.readFileSync(auditToolPath, 'utf8');
156
+ let valid = true;
157
+
158
+ // Check 1: Verify no hardcoded REQUIRED_PEERS array
159
+ const hardcodedPattern = /const\s+REQUIRED_PEERS\s*=\s*\[['"][^'"]+['"]/;
160
+ if (hardcodedPattern.test(auditToolContent)) {
161
+ console.error(`${colors.red}❌ Error: Found hardcoded REQUIRED_PEERS array in audit tool${colors.reset}`);
162
+ console.error(`${colors.yellow} → The audit tool should read required/optional peers from peerDependenciesMeta${colors.reset}`);
163
+ valid = false;
164
+ hasErrors = true;
165
+ }
166
+
167
+ // Check 2: Verify audit tool uses peerDependenciesMeta
168
+ if (!auditToolContent.includes('peerDependenciesMeta')) {
169
+ console.error(`${colors.red}❌ Error: Audit tool does not reference peerDependenciesMeta${colors.reset}`);
170
+ console.error(`${colors.yellow} → The audit tool should read from peerDependenciesMeta to determine required vs optional peers${colors.reset}`);
171
+ valid = false;
172
+ hasErrors = true;
173
+ }
174
+
175
+ // Check 3: Verify audit tool can read package.json
176
+ if (!auditToolContent.includes('findPaceCorePackageJson')) {
177
+ console.warn(`${colors.yellow}⚠️ Warning: Audit tool may not be able to find pace-core package.json${colors.reset}`);
178
+ hasWarnings = true;
179
+ }
180
+
181
+ // Check 4: Try to load and test the audit tool
182
+ try {
183
+ const { runDependencyAudit } = require(auditToolPath);
184
+ if (typeof runDependencyAudit !== 'function') {
185
+ console.error(`${colors.red}❌ Error: Audit tool does not export runDependencyAudit function${colors.reset}`);
186
+ valid = false;
187
+ hasErrors = true;
188
+ } else {
189
+ // Test that it can read package.json (use current directory as test)
190
+ const testResult = runDependencyAudit(path.resolve(__dirname, '../..'));
191
+ if (testResult.error && testResult.error.includes('package.json')) {
192
+ console.warn(`${colors.yellow}⚠️ Warning: Audit tool had issues reading package.json: ${testResult.error}${colors.reset}`);
193
+ hasWarnings = true;
194
+ }
195
+ }
196
+ } catch (error) {
197
+ console.error(`${colors.red}❌ Error: Could not load audit tool: ${error.message}${colors.reset}`);
198
+ valid = false;
199
+ hasErrors = true;
200
+ }
201
+
202
+ if (valid) {
203
+ console.log(`${colors.green}✅ Audit tool is consistent with package.json${colors.reset}`);
204
+ }
205
+
206
+ return valid;
207
+ }
208
+
209
+ /**
210
+ * Main validation function
211
+ */
212
+ function main() {
213
+ console.log(`${colors.bold}${colors.cyan}Dependency Validation${colors.reset}`);
214
+ console.log(`${colors.cyan}${'='.repeat(50)}${colors.reset}\n`);
215
+
216
+ const packageJsonValid = validatePackageJson();
217
+ console.log();
218
+ const auditToolValid = validateAuditTool();
219
+ console.log();
220
+
221
+ // Summary
222
+ console.log(`${colors.bold}Summary:${colors.reset}\n`);
223
+
224
+ if (hasErrors) {
225
+ console.log(`${colors.red}❌ Validation failed with errors${colors.reset}`);
226
+ console.log(`${colors.yellow}Fix the errors above before publishing${colors.reset}\n`);
227
+ process.exit(1);
228
+ } else if (hasWarnings) {
229
+ console.log(`${colors.yellow}⚠️ Validation passed with warnings${colors.reset}`);
230
+ console.log(`${colors.yellow}Review the warnings above${colors.reset}\n`);
231
+ process.exit(0);
232
+ } else {
233
+ console.log(`${colors.green}✅ All dependency validations passed!${colors.reset}\n`);
234
+ process.exit(0);
235
+ }
236
+ }
237
+
238
+ // Run if called directly
239
+ if (require.main === module) {
240
+ main();
241
+ }
242
+
243
+ // Export for use by other scripts
244
+ module.exports = {
245
+ validatePackageJson,
246
+ validateAuditTool,
247
+ main,
248
+ };
@@ -405,14 +405,20 @@ describe('[helpers] createComponentTestStructure', () => {
405
405
  expect(typeof structure.describe).toBe('function');
406
406
  });
407
407
 
408
- it('executes test function within describe block', () => {
408
+ it('provides describe function that can be called', () => {
409
409
  const testFn = vi.fn();
410
410
  const structure = createComponentTestStructure('TestComponent');
411
411
 
412
- structure.describe(testFn);
412
+ // Verify the describe function exists and is callable
413
+ expect(typeof structure.describe).toBe('function');
413
414
 
414
- // The test function should be callable
415
- expect(typeof testFn).toBe('function');
415
+ // Verify we can call it (but don't actually execute describe inside a test)
416
+ // The actual describe block execution should happen at the top level
417
+ expect(() => {
418
+ // Just verify the function signature is correct
419
+ const fn = structure.describe;
420
+ expect(fn).toBeDefined();
421
+ }).not.toThrow();
416
422
  });
417
423
  });
418
424
 
@@ -424,13 +430,19 @@ describe('[helpers] createHookTestStructure', () => {
424
430
  expect(typeof structure.describe).toBe('function');
425
431
  });
426
432
 
427
- it('executes test function within describe block', () => {
433
+ it('provides describe function that can be called', () => {
428
434
  const testFn = vi.fn();
429
435
  const structure = createHookTestStructure('useTestHook');
430
436
 
431
- structure.describe(testFn);
437
+ // Verify the describe function exists and is callable
438
+ expect(typeof structure.describe).toBe('function');
432
439
 
433
- // The test function should be callable
434
- expect(typeof testFn).toBe('function');
440
+ // Verify we can call it (but don't actually execute describe inside a test)
441
+ // The actual describe block execution should happen at the top level
442
+ expect(() => {
443
+ // Just verify the function signature is correct
444
+ const fn = structure.describe;
445
+ expect(fn).toBeDefined();
446
+ }).not.toThrow();
435
447
  });
436
448
  });
@@ -40,6 +40,7 @@ describe('ComponentName Accessibility Tests', () => {
40
40
  it('has proper heading hierarchy', () => {
41
41
  renderWithProviders(<ComponentName title="Main Title" subtitle="Subtitle" />);
42
42
 
43
+ // Note: Heading levels can skip when semantically appropriate (e.g., h2 → h5)
43
44
  const mainHeading = screen.getByRole('heading', { level: 1 });
44
45
  const subHeading = screen.getByRole('heading', { level: 2 });
45
46
 
@@ -83,6 +83,7 @@ const AddressField = React.forwardRef<HTMLInputElement, AddressFieldProps>(
83
83
  const inputRef = React.useRef<HTMLInputElement>(null);
84
84
  const suggestionsRef = React.useRef<HTMLDListElement>(null);
85
85
  const containerRef = React.useRef<HTMLFormElement>(null);
86
+ const blurTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
86
87
 
87
88
  // Use controlled or uncontrolled value
88
89
  const value = controlledValue !== undefined ? controlledValue : internalValue;
@@ -194,17 +195,41 @@ const AddressField = React.forwardRef<HTMLInputElement, AddressFieldProps>(
194
195
  // Handle blur
195
196
  const handleBlur = React.useCallback(
196
197
  (e: React.FocusEvent<HTMLInputElement>) => {
198
+ // Clear any existing timeout
199
+ if (blurTimeoutRef.current) {
200
+ clearTimeout(blurTimeoutRef.current);
201
+ blurTimeoutRef.current = null;
202
+ }
203
+
197
204
  // Delay to allow click events on suggestions
198
- setTimeout(() => {
205
+ blurTimeoutRef.current = setTimeout(() => {
206
+ // Check if window/document is available (for test environments)
207
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
208
+ blurTimeoutRef.current = null;
209
+ return;
210
+ }
211
+
212
+ // Close if focus moved outside the container
199
213
  if (!containerRef.current?.contains(document.activeElement)) {
200
214
  setInputFocused(false);
201
215
  setIsOpen(false);
202
216
  setSelectedIndex(-1);
203
217
  }
218
+ blurTimeoutRef.current = null;
204
219
  }, 200);
205
220
  },
206
221
  []
207
222
  );
223
+
224
+ // Cleanup timeout on unmount
225
+ React.useEffect(() => {
226
+ return () => {
227
+ if (blurTimeoutRef.current) {
228
+ clearTimeout(blurTimeoutRef.current);
229
+ blurTimeoutRef.current = null;
230
+ }
231
+ };
232
+ }, []);
208
233
 
209
234
  // Click outside handler
210
235
  React.useEffect(() => {