@pixelated-tech/components 3.8.0 → 3.9.0

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 (217) hide show
  1. package/README.md +7 -0
  2. package/dist/components/admin/site-health/site-health-axe-core.integration.js +135 -20
  3. package/dist/components/admin/site-health/site-health-axe-core.integration.test.js +79 -0
  4. package/dist/components/admin/site-health/site-health-axe-core.js +35 -0
  5. package/dist/components/admin/site-health/site-health-core-web-vitals.integration.js +5 -3
  6. package/dist/components/admin/site-health/site-health-core-web-vitals.integration.test.js +33 -0
  7. package/dist/components/admin/site-health/site-health-template.js +3 -1
  8. package/dist/components/admin/site-health/site-health-utils.js +22 -0
  9. package/dist/components/config/config.example.js +2 -0
  10. package/dist/components/config/config.types.js +1 -3
  11. package/dist/components/config/config.utils.js +13 -5
  12. package/dist/components/config/config.validators.js +67 -0
  13. package/dist/components/general/cache-manager.js +124 -0
  14. package/dist/components/general/google.reviews.components.js +1 -2
  15. package/dist/components/general/googlemap.js +5 -2
  16. package/dist/components/general/metadata.functions.js +15 -1
  17. package/dist/components/general/proxy-csp-listener.js +20 -0
  18. package/dist/components/general/proxy-handler.js +4 -2
  19. package/dist/components/general/sitemap.js +9 -16
  20. package/dist/components/shoppingcart/ebay.components.js +123 -15
  21. package/dist/components/shoppingcart/ebay.functions.js +136 -34
  22. package/dist/components/shoppingcart/shoppingcart.components.js +4 -2
  23. package/dist/components/sitebuilder/config/ConfigEngine.js +5 -2
  24. package/dist/components/sitebuilder/config/google-fonts.js +5 -3
  25. package/dist/components/sitebuilder/page/lib/pageStorageLocal.js +2 -1
  26. package/dist/config/pixelated.config.json +83 -69
  27. package/dist/index.adminclient.js +1 -0
  28. package/dist/index.js +3 -0
  29. package/dist/index.server.js +1 -0
  30. package/dist/scripts/release.sh +2 -2
  31. package/dist/test/config.mock.js +13 -0
  32. package/dist/test/setup.js +46 -0
  33. package/dist/test/test-utils.js +23 -0
  34. package/dist/types/components/admin/site-health/site-health-axe-core.d.ts.map +1 -1
  35. package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts +2 -0
  36. package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts.map +1 -1
  37. package/dist/types/components/admin/site-health/site-health-axe-core.integration.test.d.ts +2 -0
  38. package/dist/types/components/admin/site-health/site-health-axe-core.integration.test.d.ts.map +1 -0
  39. package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts +1 -0
  40. package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts.map +1 -1
  41. package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.test.d.ts +2 -0
  42. package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.test.d.ts.map +1 -0
  43. package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -1
  44. package/dist/types/components/admin/site-health/site-health-utils.d.ts +1 -1
  45. package/dist/types/components/admin/site-health/site-health-utils.d.ts.map +1 -1
  46. package/dist/types/components/config/config.example.d.ts.map +1 -1
  47. package/dist/types/components/config/config.types.d.ts +30 -11
  48. package/dist/types/components/config/config.types.d.ts.map +1 -1
  49. package/dist/types/components/config/config.utils.d.ts.map +1 -1
  50. package/dist/types/components/config/config.validators.d.ts +17 -0
  51. package/dist/types/components/config/config.validators.d.ts.map +1 -0
  52. package/dist/types/components/general/cache-manager.d.ts +45 -0
  53. package/dist/types/components/general/cache-manager.d.ts.map +1 -0
  54. package/dist/types/components/general/google.reviews.components.d.ts.map +1 -1
  55. package/dist/types/components/general/googlemap.d.ts +1 -1
  56. package/dist/types/components/general/googlemap.d.ts.map +1 -1
  57. package/dist/types/components/general/metadata.functions.d.ts +2 -8
  58. package/dist/types/components/general/metadata.functions.d.ts.map +1 -1
  59. package/dist/types/components/general/proxy-csp-listener.d.ts +15 -0
  60. package/dist/types/components/general/proxy-csp-listener.d.ts.map +1 -0
  61. package/dist/types/components/general/proxy-handler.d.ts.map +1 -1
  62. package/dist/types/components/general/schema-localbusiness.d.ts.map +1 -1
  63. package/dist/types/components/general/schema-website.d.ts.map +1 -1
  64. package/dist/types/components/general/sitemap.d.ts.map +1 -1
  65. package/dist/types/components/shoppingcart/ebay.components.d.ts +9 -0
  66. package/dist/types/components/shoppingcart/ebay.components.d.ts.map +1 -1
  67. package/dist/types/components/shoppingcart/ebay.functions.d.ts +20 -21
  68. package/dist/types/components/shoppingcart/ebay.functions.d.ts.map +1 -1
  69. package/dist/types/components/shoppingcart/shoppingcart.components.d.ts +1 -1
  70. package/dist/types/components/shoppingcart/shoppingcart.components.d.ts.map +1 -1
  71. package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts +135 -0
  72. package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts.map +1 -1
  73. package/dist/types/components/sitebuilder/config/ConfigEngine.d.ts +3 -2
  74. package/dist/types/components/sitebuilder/config/ConfigEngine.d.ts.map +1 -1
  75. package/dist/types/components/sitebuilder/config/google-fonts.d.ts +1 -1
  76. package/dist/types/components/sitebuilder/config/google-fonts.d.ts.map +1 -1
  77. package/dist/types/components/sitebuilder/page/lib/pageStorageLocal.d.ts.map +1 -1
  78. package/dist/types/index.adminclient.d.ts +1 -0
  79. package/dist/types/index.d.ts +3 -0
  80. package/dist/types/index.server.d.ts +1 -0
  81. package/dist/types/stories/{seo/seo.404.stories.d.ts → general/404.stories.d.ts} +1 -2
  82. package/dist/types/stories/general/404.stories.d.ts.map +1 -0
  83. package/dist/types/stories/general/accordion.stories.d.ts +3 -3
  84. package/dist/types/stories/general/accordion.stories.d.ts.map +1 -1
  85. package/dist/types/stories/general/buzzword-bingo.stories.d.ts.map +1 -0
  86. package/dist/types/stories/{callout → general}/callout.many.stories.d.ts +0 -1
  87. package/dist/types/stories/general/callout.many.stories.d.ts.map +1 -0
  88. package/dist/types/stories/{callout → general}/callout.stories.d.ts +0 -1
  89. package/dist/types/stories/general/callout.stories.d.ts.map +1 -0
  90. package/dist/types/stories/general/carousel-hero.stories.d.ts +14 -0
  91. package/dist/types/stories/general/carousel-hero.stories.d.ts.map +1 -0
  92. package/dist/types/stories/{carousel → general}/carousel-reviews.stories.d.ts +0 -1
  93. package/dist/types/stories/general/carousel-reviews.stories.d.ts.map +1 -0
  94. package/dist/types/stories/general/carousel-workportfolio.stories.d.ts +14 -0
  95. package/dist/types/stories/general/carousel-workportfolio.stories.d.ts.map +1 -0
  96. package/dist/types/stories/{carousel → general}/carousel.stories.d.ts +1 -9
  97. package/dist/types/stories/general/carousel.stories.d.ts.map +1 -0
  98. package/dist/types/stories/general/contentful.item.stories.d.ts +12 -0
  99. package/dist/types/stories/general/contentful.item.stories.d.ts.map +1 -0
  100. package/dist/types/stories/general/contentful.items.stories.d.ts +10 -0
  101. package/dist/types/stories/general/contentful.items.stories.d.ts.map +1 -0
  102. package/dist/types/stories/{cms → general}/contentful.stories.d.ts +0 -1
  103. package/dist/types/stories/general/contentful.stories.d.ts.map +1 -0
  104. package/dist/types/stories/{seo/seo.faq-accordion.stories.d.ts → general/faq-accordion.stories.d.ts} +1 -1
  105. package/dist/types/stories/general/faq-accordion.stories.d.ts.map +1 -0
  106. package/dist/types/stories/{cms → general}/google.reviews.stories.d.ts +1 -2
  107. package/dist/types/stories/general/google.reviews.stories.d.ts.map +1 -0
  108. package/dist/types/stories/{seo/seo.googleanalytics.stories.d.ts → general/googleanalytics.stories.d.ts} +2 -4
  109. package/dist/types/stories/general/googleanalytics.stories.d.ts.map +1 -0
  110. package/dist/types/stories/{seo/seo.googlesearch.stories.d.ts → general/googlesearch.stories.d.ts} +1 -1
  111. package/dist/types/stories/general/googlesearch.stories.d.ts.map +1 -0
  112. package/dist/types/stories/{cms → general}/gravatar.stories.d.ts +0 -1
  113. package/dist/types/stories/general/gravatar.stories.d.ts.map +1 -0
  114. package/dist/types/stories/general/headers.stories.d.ts.map +1 -1
  115. package/dist/types/stories/{cms → general}/instagram.stories.d.ts +2 -2
  116. package/dist/types/stories/general/instagram.stories.d.ts.map +1 -0
  117. package/dist/types/stories/general/layout.stories.d.ts +9 -9
  118. package/dist/types/stories/{structured → general}/markdown.stories.d.ts +1 -2
  119. package/dist/types/stories/general/markdown.stories.d.ts.map +1 -0
  120. package/dist/types/stories/{menu → general}/menu-accordion.stories.d.ts +2 -2
  121. package/dist/types/stories/general/menu-accordion.stories.d.ts.map +1 -0
  122. package/dist/types/stories/{menu → general}/menu-expando.stories.d.ts +1 -1
  123. package/dist/types/stories/general/menu-expando.stories.d.ts.map +1 -0
  124. package/dist/types/stories/{menu → general}/menu-simple.stories.d.ts +1 -1
  125. package/dist/types/stories/general/menu-simple.stories.d.ts.map +1 -0
  126. package/dist/types/stories/{seo/seo.metadata.stories.d.ts → general/metadata.stories.d.ts} +1 -1
  127. package/dist/types/stories/general/metadata.stories.d.ts.map +1 -0
  128. package/dist/types/stories/general/microinteractions.stories.d.ts +0 -1
  129. package/dist/types/stories/general/microinteractions.stories.d.ts.map +1 -1
  130. package/dist/types/stories/general/modal.stories.d.ts +0 -1
  131. package/dist/types/stories/general/modal.stories.d.ts.map +1 -1
  132. package/dist/types/stories/general/nerdjoke.stories.d.ts.map +1 -0
  133. package/dist/types/stories/{structured → general}/recipe.stories.d.ts +1 -2
  134. package/dist/types/stories/general/recipe.stories.d.ts.map +1 -0
  135. package/dist/types/stories/{structured → general}/resume.stories.d.ts +1 -2
  136. package/dist/types/stories/{structured → general}/resume.stories.d.ts.map +1 -1
  137. package/dist/types/stories/{seo/seo.schema.stories.d.ts → general/schema.stories.d.ts} +1 -2
  138. package/dist/types/stories/general/schema.stories.d.ts.map +1 -0
  139. package/dist/types/stories/{seo/seo.sitemap.stories.d.ts → general/sitemap.stories.d.ts} +1 -1
  140. package/dist/types/stories/general/sitemap.stories.d.ts.map +1 -0
  141. package/dist/types/stories/general/smartimage.stories.d.ts.map +1 -1
  142. package/dist/types/stories/{structured → general}/socialcard.stories.d.ts +0 -1
  143. package/dist/types/stories/general/socialcard.stories.d.ts.map +1 -0
  144. package/dist/types/stories/general/splitscroll.stories.d.ts.map +1 -1
  145. package/dist/types/stories/{carousel → general}/tiles.stories.d.ts +0 -1
  146. package/dist/types/stories/general/tiles.stories.d.ts.map +1 -0
  147. package/dist/types/stories/{structured → general}/timeline.stories.d.ts +0 -1
  148. package/dist/types/stories/general/timeline.stories.d.ts.map +1 -0
  149. package/dist/types/stories/{cms → general}/wordpress.stories.d.ts +6 -2
  150. package/dist/types/stories/general/wordpress.stories.d.ts.map +1 -0
  151. package/dist/types/stories/shoppingcart/ebay.stories.d.ts +16 -0
  152. package/dist/types/stories/shoppingcart/ebay.stories.d.ts.map +1 -0
  153. package/dist/types/stories/shoppingcart/shoppingcart.ebay.item.stories.d.ts +1 -12
  154. package/dist/types/stories/shoppingcart/shoppingcart.ebay.item.stories.d.ts.map +1 -1
  155. package/dist/types/stories/shoppingcart/shoppingcart.ebay.items.stories.d.ts +1 -12
  156. package/dist/types/stories/shoppingcart/shoppingcart.ebay.items.stories.d.ts.map +1 -1
  157. package/dist/types/stories/shoppingcart/shoppingcart.stories.d.ts.map +1 -1
  158. package/dist/types/stories/sitebuilder/compoundfontselector.stories.d.ts +13 -35
  159. package/dist/types/stories/sitebuilder/compoundfontselector.stories.d.ts.map +1 -1
  160. package/dist/types/stories/sitebuilder/form-engine.stories.d.ts +0 -1
  161. package/dist/types/stories/sitebuilder/form-engine.stories.d.ts.map +1 -1
  162. package/dist/types/stories/sitebuilder/pageengine.stories.d.ts +0 -1
  163. package/dist/types/stories/sitebuilder/pageengine.stories.d.ts.map +1 -1
  164. package/dist/types/test/config.mock.d.ts +11 -0
  165. package/dist/types/test/config.mock.d.ts.map +1 -0
  166. package/dist/types/test/setup.d.ts.map +1 -0
  167. package/dist/types/test/test-utils.d.ts +87 -0
  168. package/dist/types/test/test-utils.d.ts.map +1 -0
  169. package/dist/types/tests/cache-manager.test.d.ts +2 -0
  170. package/dist/types/tests/cache-manager.test.d.ts.map +1 -0
  171. package/dist/types/tests/config-core.test.d.ts +2 -0
  172. package/dist/types/tests/config-core.test.d.ts.map +1 -0
  173. package/dist/types/tests/config.validators.test.d.ts +2 -0
  174. package/dist/types/tests/config.validators.test.d.ts.map +1 -0
  175. package/dist/types/tests/ebay-functions.test.d.ts +2 -0
  176. package/dist/types/tests/ebay-functions.test.d.ts.map +1 -0
  177. package/dist/types/tests/site-health-utils.test.d.ts +2 -0
  178. package/dist/types/tests/site-health-utils.test.d.ts.map +1 -0
  179. package/package.json +7 -7
  180. package/dist/types/stories/callout/callout.many.stories.d.ts.map +0 -1
  181. package/dist/types/stories/callout/callout.stories.d.ts.map +0 -1
  182. package/dist/types/stories/carousel/carousel-hero.stories.d.ts +0 -22
  183. package/dist/types/stories/carousel/carousel-hero.stories.d.ts.map +0 -1
  184. package/dist/types/stories/carousel/carousel-reviews.stories.d.ts.map +0 -1
  185. package/dist/types/stories/carousel/carousel-workportfolio.stories.d.ts +0 -22
  186. package/dist/types/stories/carousel/carousel-workportfolio.stories.d.ts.map +0 -1
  187. package/dist/types/stories/carousel/carousel.stories.d.ts.map +0 -1
  188. package/dist/types/stories/carousel/tiles.stories.d.ts.map +0 -1
  189. package/dist/types/stories/cms/contentful.item.stories.d.ts +0 -21
  190. package/dist/types/stories/cms/contentful.item.stories.d.ts.map +0 -1
  191. package/dist/types/stories/cms/contentful.items.stories.d.ts +0 -20
  192. package/dist/types/stories/cms/contentful.items.stories.d.ts.map +0 -1
  193. package/dist/types/stories/cms/contentful.stories.d.ts.map +0 -1
  194. package/dist/types/stories/cms/google.reviews.stories.d.ts.map +0 -1
  195. package/dist/types/stories/cms/gravatar.stories.d.ts.map +0 -1
  196. package/dist/types/stories/cms/instagram.stories.d.ts.map +0 -1
  197. package/dist/types/stories/cms/wordpress.stories.d.ts.map +0 -1
  198. package/dist/types/stories/menu/menu-accordion.stories.d.ts.map +0 -1
  199. package/dist/types/stories/menu/menu-expando.stories.d.ts.map +0 -1
  200. package/dist/types/stories/menu/menu-simple.stories.d.ts.map +0 -1
  201. package/dist/types/stories/nerdjoke.stories.d.ts.map +0 -1
  202. package/dist/types/stories/seo/seo.404.stories.d.ts.map +0 -1
  203. package/dist/types/stories/seo/seo.faq-accordion.stories.d.ts.map +0 -1
  204. package/dist/types/stories/seo/seo.googleanalytics.stories.d.ts.map +0 -1
  205. package/dist/types/stories/seo/seo.googlesearch.stories.d.ts.map +0 -1
  206. package/dist/types/stories/seo/seo.metadata.stories.d.ts.map +0 -1
  207. package/dist/types/stories/seo/seo.schema.stories.d.ts.map +0 -1
  208. package/dist/types/stories/seo/seo.sitemap.stories.d.ts.map +0 -1
  209. package/dist/types/stories/structured/buzzword-bingo.stories.d.ts.map +0 -1
  210. package/dist/types/stories/structured/markdown.stories.d.ts.map +0 -1
  211. package/dist/types/stories/structured/recipe.stories.d.ts.map +0 -1
  212. package/dist/types/stories/structured/socialcard.stories.d.ts.map +0 -1
  213. package/dist/types/stories/structured/timeline.stories.d.ts.map +0 -1
  214. package/dist/types/tests/setup.d.ts.map +0 -1
  215. /package/dist/types/stories/{structured → general}/buzzword-bingo.stories.d.ts +0 -0
  216. /package/dist/types/stories/{nerdjoke.stories.d.ts → general/nerdjoke.stories.d.ts} +0 -0
  217. /package/dist/types/{tests → test}/setup.d.ts +0 -0
package/README.md CHANGED
@@ -242,6 +242,13 @@ External service integrations:
242
242
  - **Yelp** - Business reviews and ratings
243
243
 
244
244
 
245
+ ### Utilities
246
+ Shared technical utilities and helpers:
247
+ - **CacheManager** - Unified caching layer with Memory, Session, and LocalStorage support with TTL and SSR fallbacks.
248
+ - **Cloudinary** - Image processing and URL generation helpers.
249
+ - **Date/Time** - Formatting and manipulation utilities.
250
+
251
+
245
252
  ### Site Health & Monitoring
246
253
  Comprehensive site health monitoring and analytics:
247
254
  - **SiteHealthOverview** - Dashboard overview of site health metrics
@@ -1,9 +1,12 @@
1
1
  "use server";
2
2
  import puppeteer from 'puppeteer';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ const debug = false;
3
6
  export async function performAxeCoreAnalysis(url) {
4
7
  try {
5
8
  // Run axe-core analysis
6
- const axeResult = await runAxeCoreAnalysis(url);
9
+ const { result: axeResult, injectionSource } = await runAxeCoreAnalysis(url);
7
10
  // Calculate summary
8
11
  const summary = {
9
12
  violations: axeResult.violations.length,
@@ -22,6 +25,7 @@ export async function performAxeCoreAnalysis(url) {
22
25
  summary,
23
26
  timestamp: new Date().toISOString(),
24
27
  status: 'success',
28
+ injectionSource: injectionSource,
25
29
  };
26
30
  }
27
31
  catch (error) {
@@ -80,6 +84,38 @@ async function runAxeCoreAnalysis(url) {
80
84
  const page = await browser.newPage();
81
85
  // Set viewport for consistent results
82
86
  await page.setViewport({ width: 1280, height: 720 });
87
+ // Capture console messages from the page for debugging
88
+ if (debug) {
89
+ page.on('console', msg => {
90
+ try {
91
+ console.info('PAGE CONSOLE:', msg.text());
92
+ }
93
+ catch (e) {
94
+ console.warn('PAGE CONSOLE (error reading):', e);
95
+ }
96
+ });
97
+ // Capture failed requests (esp. script loads) and successful script responses
98
+ page.on('requestfailed', req => {
99
+ try {
100
+ if (req.resourceType && req.resourceType() === 'script') {
101
+ console.warn('PAGE REQUEST FAILED:', req.url(), req.failure()?.errorText);
102
+ }
103
+ }
104
+ catch (e) {
105
+ // ignore
106
+ }
107
+ });
108
+ page.on('response', resp => {
109
+ try {
110
+ if (resp.request && resp.request().resourceType() === 'script') {
111
+ console.info('PAGE SCRIPT RESPONSE:', resp.url(), resp.status());
112
+ }
113
+ }
114
+ catch (e) {
115
+ // ignore
116
+ }
117
+ });
118
+ }
83
119
  // Set user agent to avoid bot detection
84
120
  await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
85
121
  // Navigate to the page with timeout
@@ -89,27 +125,106 @@ async function runAxeCoreAnalysis(url) {
89
125
  });
90
126
  // Wait a bit for dynamic content to load
91
127
  await new Promise(resolve => setTimeout(resolve, 2000));
92
- // Inject axe-core by adding the script tag
93
- await page.addScriptTag({
94
- url: 'https://cdn.jsdelivr.net/npm/axe-core@4.8.2/axe.min.js'
95
- });
96
- // Wait a bit for axe to load
97
- await new Promise(resolve => setTimeout(resolve, 1000));
98
- // Run axe-core analysis
99
- const result = await page.evaluate(async () => {
100
- // Check if axe is available
101
- if (typeof window.axe === 'undefined') {
102
- throw new Error('axe-core not loaded');
128
+ // Try to inject axe-core via CDN first; if that fails (network restrictions), fall back to several local strategies
129
+ let injectionSource = 'none';
130
+ try {
131
+ await page.addScriptTag({ url: 'https://cdn.jsdelivr.net/npm/axe-core/axe.min.js' });
132
+ // Wait a bit for axe to load
133
+ await new Promise(resolve => setTimeout(resolve, 1000));
134
+ injectionSource = 'cdn';
135
+ }
136
+ catch (err) {
137
+ let injected = false;
138
+ // Try common local node_modules locations relative to process.cwd() and __dirname
139
+ const possiblePaths = [
140
+ path.join(process.cwd(), 'node_modules', 'axe-core', 'axe.min.js'),
141
+ path.join(process.cwd(), '..', 'node_modules', 'axe-core', 'axe.min.js'),
142
+ path.join(__dirname, '..', '..', 'node_modules', 'axe-core', 'axe.min.js')
143
+ ];
144
+ for (const p of possiblePaths) {
145
+ try {
146
+ if (fs.existsSync(p)) {
147
+ const fileSrc = fs.readFileSync(p, 'utf8');
148
+ await page.addScriptTag({ content: fileSrc });
149
+ injected = true;
150
+ injectionSource = 'local-inline';
151
+ break;
152
+ }
153
+ }
154
+ catch (e) {
155
+ // ignore local file read errors
156
+ }
157
+ }
158
+ // Last resort: require.resolve
159
+ if (!injected) {
160
+ try {
161
+ const axePath = require.resolve('axe-core/axe.min.js');
162
+ const axeSrc = fs.readFileSync(axePath, 'utf8');
163
+ await page.addScriptTag({ content: axeSrc });
164
+ injected = true;
165
+ injectionSource = 'require-resolve';
166
+ }
167
+ catch (e) {
168
+ // ignore
169
+ }
170
+ }
171
+ if (!injected) {
172
+ throw new Error('Could not load axe-core via CDN or local inline injection');
103
173
  }
104
- // Run axe with all rules enabled
105
- const axeResults = await window.axe.run(document, {
106
- rules: {}, // Run all rules
107
- runOnly: undefined, // Don't restrict to specific rule sets
108
- reporter: 'v2'
174
+ }
175
+ // Run axe-core analysis (poll across frames for availability after injection)
176
+ // Wait up to 10s total for window.axe to appear (check every 200ms across frames)
177
+ const timeoutMs = 10000;
178
+ const intervalMs = 200;
179
+ const start = Date.now();
180
+ let axeResults = null;
181
+ let frameWithAxe = null;
182
+ while (!frameWithAxe && Date.now() - start < timeoutMs) {
183
+ const frames = page.frames();
184
+ for (const f of frames) {
185
+ try {
186
+ const hasAxe = await f.evaluate(() => typeof window.axe !== 'undefined').catch(() => false);
187
+ if (hasAxe) {
188
+ frameWithAxe = f;
189
+ break;
190
+ }
191
+ }
192
+ catch (e) {
193
+ // ignore frame evaluation errors
194
+ }
195
+ }
196
+ if (!frameWithAxe)
197
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
198
+ }
199
+ if (!frameWithAxe) {
200
+ // Collect some debug info to help identify why axe didn't attach
201
+ try {
202
+ const scripts = await page.evaluate(() => Array.from(document.querySelectorAll('script')).map(s => (s.src || (s.innerText || '').slice(0, 200))));
203
+ const csp = await page.evaluate(() => document.querySelector('meta[http-equiv="Content-Security-Policy"]')?.getAttribute('content') || null);
204
+ const pageDiag = await page.evaluate(() => ({ hasAxe: typeof window.axe !== 'undefined', axeKeys: Object.keys(window).filter(k => /axe/i.test(k)), windowHasAxeRun: typeof window.axe?.run === 'function' }));
205
+ }
206
+ catch (e) {
207
+ // ignore diagnostic errors
208
+ }
209
+ // Diagnostic: no axe found in any frame
210
+ throw new Error('axe-core not loaded');
211
+ }
212
+ // Run axe in the frame that has it
213
+ try {
214
+ axeResults = await frameWithAxe.evaluate(async () => {
215
+ return await window.axe.run(document, {
216
+ rules: {},
217
+ runOnly: undefined,
218
+ reporter: 'v2'
219
+ });
109
220
  });
110
- return axeResults;
111
- });
112
- return result;
221
+ }
222
+ catch (e) {
223
+ if (debug)
224
+ console.error('Axe run failed:', e);
225
+ throw e;
226
+ }
227
+ return { result: axeResults, injectionSource };
113
228
  }
114
229
  finally {
115
230
  if (browser) {
@@ -0,0 +1,79 @@
1
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ // We'll mock puppeteer and fs before importing the module under test to avoid ESM spy limitations
3
+ describe('performAxeCoreAnalysis (CDN blocked -> local-inline fallback)', () => {
4
+ beforeEach(async () => {
5
+ vi.resetModules();
6
+ // Mock puppeteer browser and page
7
+ const page = {
8
+ setViewport: vi.fn().mockResolvedValue(undefined),
9
+ on: vi.fn().mockReturnValue(undefined),
10
+ setUserAgent: vi.fn().mockResolvedValue(undefined),
11
+ goto: vi.fn().mockResolvedValue(undefined),
12
+ addScriptTag: vi.fn().mockImplementation(async (opts) => {
13
+ if (opts && opts.url && opts.url.includes('cdn.jsdelivr')) {
14
+ // Simulate CDN blocked
15
+ throw new Error('CDN blocked');
16
+ }
17
+ // Otherwise pretend inline injection succeeded
18
+ return Promise.resolve(undefined);
19
+ }),
20
+ frames: vi.fn().mockReturnValue([{
21
+ evaluate: vi.fn().mockImplementation(async (fn) => {
22
+ const fnStr = fn.toString();
23
+ if (fnStr.includes('typeof (window as any).axe') || fnStr.includes('typeof window.axe')) {
24
+ return true; // axe is present after inline injection
25
+ }
26
+ if (fnStr.includes('axe.run') || fnStr.includes('window.axe.run')) {
27
+ // return a minimal axe result shape
28
+ return {
29
+ violations: [],
30
+ passes: [],
31
+ incomplete: [],
32
+ inapplicable: [],
33
+ testEngine: { name: 'axe-core', version: 'test' },
34
+ testRunner: { name: 'mock' },
35
+ testEnvironment: { userAgent: 'mock', windowWidth: 1280, windowHeight: 720 },
36
+ timestamp: new Date().toISOString(),
37
+ url: 'http://example'
38
+ };
39
+ }
40
+ return null;
41
+ })
42
+ }])
43
+ };
44
+ const browser = {
45
+ newPage: vi.fn().mockResolvedValue(page),
46
+ close: vi.fn().mockResolvedValue(undefined)
47
+ };
48
+ // Mock puppeteer before importing the module to avoid ESM spy issues
49
+ vi.doMock('puppeteer', async (importOriginal) => {
50
+ // Provide a minimal mock that exposes launch as both default and named
51
+ return {
52
+ default: { launch: () => Promise.resolve(browser) },
53
+ launch: () => Promise.resolve(browser)
54
+ };
55
+ });
56
+ // Mock fs before importing the module (provide both default and named exports for interop)
57
+ vi.doMock('fs', () => ({
58
+ existsSync: () => true,
59
+ readFileSync: () => '/* fake axe content */',
60
+ default: {
61
+ existsSync: () => true,
62
+ readFileSync: () => '/* fake axe content */'
63
+ }
64
+ }));
65
+ });
66
+ afterEach(() => {
67
+ vi.restoreAllMocks();
68
+ vi.clearAllMocks();
69
+ vi.resetModules();
70
+ });
71
+ it('falls back to local inline injection when CDN is blocked and reports injectionSource "local-inline"', async () => {
72
+ const { performAxeCoreAnalysis } = await import('./site-health-axe-core.integration');
73
+ const url = 'http://example.local';
74
+ const res = await performAxeCoreAnalysis(url);
75
+ expect(res).toBeDefined();
76
+ expect(res.status).toBe('success');
77
+ expect(res.injectionSource).toBe('local-inline');
78
+ });
79
+ });
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import React from 'react';
3
4
  import PropTypes from 'prop-types';
4
5
  import { SiteHealthTemplate } from './site-health-template';
5
6
  import { getImpactIndicator, getIncompleteIndicator, getPassingIndicator } from './site-health-indicators';
@@ -10,6 +11,40 @@ export function SiteHealthAxeCore({ siteName }) {
10
11
  const getImpactColor = (impact) => {
11
12
  return getImpactIndicator(impact).color;
12
13
  };
14
+ // Ensure axe-core is available on the admin page so self-analysis and debugging are possible
15
+ React.useEffect(() => {
16
+ if (typeof window === 'undefined')
17
+ return;
18
+ if (window.axe)
19
+ return; // already loaded
20
+ const cdnSrc = 'https://cdn.jsdelivr.net/npm/axe-core/axe.min.js';
21
+ const apiFallback = '/api/axe-core';
22
+ function injectScript(src) {
23
+ const script = document.createElement('script');
24
+ script.src = src;
25
+ script.async = false; // preserve execution order
26
+ script.onload = () => console.info('axe-core loaded from', src);
27
+ script.onerror = async () => {
28
+ console.warn('Failed to load axe-core from', src);
29
+ if (src !== apiFallback) {
30
+ // Try fallback to local API that serves the bundle
31
+ injectScript(apiFallback);
32
+ }
33
+ };
34
+ document.head.appendChild(script);
35
+ }
36
+ // Try CDN first, then API fallback
37
+ try {
38
+ injectScript(cdnSrc);
39
+ }
40
+ catch (e) {
41
+ console.warn('Could not inject axe-core script:', e);
42
+ injectScript(apiFallback);
43
+ }
44
+ return () => {
45
+ // no cleanup: scripts persist on the page
46
+ };
47
+ }, []);
13
48
  const getImpactIcon = (impact) => {
14
49
  return getImpactIndicator(impact).icon;
15
50
  };
@@ -1,4 +1,5 @@
1
1
  "use server";
2
+ import { getFullPixelatedConfig } from '../../config/config';
2
3
  const psiCache = new Map();
3
4
  const CACHE_TTL_SUCCESS = 60 * 60 * 1000; // 1 hour for successful results
4
5
  const CACHE_TTL_ERROR = 5 * 60 * 1000; // 5 minutes for error results
@@ -83,10 +84,11 @@ export async function performCoreWebVitalsAnalysis(url, siteName, useCache = tru
83
84
  return errorResult;
84
85
  }
85
86
  }
86
- async function fetchPSIData(url) {
87
- const apiKey = process.env.GOOGLE_API_KEY;
87
+ export async function fetchPSIData(url) {
88
+ // Require the API key from the unified pixelated.config.json. No environment fallback.
89
+ const apiKey = getFullPixelatedConfig()?.google?.api_key;
88
90
  if (!apiKey) {
89
- throw new Error('GOOGLE_API_KEY environment variable is not set');
91
+ throw new Error('Google API key is not set; set google.api_key in pixelated.config.json');
90
92
  }
91
93
  const psiUrl = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(url)}&key=${apiKey}&strategy=mobile&category=performance&category=accessibility&category=best-practices&category=seo`;
92
94
  const fetchWithRetry = async (url, maxRetries = 2) => {
@@ -0,0 +1,33 @@
1
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as configModule from '../../config/config';
3
+ import { fetchPSIData } from './site-health-core-web-vitals.integration';
4
+ import { mockConfig } from '../../../test/config.mock';
5
+ // Use the test harness mock config derived from src/config/pixelated.config.json
6
+ describe('fetchPSIData', () => {
7
+ let originalFetch;
8
+ let getFullConfigSpy;
9
+ beforeEach(() => {
10
+ originalFetch = globalThis.fetch;
11
+ globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ lighthouseResult: { audits: { someAudit: {} }, categories: {} } }) });
12
+ // Ensure server-side code sees the standard test harness config by default
13
+ getFullConfigSpy = vi.spyOn(configModule, 'getFullPixelatedConfig').mockReturnValue(mockConfig);
14
+ });
15
+ afterEach(() => {
16
+ globalThis.fetch = originalFetch;
17
+ getFullConfigSpy?.mockRestore();
18
+ vi.restoreAllMocks();
19
+ });
20
+ it('uses API key from pixelated.config.json', async () => {
21
+ const apiKey = mockConfig?.google?.api_key;
22
+ expect(apiKey).toBeDefined();
23
+ const url = 'https://example.com';
24
+ await fetchPSIData(url);
25
+ expect(globalThis.fetch).toHaveBeenCalled();
26
+ const calledUrl = globalThis.fetch.mock.calls[0][0];
27
+ expect(calledUrl).toContain(`key=${apiKey}`);
28
+ });
29
+ it('throws when api key is missing from config', async () => {
30
+ getFullConfigSpy.mockReturnValue({});
31
+ await expect(fetchPSIData('https://example.com')).rejects.toThrow('Google API key is not set');
32
+ });
33
+ });
@@ -76,7 +76,9 @@ export function SiteHealthTemplate(props) {
76
76
  // Check for cache control from URL query parameters
77
77
  const urlParams = new URLSearchParams(window.location.search);
78
78
  const cacheParam = urlParams.get('cache');
79
- const useCache = typedProps.enableCacheControl ?? true ? (cacheParam !== 'false') : true;
79
+ // Correctly compute useCache: if enableCacheControl is enabled (default true), honor cacheParam; if disabled, caching is off
80
+ const enableCacheControl = (typeof typedProps.enableCacheControl === 'boolean') ? typedProps.enableCacheControl : true;
81
+ const useCache = enableCacheControl ? (String(cacheParam).toLowerCase() !== 'false') : false;
80
82
  const result = await fetchFromEndpoint(useCache);
81
83
  setData(result);
82
84
  setError(null);
@@ -25,6 +25,28 @@ export function formatScore(score) {
25
25
  * Formats audit item details for display
26
26
  */
27
27
  export function formatAuditItem(item, auditTitle) {
28
+ // Handle raw timing data that might be passed directly
29
+ if (typeof item === 'number') {
30
+ let context = '';
31
+ if (auditTitle) {
32
+ if (auditTitle.toLowerCase().includes('server') || auditTitle.toLowerCase().includes('backend')) {
33
+ context = ' server response';
34
+ }
35
+ else if (auditTitle.toLowerCase().includes('network') || auditTitle.toLowerCase().includes('request')) {
36
+ context = ' network request';
37
+ }
38
+ else if (auditTitle.toLowerCase().includes('render') || auditTitle.toLowerCase().includes('blocking')) {
39
+ context = ' render blocking';
40
+ }
41
+ else if (auditTitle.toLowerCase().includes('javascript') || auditTitle.toLowerCase().includes('js')) {
42
+ context = ' JavaScript';
43
+ }
44
+ else if (auditTitle.toLowerCase().includes('image') || auditTitle.toLowerCase().includes('media')) {
45
+ context = ' media resource';
46
+ }
47
+ }
48
+ return `${item.toFixed(2)}ms${context}`;
49
+ }
28
50
  // Handle URLs
29
51
  if (item.url && typeof item.url === 'string') {
30
52
  return item.url;
@@ -21,8 +21,10 @@ const pixelatedConfig = {
21
21
  ebay: {
22
22
  proxyURL: 'https://proxy.provier.com/proxy?url=',
23
23
  appId: 'your-ebay-client-id',
24
+ appDevId: 'your-ebay-client-dev-id',
24
25
  appCertId: 'your-ebay-client-secret',
25
26
  sbxAppId: 'your-ebay-sandbox-client-id',
27
+ sbxAppDevId: 'your-ebay-sandbox-client-dev-id',
26
28
  sbxAppCertId: 'your-ebay-sandbox-client-secret',
27
29
  globalId: 'EBAY_US',
28
30
  environment: 'production',
@@ -19,9 +19,7 @@ export const SECRET_CONFIG_KEYS = {
19
19
  'preview_access_token'
20
20
  ],
21
21
  ebay: [
22
- 'appCertId',
23
- 'sbxAppId',
24
- 'sbxAppCertId'
22
+ 'sbxAppId'
25
23
  ],
26
24
  paypal: [
27
25
  'sandboxPayPalApiKey',
@@ -11,25 +11,33 @@ export function getClientOnlyPixelatedConfig(src) {
11
11
  // 2. Check Service-Specific Secret List
12
12
  if (serviceName && SECRET_CONFIG_KEYS.services[serviceName]) {
13
13
  const serviceSecrets = SECRET_CONFIG_KEYS.services[serviceName];
14
- if (serviceSecrets.includes(key))
14
+ if (serviceSecrets.includes(key)) {
15
+ // console.log(`Config Stripper: Removing secret key "${key}" from service "${serviceName}"`);
15
16
  return true;
17
+ }
16
18
  }
17
19
  return false;
18
20
  }
19
21
  function strip(obj, serviceName) {
20
- if (!obj || typeof obj !== 'object' || obj === null)
22
+ // Base case for non-objects
23
+ if (obj === null || typeof obj !== 'object')
21
24
  return obj;
25
+ // Avoid circular references
22
26
  if (visited.has(obj))
23
27
  return '[Circular]';
24
28
  visited.add(obj);
25
- if (Array.isArray(obj))
29
+ // Handle Arrays
30
+ if (Array.isArray(obj)) {
26
31
  return obj.map((item) => strip(item, serviceName));
32
+ }
27
33
  const out = {};
28
34
  for (const k of Object.keys(obj)) {
29
- // If we are at the top level, the key 'k' IS the service name (ebay, cloudinary, etc.)
35
+ // At the top level (serviceName is undefined), k is the service name
30
36
  const currentService = serviceName || k;
31
- if (isSecretKey(k, serviceName))
37
+ // Check if this key should be stripped
38
+ if (isSecretKey(k, serviceName)) {
32
39
  continue;
40
+ }
33
41
  out[k] = strip(obj[k], currentService);
34
42
  }
35
43
  return out;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Assert that an object is a valid SiteInfo with required fields.
3
+ * Throws with a clear message if validation fails.
4
+ */
5
+ export function assertSiteInfo(v) {
6
+ const missing = [];
7
+ if (!v || typeof v !== 'object') {
8
+ throw new Error('Invalid routes.json: siteInfo is missing or not an object');
9
+ }
10
+ ['name', 'url', 'description'].forEach(k => {
11
+ if (!v[k] || typeof v[k] !== 'string' || String(v[k]).trim() === '')
12
+ missing.push(k);
13
+ });
14
+ if (missing.length) {
15
+ throw new Error(`Invalid routes.json: siteInfo missing required fields: ${missing.join(', ')}`);
16
+ }
17
+ }
18
+ /**
19
+ * Basic validation for a routes object/array. Ensures the structure looks like
20
+ * a routes data blob and contains at least one named route.
21
+ */
22
+ export function assertRoutes(routes) {
23
+ if (!routes || (typeof routes !== 'object' && !Array.isArray(routes))) {
24
+ throw new Error('Invalid routes.json: routes is missing or not an object/array');
25
+ }
26
+ const found = (function findAnyNamed(obj) {
27
+ if (!obj || typeof obj !== 'object')
28
+ return false;
29
+ if (Array.isArray(obj))
30
+ return obj.some(item => findAnyNamed(item));
31
+ if (obj.name && typeof obj.name === 'string')
32
+ return true;
33
+ for (const k of Object.keys(obj)) {
34
+ if (findAnyNamed(obj[k]))
35
+ return true;
36
+ }
37
+ return false;
38
+ })(routes);
39
+ if (!found) {
40
+ throw new Error('Invalid routes.json: expected at least one route entry with a `name` property');
41
+ }
42
+ }
43
+ /**
44
+ * Basic validation for visualdesign tokens. Ensures it's an object and contains
45
+ * at least the common tokens we care about (e.g., colors or fonts may be optional)
46
+ */
47
+ export function assertVisualDesign(v) {
48
+ if (v === undefined || v === null) {
49
+ throw new Error('Invalid routes.json: visualdesign is missing');
50
+ }
51
+ if (typeof v !== 'object' || Array.isArray(v)) {
52
+ throw new Error('Invalid routes.json: visualdesign must be an object');
53
+ }
54
+ const keys = Object.keys(v || {});
55
+ if (keys.length === 0) {
56
+ throw new Error('Invalid routes.json: visualdesign must contain at least one token');
57
+ }
58
+ for (const k of keys) {
59
+ const val = v[k];
60
+ // Accept simple string tokens or objects with a string `value` property
61
+ if (typeof val === 'string')
62
+ continue;
63
+ if (val && typeof val === 'object' && typeof val.value === 'string')
64
+ continue;
65
+ throw new Error(`Invalid routes.json: visualdesign token '${k}' has an invalid value`);
66
+ }
67
+ }