@elementor/editor-audits 4.3.0-1036
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dist/index.d.mts +122 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +2822 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2801 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +65 -0
- package/src/api/page-context-client.ts +19 -0
- package/src/audits/accessibility-policy.ts +32 -0
- package/src/audits/cookie-policy.ts +32 -0
- package/src/audits/deep-nesting.ts +45 -0
- package/src/audits/default-design-system.ts +32 -0
- package/src/audits/deprecated-widgets.ts +57 -0
- package/src/audits/heading-structure.ts +294 -0
- package/src/audits/hidden-elements.ts +41 -0
- package/src/audits/images-alt-text.ts +47 -0
- package/src/audits/images-too-large.ts +61 -0
- package/src/audits/nested-boxed-containers.ts +44 -0
- package/src/audits/page-excerpt.ts +30 -0
- package/src/audits/page-featured-image.ts +29 -0
- package/src/audits/page-title.ts +48 -0
- package/src/audits/prefer-global-colors.ts +52 -0
- package/src/audits/prefer-global-fonts.ts +50 -0
- package/src/audits/privacy-policy.ts +35 -0
- package/src/audits/robots-noindex.ts +35 -0
- package/src/audits/sections-and-columns.ts +39 -0
- package/src/audits/site-identity.ts +62 -0
- package/src/audits/too-many-widgets.ts +43 -0
- package/src/components/audit-feedback.tsx +178 -0
- package/src/components/audit-panel.tsx +55 -0
- package/src/components/category-icons.ts +17 -0
- package/src/components/count-summary-circle.tsx +66 -0
- package/src/components/fix-violation-with-angie.tsx +50 -0
- package/src/components/issues-category-row.tsx +62 -0
- package/src/components/pages/all-audits-page.tsx +65 -0
- package/src/components/pages/category-page.tsx +50 -0
- package/src/components/pages/error-page.tsx +21 -0
- package/src/components/pages/issues-page.tsx +48 -0
- package/src/components/pages/loading-page.tsx +12 -0
- package/src/components/pages/overview-page.tsx +140 -0
- package/src/components/pages/welcome-page.tsx +48 -0
- package/src/components/promotion-card.tsx +43 -0
- package/src/components/promotions.tsx +69 -0
- package/src/components/report-shell.tsx +110 -0
- package/src/components/score-bar.tsx +56 -0
- package/src/components/score-circle.tsx +60 -0
- package/src/components/severity-icons.tsx +26 -0
- package/src/components/status-section.tsx +47 -0
- package/src/components/subpage-header.tsx +29 -0
- package/src/components/violation-icons.tsx +33 -0
- package/src/components/violation-row.tsx +169 -0
- package/src/constants.ts +35 -0
- package/src/editor-app-bar.tsx +13 -0
- package/src/editor-panel.tsx +19 -0
- package/src/hooks/focus-violation.ts +53 -0
- package/src/hooks/use-audit-report.ts +43 -0
- package/src/hooks/use-audit-toggle-props.ts +17 -0
- package/src/index.ts +14 -0
- package/src/init.ts +14 -0
- package/src/register-audits.ts +56 -0
- package/src/register-promotions.ts +75 -0
- package/src/registry.ts +19 -0
- package/src/runner.ts +44 -0
- package/src/store/index.ts +2 -0
- package/src/store/selectors.ts +15 -0
- package/src/store/slice.ts +42 -0
- package/src/types.ts +112 -0
- package/src/utils/audit-status-summary.ts +112 -0
- package/src/utils/build-angie-prompt.ts +9 -0
- package/src/utils/collect-hardcoded-colors.ts +74 -0
- package/src/utils/collect-hardcoded-fonts.ts +70 -0
- package/src/utils/compute-report.ts +51 -0
- package/src/utils/find-audit-run.ts +5 -0
- package/src/utils/image-alt.ts +19 -0
- package/src/utils/image-like-sources.ts +70 -0
- package/src/utils/keyboard-click.ts +10 -0
- package/src/utils/match-global-by-value.ts +5 -0
- package/src/utils/page-attachments.ts +14 -0
- package/src/utils/read-kit-snapshot.ts +149 -0
- package/src/utils/report-storage.ts +16 -0
- package/src/utils/score-thresholds.ts +23 -0
- package/src/utils/severity-counts.ts +59 -0
- package/src/utils/sort-failed-audits.ts +19 -0
- package/src/utils/v1-snapshot.ts +27 -0
- package/src/utils/walk.ts +17 -0
- package/src/utils/window-config.ts +20 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const RESERVED_SETTING_KEYS = new Set( [ '__globals__', '__dynamic__' ] );
|
|
2
|
+
const REPEATER_ROW_ID_KEY = '_id';
|
|
3
|
+
const FONT_FAMILY_KEY_PATTERN = /font_family/;
|
|
4
|
+
|
|
5
|
+
export type HardcodedFont = {
|
|
6
|
+
path: string;
|
|
7
|
+
value: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function isGlobalFontReference( value: string ): boolean {
|
|
11
|
+
return value.startsWith( 'globals/' );
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isHardcodedFontCandidate( key: string, value: string ): boolean {
|
|
15
|
+
return FONT_FAMILY_KEY_PATTERN.test( key ) && value.trim().length > 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatPath( prefix: string, key: string ): string {
|
|
19
|
+
return prefix ? `${ prefix }.${ key }` : key;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatArrayPath( prefix: string, index: number ): string {
|
|
23
|
+
return `${ prefix }[${ index }]`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function collectHardcodedFonts( settings: Record< string, unknown >, pathPrefix = '' ): HardcodedFont[] {
|
|
27
|
+
const linkedGlobals = ( settings.__globals__ ?? {} ) as Record< string, string >;
|
|
28
|
+
const results: HardcodedFont[] = [];
|
|
29
|
+
|
|
30
|
+
for ( const [ key, value ] of Object.entries( settings ) ) {
|
|
31
|
+
if ( RESERVED_SETTING_KEYS.has( key ) || key === REPEATER_ROW_ID_KEY ) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if ( linkedGlobals[ key ] ) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if ( typeof value === 'string' ) {
|
|
40
|
+
if ( ! isGlobalFontReference( value ) && isHardcodedFontCandidate( key, value ) ) {
|
|
41
|
+
results.push( { path: formatPath( pathPrefix, key ), value } );
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ( Array.isArray( value ) ) {
|
|
47
|
+
const arrayPath = formatPath( pathPrefix, key );
|
|
48
|
+
|
|
49
|
+
value.forEach( ( item, index ) => {
|
|
50
|
+
if ( item && typeof item === 'object' && ! Array.isArray( item ) ) {
|
|
51
|
+
results.push(
|
|
52
|
+
...collectHardcodedFonts(
|
|
53
|
+
item as Record< string, unknown >,
|
|
54
|
+
formatArrayPath( arrayPath, index )
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
} );
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if ( value && typeof value === 'object' ) {
|
|
63
|
+
results.push(
|
|
64
|
+
...collectHardcodedFonts( value as Record< string, unknown >, formatPath( pathPrefix, key ) )
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ALL_CATEGORIES } from '../constants';
|
|
2
|
+
import { type AuditRun, type PageAuditReport } from '../types';
|
|
3
|
+
import { getPopulatedCategories } from './audit-status-summary';
|
|
4
|
+
|
|
5
|
+
type CategoryAccumulator = { totalWeight: number; passedWeight: number; total: number; failed: number };
|
|
6
|
+
|
|
7
|
+
export function computeReport( documentId: number, results: AuditRun[] ): PageAuditReport {
|
|
8
|
+
const accumulators = Object.fromEntries(
|
|
9
|
+
ALL_CATEGORIES.map( ( c ) => [ c, { totalWeight: 0, passedWeight: 0, total: 0, failed: 0 } ] )
|
|
10
|
+
) as Record< string, CategoryAccumulator >;
|
|
11
|
+
|
|
12
|
+
for ( const { audit, result } of results ) {
|
|
13
|
+
if ( result.status === 'skipped' ) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for ( const category of audit.categories ) {
|
|
18
|
+
const acc = accumulators[ category ];
|
|
19
|
+
acc.total++;
|
|
20
|
+
acc.totalWeight += audit.weight;
|
|
21
|
+
|
|
22
|
+
if ( result.status === 'pass' ) {
|
|
23
|
+
acc.passedWeight += audit.weight;
|
|
24
|
+
} else {
|
|
25
|
+
acc.failed++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const categories = Object.fromEntries(
|
|
31
|
+
ALL_CATEGORIES.map( ( c ) => {
|
|
32
|
+
const acc = accumulators[ c ];
|
|
33
|
+
const score = acc.totalWeight === 0 ? 0 : Math.round( ( acc.passedWeight / acc.totalWeight ) * 100 );
|
|
34
|
+
return [ c, { score, failed: acc.failed, total: acc.total } ];
|
|
35
|
+
} )
|
|
36
|
+
) as PageAuditReport[ 'categories' ];
|
|
37
|
+
|
|
38
|
+
const populated = getPopulatedCategories( categories, ALL_CATEGORIES );
|
|
39
|
+
const overall =
|
|
40
|
+
populated.length === 0
|
|
41
|
+
? 0
|
|
42
|
+
: Math.round( populated.reduce( ( sum, c ) => sum + categories[ c ].score, 0 ) / populated.length );
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
documentId,
|
|
46
|
+
runAt: Date.now(),
|
|
47
|
+
overall,
|
|
48
|
+
categories,
|
|
49
|
+
auditResults: results,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type PageContextResponse } from '../types';
|
|
2
|
+
import { type ImageLikeMedia } from './image-like-sources';
|
|
3
|
+
|
|
4
|
+
export function hasMeaningfulAlt( media: ImageLikeMedia, pageContext: PageContextResponse ): boolean {
|
|
5
|
+
if ( ! media.id && ! media.url ) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if ( media.id ) {
|
|
10
|
+
const alt = pageContext.image_sizes[ media.id ]?.alt ?? '';
|
|
11
|
+
return alt.trim().length > 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return ( media.alt ?? '' ).trim().length > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isImageSourcePresent( media: ImageLikeMedia ): boolean {
|
|
18
|
+
return Boolean( media.id || media.url );
|
|
19
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type ElementSnapshotNode } from '../types';
|
|
2
|
+
import { walkElements } from './walk';
|
|
3
|
+
|
|
4
|
+
export const IMAGE_LIKE_WIDGETS = new Set( [ 'image', 'image-box', 'image-carousel', 'gallery', 'image-gallery' ] );
|
|
5
|
+
|
|
6
|
+
export type ImageLikeMedia = {
|
|
7
|
+
id?: number;
|
|
8
|
+
url?: string;
|
|
9
|
+
alt?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ImageLikeSourceVisit = {
|
|
13
|
+
node: ElementSnapshotNode;
|
|
14
|
+
media: ImageLikeMedia;
|
|
15
|
+
};
|
|
16
|
+
|
|
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
|
+
export function walkImageLikeSources(
|
|
30
|
+
tree: ElementSnapshotNode[],
|
|
31
|
+
visit: ( source: ImageLikeSourceVisit ) => void
|
|
32
|
+
): void {
|
|
33
|
+
walkElements( tree, ( node ) => {
|
|
34
|
+
if ( node.elType !== 'widget' ) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if ( ! IMAGE_LIKE_WIDGETS.has( node.widgetType ?? '' ) ) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for ( const media of collectMediaFromNode( node ) ) {
|
|
43
|
+
visit( { node, media } );
|
|
44
|
+
}
|
|
45
|
+
} );
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
|
|
49
|
+
const sources: ImageLikeMedia[] = [];
|
|
50
|
+
|
|
51
|
+
const image = node.settings.image as ImageLikeMedia | undefined;
|
|
52
|
+
|
|
53
|
+
if ( image?.id || image?.url ) {
|
|
54
|
+
sources.push( image );
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const gallery = ( node.settings.carousel ?? node.settings.gallery ?? node.settings.wp_gallery ) as
|
|
58
|
+
| ImageLikeMedia[]
|
|
59
|
+
| undefined;
|
|
60
|
+
|
|
61
|
+
if ( Array.isArray( gallery ) ) {
|
|
62
|
+
for ( const item of gallery ) {
|
|
63
|
+
if ( item?.id || item?.url ) {
|
|
64
|
+
sources.push( item );
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return sources;
|
|
70
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ElementSnapshotNode } from '../types';
|
|
2
|
+
import { walkImageLikeSources } from './image-like-sources';
|
|
3
|
+
|
|
4
|
+
export function extractAttachmentIds( tree: ElementSnapshotNode[] ): number[] {
|
|
5
|
+
const ids = new Set< number >();
|
|
6
|
+
|
|
7
|
+
walkImageLikeSources( tree, ( { media } ) => {
|
|
8
|
+
if ( media.id ) {
|
|
9
|
+
ids.add( media.id );
|
|
10
|
+
}
|
|
11
|
+
} );
|
|
12
|
+
|
|
13
|
+
return Array.from( ids ).sort( ( a, b ) => a - b );
|
|
14
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { type KitSnapshot } from '../types';
|
|
2
|
+
|
|
3
|
+
type GlobalColorApiItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
value?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type GlobalTypographyApiItem = {
|
|
10
|
+
id: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
value?: {
|
|
13
|
+
typography_font_family?: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type KitColorRow = {
|
|
18
|
+
_id: string;
|
|
19
|
+
color: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type KitTypographyRow = {
|
|
24
|
+
_id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
typography_font_family?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type AuditWindow = Window & {
|
|
30
|
+
elementor?: {
|
|
31
|
+
documents?: {
|
|
32
|
+
get: ( id: number ) => { config?: { settings?: { settings?: KitDocumentSettings } } } | undefined;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
$e?: {
|
|
36
|
+
data?: {
|
|
37
|
+
get: (
|
|
38
|
+
command: string
|
|
39
|
+
) => Promise< { data?: Record< string, GlobalColorApiItem | GlobalTypographyApiItem > } >;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type KitDocumentSettings = {
|
|
45
|
+
system_colors?: KitColorRow[];
|
|
46
|
+
custom_colors?: KitColorRow[];
|
|
47
|
+
system_typography?: KitTypographyRow[];
|
|
48
|
+
custom_typography?: KitTypographyRow[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type GlobalWithValue = { value: string };
|
|
52
|
+
|
|
53
|
+
function hasNonEmptyGlobalValue< T extends GlobalWithValue >( item: T ): boolean {
|
|
54
|
+
return item.value.length > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function readKitSnapshot( kitId: number ): Promise< KitSnapshot > {
|
|
58
|
+
const [ colorsFromApi, fontsFromApi ] = await Promise.all( [
|
|
59
|
+
readGlobalsFromApi( 'globals/colors', mapApiColor, hasNonEmptyGlobalValue ),
|
|
60
|
+
readGlobalsFromApi( 'globals/typography', mapApiTypography, hasNonEmptyGlobalValue ),
|
|
61
|
+
] );
|
|
62
|
+
|
|
63
|
+
const documentSettings =
|
|
64
|
+
colorsFromApi.length === 0 || fontsFromApi.length === 0 ? getKitDocumentSettings( kitId ) : null;
|
|
65
|
+
|
|
66
|
+
const colors = colorsFromApi.length > 0 ? colorsFromApi : readColorsFromKitDocumentSettings( documentSettings );
|
|
67
|
+
const fonts = fontsFromApi.length > 0 ? fontsFromApi : readFontsFromKitDocumentSettings( documentSettings );
|
|
68
|
+
|
|
69
|
+
return { id: kitId, globals: { colors, fonts } };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readGlobalsFromApi< TApiItem, TGlobal extends GlobalWithValue >(
|
|
73
|
+
command: string,
|
|
74
|
+
mapItem: ( item: TApiItem ) => TGlobal,
|
|
75
|
+
hasValue: ( item: TGlobal ) => boolean
|
|
76
|
+
): Promise< TGlobal[] > {
|
|
77
|
+
const $e = ( window as AuditWindow ).$e;
|
|
78
|
+
|
|
79
|
+
if ( ! $e?.data?.get ) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const result = await $e.data.get( command );
|
|
85
|
+
const data = result?.data ?? {};
|
|
86
|
+
|
|
87
|
+
return Object.values( data )
|
|
88
|
+
.map( ( item ) => mapItem( item as TApiItem ) )
|
|
89
|
+
.filter( hasValue );
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function mapApiColor( item: GlobalColorApiItem ): KitSnapshot[ 'globals' ][ 'colors' ][ number ] {
|
|
96
|
+
return {
|
|
97
|
+
id: item.id,
|
|
98
|
+
value: item.value ?? '',
|
|
99
|
+
title: item.title ?? item.id,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function mapApiTypography( item: GlobalTypographyApiItem ): KitSnapshot[ 'globals' ][ 'fonts' ][ number ] {
|
|
104
|
+
return {
|
|
105
|
+
id: item.id,
|
|
106
|
+
value: item.value?.typography_font_family ?? '',
|
|
107
|
+
title: item.title ?? item.id,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readColorsFromKitDocumentSettings(
|
|
112
|
+
settings: KitDocumentSettings | null
|
|
113
|
+
): KitSnapshot[ 'globals' ][ 'colors' ] {
|
|
114
|
+
if ( ! settings ) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const rows = [ ...( settings.system_colors ?? [] ), ...( settings.custom_colors ?? [] ) ];
|
|
119
|
+
|
|
120
|
+
return rows
|
|
121
|
+
.map( ( row ) => ( {
|
|
122
|
+
id: row._id,
|
|
123
|
+
value: row.color,
|
|
124
|
+
title: row.title ?? row._id,
|
|
125
|
+
} ) )
|
|
126
|
+
.filter( hasNonEmptyGlobalValue );
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readFontsFromKitDocumentSettings( settings: KitDocumentSettings | null ): KitSnapshot[ 'globals' ][ 'fonts' ] {
|
|
130
|
+
if ( ! settings ) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const rows = [ ...( settings.system_typography ?? [] ), ...( settings.custom_typography ?? [] ) ];
|
|
135
|
+
|
|
136
|
+
return rows
|
|
137
|
+
.map( ( row ) => ( {
|
|
138
|
+
id: row._id,
|
|
139
|
+
value: row.typography_font_family ?? '',
|
|
140
|
+
title: row.title ?? row._id,
|
|
141
|
+
} ) )
|
|
142
|
+
.filter( hasNonEmptyGlobalValue );
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getKitDocumentSettings( kitId: number ): KitDocumentSettings | null {
|
|
146
|
+
const document = ( window as AuditWindow ).elementor?.documents?.get?.( kitId );
|
|
147
|
+
|
|
148
|
+
return document?.config?.settings?.settings ?? null;
|
|
149
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getSessionStorageItem, setSessionStorageItem } from '@elementor/session';
|
|
2
|
+
|
|
3
|
+
import { REPORT_STORAGE_KEY_PREFIX } from '../constants';
|
|
4
|
+
import { type PageAuditReport } from '../types';
|
|
5
|
+
|
|
6
|
+
function getStorageKey( documentId: number ): string {
|
|
7
|
+
return `${ REPORT_STORAGE_KEY_PREFIX }/${ documentId }`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getPersistedReport( documentId: number ): PageAuditReport | null {
|
|
11
|
+
return getSessionStorageItem< PageAuditReport >( getStorageKey( documentId ) ) ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function persistReport( documentId: number, report: PageAuditReport ): void {
|
|
15
|
+
setSessionStorageItem( getStorageKey( documentId ), report );
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
export const GOOD_THRESHOLD = 90;
|
|
4
|
+
export const OK_THRESHOLD = 50;
|
|
5
|
+
|
|
6
|
+
export type ScoreColor = 'success' | 'warning' | 'error';
|
|
7
|
+
|
|
8
|
+
export type ScoreTier = {
|
|
9
|
+
color: ScoreColor;
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function getScoreTier( score: number ): ScoreTier {
|
|
14
|
+
if ( score >= GOOD_THRESHOLD ) {
|
|
15
|
+
return { color: 'success', label: __( 'Good', 'elementor' ) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if ( score >= OK_THRESHOLD ) {
|
|
19
|
+
return { color: 'warning', label: __( 'Needs work', 'elementor' ) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { color: 'error', label: __( 'At risk', 'elementor' ) };
|
|
23
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type AuditCategory, type AuditSeverity, type PageAuditReport } from '../types';
|
|
4
|
+
|
|
5
|
+
export type SeverityCounts = Record< AuditSeverity, number >;
|
|
6
|
+
|
|
7
|
+
export const ALL_SEVERITIES = [ 'error', 'warning', 'info' ] as const;
|
|
8
|
+
|
|
9
|
+
export function countSeverities( report: PageAuditReport, category?: AuditCategory ): SeverityCounts {
|
|
10
|
+
const counts: SeverityCounts = { error: 0, warning: 0, info: 0 };
|
|
11
|
+
|
|
12
|
+
for ( const { audit, result } of report.auditResults ) {
|
|
13
|
+
if ( result.status !== 'fail' ) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if ( category && ! audit.categories.includes( category ) ) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
counts[ audit.severity ] += result.violations.length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return counts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function severityPluralLabel( severity: AuditSeverity ): string {
|
|
28
|
+
switch ( severity ) {
|
|
29
|
+
case 'error':
|
|
30
|
+
return __( 'Errors', 'elementor' );
|
|
31
|
+
case 'warning':
|
|
32
|
+
return __( 'Warnings', 'elementor' );
|
|
33
|
+
case 'info':
|
|
34
|
+
return __( 'Info', 'elementor' );
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function severityRemainingCountLabel( severity: AuditSeverity, count: number ): string {
|
|
39
|
+
switch ( severity ) {
|
|
40
|
+
case 'error':
|
|
41
|
+
return sprintf(
|
|
42
|
+
/* translators: %d: number of remaining error violations. */
|
|
43
|
+
__( '%d errors', 'elementor' ),
|
|
44
|
+
count
|
|
45
|
+
);
|
|
46
|
+
case 'warning':
|
|
47
|
+
return sprintf(
|
|
48
|
+
/* translators: %d: number of remaining warning violations. */
|
|
49
|
+
__( '%d warnings', 'elementor' ),
|
|
50
|
+
count
|
|
51
|
+
);
|
|
52
|
+
case 'info':
|
|
53
|
+
return sprintf(
|
|
54
|
+
/* translators: %d: number of remaining info violations. */
|
|
55
|
+
__( '%d info', 'elementor' ),
|
|
56
|
+
count
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AuditMeta, type AuditSeverity } from '../types';
|
|
2
|
+
|
|
3
|
+
const SEVERITY_SORTING_RANK: Record< AuditSeverity, number > = {
|
|
4
|
+
error: 0,
|
|
5
|
+
warning: 1,
|
|
6
|
+
info: 2,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function sortFailedAuditResults< T extends { audit: AuditMeta } >( results: T[] ): T[] {
|
|
10
|
+
return [ ...results ].sort( ( a, b ) => {
|
|
11
|
+
const rankDiff = SEVERITY_SORTING_RANK[ a.audit.severity ] - SEVERITY_SORTING_RANK[ b.audit.severity ];
|
|
12
|
+
|
|
13
|
+
if ( rankDiff !== 0 ) {
|
|
14
|
+
return rankDiff;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return a.audit.title.localeCompare( b.audit.title );
|
|
18
|
+
} );
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type V1Element } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
import { type ElementSnapshotNode } from '../types';
|
|
4
|
+
|
|
5
|
+
export function buildSnapshotTree( elements: V1Element[] ): ElementSnapshotNode[] {
|
|
6
|
+
if ( elements.length === 0 ) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const root = elements[ 0 ];
|
|
11
|
+
const children = root.children ?? [];
|
|
12
|
+
|
|
13
|
+
return children.map( toSnapshot );
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toSnapshot( element: V1Element ): ElementSnapshotNode {
|
|
17
|
+
const model = element.model;
|
|
18
|
+
const settings = element.settings?.toJSON?.() ?? {};
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
id: element.id,
|
|
22
|
+
elType: ( model.get( 'elType' ) as string ) ?? '',
|
|
23
|
+
widgetType: model.get( 'widgetType' ) as string | undefined,
|
|
24
|
+
settings: settings as Record< string, unknown >,
|
|
25
|
+
elements: ( element.children ?? [] ).map( toSnapshot ),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ElementSnapshotNode } from '../types';
|
|
2
|
+
|
|
3
|
+
export type WalkVisitor = ( node: ElementSnapshotNode, parents: readonly ElementSnapshotNode[] ) => void;
|
|
4
|
+
|
|
5
|
+
export function walkElements( tree: ElementSnapshotNode[], visit: WalkVisitor ): void {
|
|
6
|
+
const parents: ElementSnapshotNode[] = [];
|
|
7
|
+
recurse( tree, parents, visit );
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function recurse( nodes: ElementSnapshotNode[], parents: ElementSnapshotNode[], visit: WalkVisitor ): void {
|
|
11
|
+
for ( const node of nodes ) {
|
|
12
|
+
visit( node, parents );
|
|
13
|
+
parents.push( node );
|
|
14
|
+
recurse( node.elements, parents, visit );
|
|
15
|
+
parents.pop();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type WindowConfig = {
|
|
2
|
+
restNamespace: string;
|
|
3
|
+
nonce: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface Window {
|
|
8
|
+
elementorAudits?: WindowConfig;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getWindowConfig(): WindowConfig {
|
|
13
|
+
const config = window.elementorAudits;
|
|
14
|
+
|
|
15
|
+
if ( ! config ) {
|
|
16
|
+
return { restNamespace: 'elementor/v1', nonce: '' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return config;
|
|
20
|
+
}
|