@elementor/editor-audits 4.3.3 → 4.3.5

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 (59) hide show
  1. package/dist/index.d.mts +11 -1
  2. package/dist/index.d.ts +11 -1
  3. package/dist/index.js +1103 -1146
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1075 -1108
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +17 -15
  8. package/src/api/page-context-client.ts +8 -5
  9. package/src/audits/accessibility-statement.ts +39 -0
  10. package/src/audits/accessibility-widget.ts +41 -0
  11. package/src/audits/cookie-policy.ts +1 -0
  12. package/src/audits/google-consent-mode.ts +4 -7
  13. package/src/audits/images-alt-text.ts +6 -4
  14. package/src/audits/images-inefficient-format.ts +66 -0
  15. package/src/audits/images-too-large.ts +23 -10
  16. package/src/audits/nested-boxed-containers.ts +5 -0
  17. package/src/components/audit-feedback.tsx +4 -1
  18. package/src/components/audit-panel.tsx +13 -13
  19. package/src/components/audit-toggle-icon.tsx +22 -0
  20. package/src/components/fix-violation-with-angie.tsx +18 -7
  21. package/src/components/pages/category-page.tsx +27 -3
  22. package/src/components/pages/overview-page.tsx +24 -29
  23. package/src/components/report-shell.tsx +10 -91
  24. package/src/components/rescan-button.tsx +38 -0
  25. package/src/components/severity-icons.tsx +5 -1
  26. package/src/components/severity-summary-cards.tsx +52 -0
  27. package/src/components/violation-cta-button.tsx +11 -2
  28. package/src/components/violation-icons.tsx +2 -2
  29. package/src/components/violation-row.tsx +120 -62
  30. package/src/hooks/use-audit-report.ts +5 -4
  31. package/src/hooks/use-audit-toggle-props.ts +2 -2
  32. package/src/hooks/use-single-expanded-card.ts +11 -0
  33. package/src/init.ts +2 -0
  34. package/src/register-audits.ts +6 -2
  35. package/src/runner.ts +15 -7
  36. package/src/store/selectors.ts +4 -0
  37. package/src/store/slice.ts +10 -1
  38. package/src/sync-report-staleness.ts +14 -0
  39. package/src/types.ts +11 -1
  40. package/src/utils/all-image-sources.ts +17 -0
  41. package/src/utils/alt-text-image-sources.ts +24 -0
  42. package/src/utils/atomic-background-image-sources.ts +56 -0
  43. package/src/utils/atomic-image-sources.ts +66 -0
  44. package/src/utils/audit-status-summary.ts +0 -57
  45. package/src/utils/background-image-sources.ts +32 -0
  46. package/src/utils/image-alt.ts +3 -1
  47. package/src/utils/image-like-sources.ts +5 -14
  48. package/src/utils/image-size-key.ts +8 -0
  49. package/src/utils/page-attachments.ts +7 -6
  50. package/src/utils/scan-accessibility-widgets.ts +7 -0
  51. package/src/audits/too-many-widgets.ts +0 -53
  52. package/src/components/audit-statuses-section.tsx +0 -65
  53. package/src/components/count-summary-circle.tsx +0 -66
  54. package/src/components/pages/all-audits-page.tsx +0 -65
  55. package/src/components/pages/issues-page.tsx +0 -65
  56. package/src/components/score-bar.tsx +0 -56
  57. package/src/components/score-circle.tsx +0 -60
  58. package/src/components/severity-filter-chips.tsx +0 -57
  59. package/src/utils/score-thresholds.ts +0 -23
@@ -0,0 +1,11 @@
1
+ import { useState } from 'react';
2
+
3
+ export function useSingleExpandedCard( initialId: string | null = null ) {
4
+ const [ expandedId, setExpandedId ] = useState< string | null >( initialId );
5
+
6
+ const toggle = ( id: string ) => {
7
+ setExpandedId( ( current ) => ( current === id ? null : id ) );
8
+ };
9
+
10
+ return { expandedId, toggle };
11
+ }
package/src/init.ts CHANGED
@@ -5,10 +5,12 @@ import { registerAppBarAuditsToggle } from './editor-app-bar';
5
5
  import { auditPanel } from './editor-panel';
6
6
  import { registerAllAudits } from './register-audits';
7
7
  import { slice } from './store/slice';
8
+ import { syncReportStaleness } from './sync-report-staleness';
8
9
 
9
10
  export function init(): void {
10
11
  registerSlice( slice );
11
12
  registerAllAudits();
12
13
  registerFloatingPanel( auditPanel.panel );
13
14
  registerAppBarAuditsToggle();
15
+ syncReportStaleness();
14
16
  }
@@ -1,4 +1,6 @@
1
1
  import * as accessibilityPolicy from './audits/accessibility-policy';
2
+ import * as accessibilityStatement from './audits/accessibility-statement';
3
+ import * as accessibilityWidget from './audits/accessibility-widget';
2
4
  import * as cookiePolicy from './audits/cookie-policy';
3
5
  import * as deepNesting from './audits/deep-nesting';
4
6
  import * as defaultDesignSystem from './audits/default-design-system';
@@ -7,6 +9,7 @@ import * as googleConsentMode from './audits/google-consent-mode';
7
9
  import * as headingStructure from './audits/heading-structure';
8
10
  import * as hiddenElements from './audits/hidden-elements';
9
11
  import * as imagesAltText from './audits/images-alt-text';
12
+ import * as imagesInefficientFormat from './audits/images-inefficient-format';
10
13
  import * as imagesTooLarge from './audits/images-too-large';
11
14
  import * as nestedBoxedContainers from './audits/nested-boxed-containers';
12
15
  import * as pageExcerpt from './audits/page-excerpt';
@@ -19,7 +22,6 @@ import * as robotsNoindex from './audits/robots-noindex';
19
22
  import * as scanForCookies from './audits/scan-for-cookies';
20
23
  import * as sectionsAndColumns from './audits/sections-and-columns';
21
24
  import * as siteIdentity from './audits/site-identity';
22
- import * as tooManyWidgets from './audits/too-many-widgets';
23
25
  import { registerAudit } from './registry';
24
26
  import { type Audit } from './types';
25
27
 
@@ -30,7 +32,6 @@ const AUDITS: Audit[] = [
30
32
 
31
33
  hiddenElements.audit,
32
34
  deprecatedWidgets.audit,
33
- tooManyWidgets.audit,
34
35
  sectionsAndColumns.audit,
35
36
  deepNesting.audit,
36
37
  nestedBoxedContainers.audit,
@@ -43,11 +44,14 @@ const AUDITS: Audit[] = [
43
44
  headingStructure.audit,
44
45
  imagesAltText.audit,
45
46
  imagesTooLarge.audit,
47
+ imagesInefficientFormat.audit,
46
48
 
47
49
  robotsNoindex.audit,
48
50
 
49
51
  privacyPolicy.audit,
50
52
  accessibilityPolicy.audit,
53
+ accessibilityStatement.audit,
54
+ accessibilityWidget.audit,
51
55
  cookiePolicy.audit,
52
56
  scanForCookies.audit,
53
57
  googleConsentMode.audit,
package/src/runner.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { getElements } from '@elementor/editor-elements';
1
+ import { getHostDocumentElements, getHostDocumentId } from '@elementor/editor-elements';
2
2
 
3
3
  import { fetchPageContext } from './api/page-context-client';
4
4
  import { getRegisteredAudits } from './registry';
@@ -12,19 +12,27 @@ import {
12
12
  type PageAuditReport,
13
13
  } from './types';
14
14
  import { computeReport } from './utils/compute-report';
15
- import { extractAttachmentIds } from './utils/page-attachments';
15
+ import { fetchRenderedHtml } from './utils/fetch-rendered-html';
16
+ import { extractImageSizeRequests } from './utils/page-attachments';
16
17
  import { readKitSnapshot } from './utils/read-kit-snapshot';
17
18
  import { buildSnapshotTree } from './utils/v1-snapshot';
18
19
 
19
20
  export async function runPageAudit( documentId: number ): Promise< PageAuditReport > {
20
- const tree = buildSnapshotTree( getElements() );
21
- const attachmentIds = extractAttachmentIds( tree );
22
- const pageContext = await fetchPageContext( documentId, attachmentIds );
21
+ if ( documentId !== getHostDocumentId() ) {
22
+ throw new Error( 'runPageAudit: documentId must match the host document.' );
23
+ }
24
+
25
+ const tree = buildSnapshotTree( getHostDocumentElements() );
26
+ const imageSizeRequests = extractImageSizeRequests( tree );
27
+ const pageContext = await fetchPageContext( documentId, imageSizeRequests );
23
28
 
24
29
  const elements: ElementsModelSnapshot = { documentId, tree };
25
- const kit: KitSnapshot = await readKitSnapshot( pageContext.kit_id );
30
+ const [ kit, renderedHtml ]: [ KitSnapshot, string | null ] = await Promise.all( [
31
+ readKitSnapshot( pageContext.kit_id ),
32
+ fetchRenderedHtml( pageContext.frontend_url ),
33
+ ] );
26
34
 
27
- const ctx: AuditContext = { documentId, elements, pageContext, kit };
35
+ const ctx: AuditContext = { documentId, elements, pageContext, kit, renderedHtml };
28
36
  const registered = getRegisteredAudits();
29
37
 
30
38
  const auditResults: AuditRun[] = await Promise.all(
@@ -13,3 +13,7 @@ export function selectReport( state: GlobalState ) {
13
13
  export function selectError( state: GlobalState ) {
14
14
  return state.audits.error;
15
15
  }
16
+
17
+ export function selectIsStale( state: GlobalState ) {
18
+ return state.audits.isStale;
19
+ }
@@ -6,9 +6,10 @@ type SliceState = {
6
6
  status: 'idle' | 'loading' | 'error' | 'ready';
7
7
  report: PageAuditReport | null;
8
8
  error: string | null;
9
+ isStale: boolean;
9
10
  };
10
11
 
11
- const initialState: SliceState = { status: 'idle', report: null, error: null };
12
+ const initialState: SliceState = { status: 'idle', report: null, error: null, isStale: false };
12
13
 
13
14
  export const slice = __createSlice( {
14
15
  name: 'audits',
@@ -21,6 +22,7 @@ export const slice = __createSlice( {
21
22
  runSucceeded( state, action: PayloadAction< PageAuditReport > ) {
22
23
  state.status = 'ready';
23
24
  state.report = action.payload;
25
+ state.isStale = false;
24
26
  },
25
27
  runFailed( state, action: PayloadAction< string > ) {
26
28
  state.status = 'error';
@@ -33,11 +35,18 @@ export const slice = __createSlice( {
33
35
  state.status = 'ready';
34
36
  state.report = action.payload;
35
37
  state.error = null;
38
+ state.isStale = false;
36
39
  },
37
40
  reportCleared( state ) {
38
41
  state.status = 'idle';
39
42
  state.report = null;
40
43
  state.error = null;
44
+ state.isStale = false;
45
+ },
46
+ reportStale( state ) {
47
+ if ( state.report ) {
48
+ state.isStale = true;
49
+ }
41
50
  },
42
51
  },
43
52
  } );
@@ -0,0 +1,14 @@
1
+ import { __privateListenTo as listenTo, commandEndEvent, type CommandEvent } from '@elementor/editor-v1-adapters';
2
+ import { __dispatch as dispatch } from '@elementor/store';
3
+
4
+ import { slice } from './store/slice';
5
+
6
+ export function syncReportStaleness() {
7
+ listenTo( commandEndEvent( 'document/save/set-is-modified' ), ( e ) => {
8
+ const event = e as CommandEvent< { status: boolean } >;
9
+
10
+ if ( event.args?.status ) {
11
+ dispatch( slice.actions.reportStale() );
12
+ }
13
+ } );
14
+ }
package/src/types.ts CHANGED
@@ -23,6 +23,7 @@ export type AuditRun = {
23
23
  export type AuditFailMetadata = {
24
24
  missingAltImageCount?: number;
25
25
  oversizedImageCount?: number;
26
+ inefficientImageFormatCount?: number;
26
27
  };
27
28
 
28
29
  export type AuditResult =
@@ -40,14 +41,17 @@ export type AuditViolation = {
40
41
  angieFix?: boolean;
41
42
  angiePrompt?: string;
42
43
  ctaLabel?: string;
44
+ secondaryCtaLabel?: string;
45
+ secondaryCtaUrl?: string;
43
46
  };
44
47
 
45
48
  export type PageContextResponse = {
46
49
  post_title: string | null;
47
50
  post_excerpt: string | null;
48
51
  featured_image_id: number | null;
52
+ /** Keyed by the composite `buildImageSizeKey` result (`"{attachmentId}:{size}"`), since one attachment can resolve to different files at different sizes. */
49
53
  image_sizes: Record<
50
- number,
54
+ string,
51
55
  {
52
56
  width: number;
53
57
  height: number;
@@ -65,6 +69,10 @@ export type PageContextResponse = {
65
69
  privacy_settings_url: string;
66
70
  ally_plugin_active: boolean;
67
71
  ally_plugin_url: string;
72
+ ally_accessibility_statement_created: boolean;
73
+ ally_accessibility_statement_url: string;
74
+ ally_widget_settings_url: string;
75
+ ally_scan_url: string;
68
76
  cookiez_plugin_active: boolean;
69
77
  cookiez_plugin_url: string;
70
78
  cookiez_plugin_installed: boolean;
@@ -73,6 +81,7 @@ export type PageContextResponse = {
73
81
  cookiez_consent_mode_settings_url: string;
74
82
  image_optimization_plugin_active: boolean;
75
83
  image_optimization_plugin_url: string;
84
+ image_optimization_settings_url: string;
76
85
  frontend_url: string | null;
77
86
  site_identity: {
78
87
  site_name_set: boolean;
@@ -108,6 +117,7 @@ export type AuditContext = {
108
117
  elements: ElementsModelSnapshot;
109
118
  pageContext: PageContextResponse;
110
119
  kit: KitSnapshot;
120
+ renderedHtml: string | null;
111
121
  };
112
122
 
113
123
  export type PageAuditReport = {
@@ -0,0 +1,17 @@
1
+ import { type ElementSnapshotNode } from '../types';
2
+ import { walkAtomicBackgroundImageSources } from './atomic-background-image-sources';
3
+ import { walkAtomicImageSources } from './atomic-image-sources';
4
+ import { walkBackgroundImageSources } from './background-image-sources';
5
+ import { type ImageLikeMedia, walkImageLikeSources } from './image-like-sources';
6
+
7
+ export type ImageSourceVisit = {
8
+ node: ElementSnapshotNode;
9
+ media: ImageLikeMedia;
10
+ };
11
+
12
+ export function walkAllImageSources( tree: ElementSnapshotNode[], visit: ( source: ImageSourceVisit ) => void ): void {
13
+ walkImageLikeSources( tree, visit );
14
+ walkBackgroundImageSources( tree, visit );
15
+ walkAtomicImageSources( tree, visit );
16
+ walkAtomicBackgroundImageSources( tree, visit );
17
+ }
@@ -0,0 +1,24 @@
1
+ import { type ElementSnapshotNode } from '../types';
2
+ import { type ImageSourceVisit } from './all-image-sources';
3
+ import { ATOMIC_CONTENT_IMAGE_PROP_KEYS, walkAtomicImageSources } from './atomic-image-sources';
4
+ import { walkImageLikeSources } from './image-like-sources';
5
+
6
+ export function walkAltTextImageSources(
7
+ tree: ElementSnapshotNode[],
8
+ visit: ( source: ImageSourceVisit ) => void
9
+ ): void {
10
+ walkImageLikeSources( tree, visit );
11
+ walkAtomicImageSources( tree, visit, ATOMIC_CONTENT_IMAGE_PROP_KEYS );
12
+ }
13
+
14
+ export function hasAltTextImages( tree: ElementSnapshotNode[] ): boolean {
15
+ let found = false;
16
+
17
+ walkAltTextImageSources( tree, ( { media } ) => {
18
+ if ( media.id || media.url ) {
19
+ found = true;
20
+ }
21
+ } );
22
+
23
+ return found;
24
+ }
@@ -0,0 +1,56 @@
1
+ import { getElementStyles } from '@elementor/editor-elements';
2
+ import {
3
+ backgroundImageOverlayPropTypeUtil,
4
+ backgroundOverlayPropTypeUtil,
5
+ backgroundPropTypeUtil,
6
+ } from '@elementor/editor-props';
7
+ import { type StyleDefinition } from '@elementor/editor-styles';
8
+
9
+ import { type ElementSnapshotNode } from '../types';
10
+ import { extractAtomicImageMedia } from './atomic-image-sources';
11
+ import { type ImageLikeMedia } from './image-like-sources';
12
+ import { walkElements } from './walk';
13
+
14
+ export type AtomicBackgroundImageSourceVisit = {
15
+ node: ElementSnapshotNode;
16
+ media: ImageLikeMedia;
17
+ };
18
+
19
+ export function walkAtomicBackgroundImageSources(
20
+ tree: ElementSnapshotNode[],
21
+ visit: ( source: AtomicBackgroundImageSourceVisit ) => void
22
+ ): void {
23
+ walkElements( tree, ( node ) => {
24
+ const styles = getElementStyles( node.id );
25
+
26
+ if ( ! styles ) {
27
+ return;
28
+ }
29
+
30
+ for ( const media of extractBackgroundImageMedia( styles ) ) {
31
+ visit( { node, media } );
32
+ }
33
+ } );
34
+ }
35
+
36
+ function extractBackgroundImageMedia( styles: Record< string, StyleDefinition > ): ImageLikeMedia[] {
37
+ const sources: ImageLikeMedia[] = [];
38
+
39
+ for ( const style of Object.values( styles ) ) {
40
+ for ( const variant of style.variants ) {
41
+ const background = backgroundPropTypeUtil.extract( variant.props.background );
42
+ const overlays = backgroundOverlayPropTypeUtil.extract( background?.[ 'background-overlay' ] ) ?? [];
43
+
44
+ for ( const overlay of overlays ) {
45
+ const imageOverlay = backgroundImageOverlayPropTypeUtil.extract( overlay );
46
+ const media = extractAtomicImageMedia( imageOverlay?.image );
47
+
48
+ if ( media ) {
49
+ sources.push( media );
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ return sources;
56
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ imageAttachmentIdPropType,
3
+ imagePropTypeUtil,
4
+ imageSrcPropTypeUtil,
5
+ stringPropTypeUtil,
6
+ urlPropTypeUtil,
7
+ } from '@elementor/editor-props';
8
+
9
+ import { type ElementSnapshotNode } from '../types';
10
+ import { type ImageLikeMedia } from './image-like-sources';
11
+ import { walkElements } from './walk';
12
+
13
+ export const ATOMIC_CONTENT_IMAGE_PROP_KEYS = [ 'image' ] as const;
14
+
15
+ const ATOMIC_IMAGE_PROP_KEYS = [ ...ATOMIC_CONTENT_IMAGE_PROP_KEYS, 'poster' ] as const;
16
+
17
+ export type AtomicImageSourceVisit = {
18
+ node: ElementSnapshotNode;
19
+ media: ImageLikeMedia;
20
+ };
21
+
22
+ export function walkAtomicImageSources(
23
+ tree: ElementSnapshotNode[],
24
+ visit: ( source: AtomicImageSourceVisit ) => void,
25
+ propKeys: readonly string[] = ATOMIC_IMAGE_PROP_KEYS
26
+ ): void {
27
+ walkElements( tree, ( node ) => {
28
+ if ( node.elType !== 'widget' ) {
29
+ return;
30
+ }
31
+
32
+ for ( const key of propKeys ) {
33
+ const media = extractAtomicImageMedia( node.settings[ key ] );
34
+
35
+ if ( media ) {
36
+ visit( { node, media } );
37
+ }
38
+ }
39
+ } );
40
+ }
41
+
42
+ export function extractAtomicImageMedia( imageProp: unknown ): ImageLikeMedia | null {
43
+ const image = imagePropTypeUtil.extract( imageProp );
44
+
45
+ if ( ! image ) {
46
+ return null;
47
+ }
48
+
49
+ const src = imageSrcPropTypeUtil.extract( image.src );
50
+
51
+ if ( ! src ) {
52
+ return null;
53
+ }
54
+
55
+ const id = imageAttachmentIdPropType.extract( src.id ) ?? undefined;
56
+ const url = urlPropTypeUtil.extract( src.url ) ?? undefined;
57
+
58
+ if ( ! id && ! url ) {
59
+ return null;
60
+ }
61
+
62
+ const alt = stringPropTypeUtil.extract( src.alt ) ?? undefined;
63
+ const size = stringPropTypeUtil.extract( image.size ) ?? undefined;
64
+
65
+ return { id, url, alt, size };
66
+ }
@@ -1,12 +1,7 @@
1
- import { type ChipProps } from '@elementor/ui';
2
- import { __ } from '@wordpress/i18n';
3
-
4
1
  import { type AuditCategory, type AuditResult, type AuditRun, type PageAuditReport } from '../types';
5
2
  import { isScoredAudit } from './is-scored-audit';
6
3
  import { sortFailedAuditResults } from './sort-failed-audits';
7
4
 
8
- export type AuditStatusGroup = 'fail' | 'pass' | 'skipped';
9
-
10
5
  export type PartitionedAuditResults = {
11
6
  failed: Array< AuditRun & { result: Extract< AuditResult, { status: 'fail' } > } >;
12
7
  passed: Array< AuditRun & { result: Extract< AuditResult, { status: 'pass' } > } >;
@@ -61,58 +56,6 @@ export function partitionAuditResults(
61
56
  };
62
57
  }
63
58
 
64
- export function auditStatusDisplayCounts( report: PageAuditReport ): Record< AuditStatusGroup, number > {
65
- let pass = 0;
66
- let skipped = 0;
67
- let totalViolations = 0;
68
-
69
- for ( const { audit, result } of report.auditResults ) {
70
- if ( ! isScoredAudit( audit ) ) {
71
- continue;
72
- }
73
-
74
- switch ( result.status ) {
75
- case 'fail':
76
- totalViolations += result.violations.length;
77
- break;
78
- case 'pass':
79
- pass++;
80
- break;
81
- case 'skipped':
82
- skipped++;
83
- break;
84
- }
85
- }
86
-
87
- return {
88
- fail: totalViolations,
89
- pass,
90
- skipped,
91
- };
92
- }
93
-
94
- export function auditStatusColor( status: AuditStatusGroup ): ChipProps[ 'color' ] {
95
- switch ( status ) {
96
- case 'fail':
97
- return 'error';
98
- case 'pass':
99
- return 'success';
100
- case 'skipped':
101
- return 'default';
102
- }
103
- }
104
-
105
- export function auditStatusLabel( status: AuditStatusGroup ): string {
106
- switch ( status ) {
107
- case 'fail':
108
- return __( 'Failed audits', 'elementor' );
109
- case 'pass':
110
- return __( 'Passed audits', 'elementor' );
111
- case 'skipped':
112
- return __( 'Skipped audits', 'elementor' );
113
- }
114
- }
115
-
116
59
  export function getPopulatedCategories(
117
60
  categoryTotals: PageAuditReport[ 'categories' ],
118
61
  categories: readonly AuditCategory[]
@@ -0,0 +1,32 @@
1
+ import { type ElementSnapshotNode } from '../types';
2
+ import { type ImageLikeMedia } from './image-like-sources';
3
+ import { walkElements } from './walk';
4
+
5
+ const BACKGROUND_IMAGE_SETTING_KEYS = [
6
+ 'background_image',
7
+ 'background_hover_image',
8
+ 'background_overlay_image',
9
+ 'background_overlay_hover_image',
10
+ '_background_image',
11
+ '_background_hover_image',
12
+ ] as const;
13
+
14
+ export type BackgroundImageSourceVisit = {
15
+ node: ElementSnapshotNode;
16
+ media: ImageLikeMedia;
17
+ };
18
+
19
+ export function walkBackgroundImageSources(
20
+ tree: ElementSnapshotNode[],
21
+ visit: ( source: BackgroundImageSourceVisit ) => void
22
+ ): void {
23
+ walkElements( tree, ( node ) => {
24
+ for ( const key of BACKGROUND_IMAGE_SETTING_KEYS ) {
25
+ const media = node.settings[ key ] as ImageLikeMedia | undefined;
26
+
27
+ if ( media?.id || media?.url ) {
28
+ visit( { node, media } );
29
+ }
30
+ }
31
+ } );
32
+ }
@@ -1,5 +1,6 @@
1
1
  import { type PageContextResponse } from '../types';
2
2
  import { type ImageLikeMedia } from './image-like-sources';
3
+ import { buildImageSizeKey } from './image-size-key';
3
4
 
4
5
  export function hasMeaningfulAlt( media: ImageLikeMedia, pageContext: PageContextResponse ): boolean {
5
6
  if ( ! media.id && ! media.url ) {
@@ -7,7 +8,8 @@ export function hasMeaningfulAlt( media: ImageLikeMedia, pageContext: PageContex
7
8
  }
8
9
 
9
10
  if ( media.id ) {
10
- const alt = pageContext.image_sizes[ media.id ]?.alt ?? '';
11
+ const key = buildImageSizeKey( { id: media.id, size: media.size } );
12
+ const alt = pageContext.image_sizes[ key ]?.alt ?? '';
11
13
  return alt.trim().length > 0;
12
14
  }
13
15
 
@@ -7,6 +7,7 @@ export type ImageLikeMedia = {
7
7
  id?: number;
8
8
  url?: string;
9
9
  alt?: string;
10
+ size?: string;
10
11
  };
11
12
 
12
13
  export type ImageLikeSourceVisit = {
@@ -14,18 +15,6 @@ export type ImageLikeSourceVisit = {
14
15
  media: ImageLikeMedia;
15
16
  };
16
17
 
17
- export function hasPageImages( tree: ElementSnapshotNode[] ): boolean {
18
- let found = false;
19
-
20
- walkImageLikeSources( tree, ( { media } ) => {
21
- if ( media.id || media.url ) {
22
- found = true;
23
- }
24
- } );
25
-
26
- return found;
27
- }
28
-
29
18
  export function walkImageLikeSources(
30
19
  tree: ElementSnapshotNode[],
31
20
  visit: ( source: ImageLikeSourceVisit ) => void
@@ -51,7 +40,7 @@ function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
51
40
  const image = node.settings.image as ImageLikeMedia | undefined;
52
41
 
53
42
  if ( image?.id || image?.url ) {
54
- sources.push( image );
43
+ sources.push( { ...image, size: node.settings.image_size as string | undefined } );
55
44
  }
56
45
 
57
46
  const gallery = ( node.settings.carousel ?? node.settings.gallery ?? node.settings.wp_gallery ) as
@@ -59,9 +48,11 @@ function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
59
48
  | undefined;
60
49
 
61
50
  if ( Array.isArray( gallery ) ) {
51
+ const gallerySize = node.settings.thumbnail_size as string | undefined;
52
+
62
53
  for ( const item of gallery ) {
63
54
  if ( item?.id || item?.url ) {
64
- sources.push( item );
55
+ sources.push( { ...item, size: gallerySize } );
65
56
  }
66
57
  }
67
58
  }
@@ -0,0 +1,8 @@
1
+ export type ImageSizeRequest = {
2
+ id: number;
3
+ size?: string;
4
+ };
5
+
6
+ export function buildImageSizeKey( { id, size }: ImageSizeRequest ): string {
7
+ return `${ id }:${ size || 'full' }`;
8
+ }
@@ -1,14 +1,15 @@
1
1
  import { type ElementSnapshotNode } from '../types';
2
- import { walkImageLikeSources } from './image-like-sources';
2
+ import { walkAllImageSources } from './all-image-sources';
3
+ import { buildImageSizeKey } from './image-size-key';
3
4
 
4
- export function extractAttachmentIds( tree: ElementSnapshotNode[] ): number[] {
5
- const ids = new Set< number >();
5
+ export function extractImageSizeRequests( tree: ElementSnapshotNode[] ): string[] {
6
+ const keys = new Set< string >();
6
7
 
7
- walkImageLikeSources( tree, ( { media } ) => {
8
+ walkAllImageSources( tree, ( { media } ) => {
8
9
  if ( media.id ) {
9
- ids.add( media.id );
10
+ keys.add( buildImageSizeKey( { id: media.id, size: media.size } ) );
10
11
  }
11
12
  } );
12
13
 
13
- return Array.from( ids ).sort( ( a, b ) => a - b );
14
+ return Array.from( keys ).sort();
14
15
  }
@@ -0,0 +1,7 @@
1
+ // Script sources of known accessibility widgets. Ally is listed first; append third-party
2
+ // widget script sources here (UserWay, AccessiBe, EqualWeb, etc.) once confirmed.
3
+ const ACCESSIBILITY_WIDGET_SCRIPT_SOURCES = [ 'cdn.elementor.com/a11y/widget.js' ];
4
+
5
+ export function hasAccessibilityWidgetScript( html: string ): boolean {
6
+ return ACCESSIBILITY_WIDGET_SCRIPT_SOURCES.some( ( src ) => html.includes( src ) );
7
+ }
@@ -1,53 +0,0 @@
1
- import { getWidgetsCache } from '@elementor/editor-elements';
2
- import { __ } from '@wordpress/i18n';
3
-
4
- import { type Audit, type ElementSnapshotNode } from '../types';
5
- import { walkElements } from '../utils/walk';
6
-
7
- const WIDGET_COUNT_THRESHOLD = 100;
8
-
9
- function isCountableWidget( node: ElementSnapshotNode, widgetsCache: ReturnType< typeof getWidgetsCache > ): boolean {
10
- if ( node.elType === 'widget' ) {
11
- return true;
12
- }
13
-
14
- return !! widgetsCache?.[ node.elType ]?.meta?.is_compound;
15
- }
16
-
17
- export const audit: Audit = {
18
- id: 'audits/too-many-widgets',
19
- title: __( 'Too many widgets', 'elementor' ),
20
- description: __( 'Excessive DOM size caused by too many widgets degrades rendering performance.', 'elementor' ),
21
- fixHint: __( 'Reduce the number of widgets on the page by removing or combining elements.', 'elementor' ),
22
- categories: [ 'best-practices', 'performance' ],
23
- severity: 'warning',
24
- weight: 5,
25
- evaluate: ( ctx ) => {
26
- if ( ctx.elements.tree.length === 0 ) {
27
- return { status: 'skipped', reason: __( 'No elements', 'elementor' ) };
28
- }
29
-
30
- const widgetsCache = getWidgetsCache();
31
- let widgetCount = 0;
32
-
33
- walkElements( ctx.elements.tree, ( node ) => {
34
- if ( isCountableWidget( node, widgetsCache ) ) {
35
- widgetCount++;
36
- }
37
- } );
38
-
39
- if ( widgetCount <= WIDGET_COUNT_THRESHOLD ) {
40
- return { status: 'pass' };
41
- }
42
-
43
- return {
44
- status: 'fail',
45
- violations: [
46
- {
47
- auditId: audit.id,
48
- label: __( 'Page has too many widgets.', 'elementor' ),
49
- },
50
- ],
51
- };
52
- },
53
- };