@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.
- package/README.md +7 -0
- package/dist/components/admin/site-health/site-health-axe-core.integration.js +135 -20
- package/dist/components/admin/site-health/site-health-axe-core.integration.test.js +79 -0
- package/dist/components/admin/site-health/site-health-axe-core.js +35 -0
- package/dist/components/admin/site-health/site-health-core-web-vitals.integration.js +5 -3
- package/dist/components/admin/site-health/site-health-core-web-vitals.integration.test.js +33 -0
- package/dist/components/admin/site-health/site-health-template.js +3 -1
- package/dist/components/admin/site-health/site-health-utils.js +22 -0
- package/dist/components/config/config.example.js +2 -0
- package/dist/components/config/config.types.js +1 -3
- package/dist/components/config/config.utils.js +13 -5
- package/dist/components/config/config.validators.js +67 -0
- package/dist/components/general/cache-manager.js +124 -0
- package/dist/components/general/google.reviews.components.js +1 -2
- package/dist/components/general/googlemap.js +5 -2
- package/dist/components/general/metadata.functions.js +15 -1
- package/dist/components/general/proxy-csp-listener.js +20 -0
- package/dist/components/general/proxy-handler.js +4 -2
- package/dist/components/general/sitemap.js +9 -16
- package/dist/components/shoppingcart/ebay.components.js +123 -15
- package/dist/components/shoppingcart/ebay.functions.js +136 -34
- package/dist/components/shoppingcart/shoppingcart.components.js +4 -2
- package/dist/components/sitebuilder/config/ConfigEngine.js +5 -2
- package/dist/components/sitebuilder/config/google-fonts.js +5 -3
- package/dist/components/sitebuilder/page/lib/pageStorageLocal.js +2 -1
- package/dist/config/pixelated.config.json +83 -69
- package/dist/index.adminclient.js +1 -0
- package/dist/index.js +3 -0
- package/dist/index.server.js +1 -0
- package/dist/scripts/release.sh +2 -2
- package/dist/test/config.mock.js +13 -0
- package/dist/test/setup.js +46 -0
- package/dist/test/test-utils.js +23 -0
- package/dist/types/components/admin/site-health/site-health-axe-core.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts +2 -0
- package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-axe-core.integration.test.d.ts +2 -0
- package/dist/types/components/admin/site-health/site-health-axe-core.integration.test.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts +1 -0
- package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.test.d.ts +2 -0
- package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.test.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-utils.d.ts +1 -1
- package/dist/types/components/admin/site-health/site-health-utils.d.ts.map +1 -1
- package/dist/types/components/config/config.example.d.ts.map +1 -1
- package/dist/types/components/config/config.types.d.ts +30 -11
- package/dist/types/components/config/config.types.d.ts.map +1 -1
- package/dist/types/components/config/config.utils.d.ts.map +1 -1
- package/dist/types/components/config/config.validators.d.ts +17 -0
- package/dist/types/components/config/config.validators.d.ts.map +1 -0
- package/dist/types/components/general/cache-manager.d.ts +45 -0
- package/dist/types/components/general/cache-manager.d.ts.map +1 -0
- package/dist/types/components/general/google.reviews.components.d.ts.map +1 -1
- package/dist/types/components/general/googlemap.d.ts +1 -1
- package/dist/types/components/general/googlemap.d.ts.map +1 -1
- package/dist/types/components/general/metadata.functions.d.ts +2 -8
- package/dist/types/components/general/metadata.functions.d.ts.map +1 -1
- package/dist/types/components/general/proxy-csp-listener.d.ts +15 -0
- package/dist/types/components/general/proxy-csp-listener.d.ts.map +1 -0
- package/dist/types/components/general/proxy-handler.d.ts.map +1 -1
- package/dist/types/components/general/schema-localbusiness.d.ts.map +1 -1
- package/dist/types/components/general/schema-website.d.ts.map +1 -1
- package/dist/types/components/general/sitemap.d.ts.map +1 -1
- package/dist/types/components/shoppingcart/ebay.components.d.ts +9 -0
- package/dist/types/components/shoppingcart/ebay.components.d.ts.map +1 -1
- package/dist/types/components/shoppingcart/ebay.functions.d.ts +20 -21
- package/dist/types/components/shoppingcart/ebay.functions.d.ts.map +1 -1
- package/dist/types/components/shoppingcart/shoppingcart.components.d.ts +1 -1
- package/dist/types/components/shoppingcart/shoppingcart.components.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts +135 -0
- package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/config/ConfigEngine.d.ts +3 -2
- package/dist/types/components/sitebuilder/config/ConfigEngine.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/config/google-fonts.d.ts +1 -1
- package/dist/types/components/sitebuilder/config/google-fonts.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/page/lib/pageStorageLocal.d.ts.map +1 -1
- package/dist/types/index.adminclient.d.ts +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.server.d.ts +1 -0
- package/dist/types/stories/{seo/seo.404.stories.d.ts → general/404.stories.d.ts} +1 -2
- package/dist/types/stories/general/404.stories.d.ts.map +1 -0
- package/dist/types/stories/general/accordion.stories.d.ts +3 -3
- package/dist/types/stories/general/accordion.stories.d.ts.map +1 -1
- package/dist/types/stories/general/buzzword-bingo.stories.d.ts.map +1 -0
- package/dist/types/stories/{callout → general}/callout.many.stories.d.ts +0 -1
- package/dist/types/stories/general/callout.many.stories.d.ts.map +1 -0
- package/dist/types/stories/{callout → general}/callout.stories.d.ts +0 -1
- package/dist/types/stories/general/callout.stories.d.ts.map +1 -0
- package/dist/types/stories/general/carousel-hero.stories.d.ts +14 -0
- package/dist/types/stories/general/carousel-hero.stories.d.ts.map +1 -0
- package/dist/types/stories/{carousel → general}/carousel-reviews.stories.d.ts +0 -1
- package/dist/types/stories/general/carousel-reviews.stories.d.ts.map +1 -0
- package/dist/types/stories/general/carousel-workportfolio.stories.d.ts +14 -0
- package/dist/types/stories/general/carousel-workportfolio.stories.d.ts.map +1 -0
- package/dist/types/stories/{carousel → general}/carousel.stories.d.ts +1 -9
- package/dist/types/stories/general/carousel.stories.d.ts.map +1 -0
- package/dist/types/stories/general/contentful.item.stories.d.ts +12 -0
- package/dist/types/stories/general/contentful.item.stories.d.ts.map +1 -0
- package/dist/types/stories/general/contentful.items.stories.d.ts +10 -0
- package/dist/types/stories/general/contentful.items.stories.d.ts.map +1 -0
- package/dist/types/stories/{cms → general}/contentful.stories.d.ts +0 -1
- package/dist/types/stories/general/contentful.stories.d.ts.map +1 -0
- package/dist/types/stories/{seo/seo.faq-accordion.stories.d.ts → general/faq-accordion.stories.d.ts} +1 -1
- package/dist/types/stories/general/faq-accordion.stories.d.ts.map +1 -0
- package/dist/types/stories/{cms → general}/google.reviews.stories.d.ts +1 -2
- package/dist/types/stories/general/google.reviews.stories.d.ts.map +1 -0
- package/dist/types/stories/{seo/seo.googleanalytics.stories.d.ts → general/googleanalytics.stories.d.ts} +2 -4
- package/dist/types/stories/general/googleanalytics.stories.d.ts.map +1 -0
- package/dist/types/stories/{seo/seo.googlesearch.stories.d.ts → general/googlesearch.stories.d.ts} +1 -1
- package/dist/types/stories/general/googlesearch.stories.d.ts.map +1 -0
- package/dist/types/stories/{cms → general}/gravatar.stories.d.ts +0 -1
- package/dist/types/stories/general/gravatar.stories.d.ts.map +1 -0
- package/dist/types/stories/general/headers.stories.d.ts.map +1 -1
- package/dist/types/stories/{cms → general}/instagram.stories.d.ts +2 -2
- package/dist/types/stories/general/instagram.stories.d.ts.map +1 -0
- package/dist/types/stories/general/layout.stories.d.ts +9 -9
- package/dist/types/stories/{structured → general}/markdown.stories.d.ts +1 -2
- package/dist/types/stories/general/markdown.stories.d.ts.map +1 -0
- package/dist/types/stories/{menu → general}/menu-accordion.stories.d.ts +2 -2
- package/dist/types/stories/general/menu-accordion.stories.d.ts.map +1 -0
- package/dist/types/stories/{menu → general}/menu-expando.stories.d.ts +1 -1
- package/dist/types/stories/general/menu-expando.stories.d.ts.map +1 -0
- package/dist/types/stories/{menu → general}/menu-simple.stories.d.ts +1 -1
- package/dist/types/stories/general/menu-simple.stories.d.ts.map +1 -0
- package/dist/types/stories/{seo/seo.metadata.stories.d.ts → general/metadata.stories.d.ts} +1 -1
- package/dist/types/stories/general/metadata.stories.d.ts.map +1 -0
- package/dist/types/stories/general/microinteractions.stories.d.ts +0 -1
- package/dist/types/stories/general/microinteractions.stories.d.ts.map +1 -1
- package/dist/types/stories/general/modal.stories.d.ts +0 -1
- package/dist/types/stories/general/modal.stories.d.ts.map +1 -1
- package/dist/types/stories/general/nerdjoke.stories.d.ts.map +1 -0
- package/dist/types/stories/{structured → general}/recipe.stories.d.ts +1 -2
- package/dist/types/stories/general/recipe.stories.d.ts.map +1 -0
- package/dist/types/stories/{structured → general}/resume.stories.d.ts +1 -2
- package/dist/types/stories/{structured → general}/resume.stories.d.ts.map +1 -1
- package/dist/types/stories/{seo/seo.schema.stories.d.ts → general/schema.stories.d.ts} +1 -2
- package/dist/types/stories/general/schema.stories.d.ts.map +1 -0
- package/dist/types/stories/{seo/seo.sitemap.stories.d.ts → general/sitemap.stories.d.ts} +1 -1
- package/dist/types/stories/general/sitemap.stories.d.ts.map +1 -0
- package/dist/types/stories/general/smartimage.stories.d.ts.map +1 -1
- package/dist/types/stories/{structured → general}/socialcard.stories.d.ts +0 -1
- package/dist/types/stories/general/socialcard.stories.d.ts.map +1 -0
- package/dist/types/stories/general/splitscroll.stories.d.ts.map +1 -1
- package/dist/types/stories/{carousel → general}/tiles.stories.d.ts +0 -1
- package/dist/types/stories/general/tiles.stories.d.ts.map +1 -0
- package/dist/types/stories/{structured → general}/timeline.stories.d.ts +0 -1
- package/dist/types/stories/general/timeline.stories.d.ts.map +1 -0
- package/dist/types/stories/{cms → general}/wordpress.stories.d.ts +6 -2
- package/dist/types/stories/general/wordpress.stories.d.ts.map +1 -0
- package/dist/types/stories/shoppingcart/ebay.stories.d.ts +16 -0
- package/dist/types/stories/shoppingcart/ebay.stories.d.ts.map +1 -0
- package/dist/types/stories/shoppingcart/shoppingcart.ebay.item.stories.d.ts +1 -12
- package/dist/types/stories/shoppingcart/shoppingcart.ebay.item.stories.d.ts.map +1 -1
- package/dist/types/stories/shoppingcart/shoppingcart.ebay.items.stories.d.ts +1 -12
- package/dist/types/stories/shoppingcart/shoppingcart.ebay.items.stories.d.ts.map +1 -1
- package/dist/types/stories/shoppingcart/shoppingcart.stories.d.ts.map +1 -1
- package/dist/types/stories/sitebuilder/compoundfontselector.stories.d.ts +13 -35
- package/dist/types/stories/sitebuilder/compoundfontselector.stories.d.ts.map +1 -1
- package/dist/types/stories/sitebuilder/form-engine.stories.d.ts +0 -1
- package/dist/types/stories/sitebuilder/form-engine.stories.d.ts.map +1 -1
- package/dist/types/stories/sitebuilder/pageengine.stories.d.ts +0 -1
- package/dist/types/stories/sitebuilder/pageengine.stories.d.ts.map +1 -1
- package/dist/types/test/config.mock.d.ts +11 -0
- package/dist/types/test/config.mock.d.ts.map +1 -0
- package/dist/types/test/setup.d.ts.map +1 -0
- package/dist/types/test/test-utils.d.ts +87 -0
- package/dist/types/test/test-utils.d.ts.map +1 -0
- package/dist/types/tests/cache-manager.test.d.ts +2 -0
- package/dist/types/tests/cache-manager.test.d.ts.map +1 -0
- package/dist/types/tests/config-core.test.d.ts +2 -0
- package/dist/types/tests/config-core.test.d.ts.map +1 -0
- package/dist/types/tests/config.validators.test.d.ts +2 -0
- package/dist/types/tests/config.validators.test.d.ts.map +1 -0
- package/dist/types/tests/ebay-functions.test.d.ts +2 -0
- package/dist/types/tests/ebay-functions.test.d.ts.map +1 -0
- package/dist/types/tests/site-health-utils.test.d.ts +2 -0
- package/dist/types/tests/site-health-utils.test.d.ts.map +1 -0
- package/package.json +7 -7
- package/dist/types/stories/callout/callout.many.stories.d.ts.map +0 -1
- package/dist/types/stories/callout/callout.stories.d.ts.map +0 -1
- package/dist/types/stories/carousel/carousel-hero.stories.d.ts +0 -22
- package/dist/types/stories/carousel/carousel-hero.stories.d.ts.map +0 -1
- package/dist/types/stories/carousel/carousel-reviews.stories.d.ts.map +0 -1
- package/dist/types/stories/carousel/carousel-workportfolio.stories.d.ts +0 -22
- package/dist/types/stories/carousel/carousel-workportfolio.stories.d.ts.map +0 -1
- package/dist/types/stories/carousel/carousel.stories.d.ts.map +0 -1
- package/dist/types/stories/carousel/tiles.stories.d.ts.map +0 -1
- package/dist/types/stories/cms/contentful.item.stories.d.ts +0 -21
- package/dist/types/stories/cms/contentful.item.stories.d.ts.map +0 -1
- package/dist/types/stories/cms/contentful.items.stories.d.ts +0 -20
- package/dist/types/stories/cms/contentful.items.stories.d.ts.map +0 -1
- package/dist/types/stories/cms/contentful.stories.d.ts.map +0 -1
- package/dist/types/stories/cms/google.reviews.stories.d.ts.map +0 -1
- package/dist/types/stories/cms/gravatar.stories.d.ts.map +0 -1
- package/dist/types/stories/cms/instagram.stories.d.ts.map +0 -1
- package/dist/types/stories/cms/wordpress.stories.d.ts.map +0 -1
- package/dist/types/stories/menu/menu-accordion.stories.d.ts.map +0 -1
- package/dist/types/stories/menu/menu-expando.stories.d.ts.map +0 -1
- package/dist/types/stories/menu/menu-simple.stories.d.ts.map +0 -1
- package/dist/types/stories/nerdjoke.stories.d.ts.map +0 -1
- package/dist/types/stories/seo/seo.404.stories.d.ts.map +0 -1
- package/dist/types/stories/seo/seo.faq-accordion.stories.d.ts.map +0 -1
- package/dist/types/stories/seo/seo.googleanalytics.stories.d.ts.map +0 -1
- package/dist/types/stories/seo/seo.googlesearch.stories.d.ts.map +0 -1
- package/dist/types/stories/seo/seo.metadata.stories.d.ts.map +0 -1
- package/dist/types/stories/seo/seo.schema.stories.d.ts.map +0 -1
- package/dist/types/stories/seo/seo.sitemap.stories.d.ts.map +0 -1
- package/dist/types/stories/structured/buzzword-bingo.stories.d.ts.map +0 -1
- package/dist/types/stories/structured/markdown.stories.d.ts.map +0 -1
- package/dist/types/stories/structured/recipe.stories.d.ts.map +0 -1
- package/dist/types/stories/structured/socialcard.stories.d.ts.map +0 -1
- package/dist/types/stories/structured/timeline.stories.d.ts.map +0 -1
- package/dist/types/tests/setup.d.ts.map +0 -1
- /package/dist/types/stories/{structured → general}/buzzword-bingo.stories.d.ts +0 -0
- /package/dist/types/stories/{nerdjoke.stories.d.ts → general/nerdjoke.stories.d.ts} +0 -0
- /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
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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',
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
35
|
+
// At the top level (serviceName is undefined), k is the service name
|
|
30
36
|
const currentService = serviceName || k;
|
|
31
|
-
if
|
|
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
|
+
}
|