Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
20cbab1
feat(a11y): implement accessibility mobile audit data processing (V1)
Oct 15, 2025
5504dd2
test coverage disabled
dianaapredaa Oct 15, 2025
df1e090
tried to fix it
Oct 17, 2025
6391786
added accessibility mobile to prefixes
Oct 17, 2025
723b647
added a change to fix the mobile report
Oct 17, 2025
aeef498
tried to fix db error
Oct 17, 2025
b1ef3e9
Merge branch 'main' into a11y-mobile-audit-v1
dianaapredaa Oct 20, 2025
7003cd2
database timestamp conflict
dianaapredaa Oct 21, 2025
9ff2ac2
debug logs
dianaapredaa Oct 21, 2025
f139e11
backward compatibility mobile-desktop
dianaapredaa Oct 21, 2025
fd992cd
test env
dianaapredaa Oct 22, 2025
702a519
test env
dianaapredaa Oct 22, 2025
a29d274
Merge branch 'main' into a11y-mobile-audit-v1
dianaapredaa Oct 23, 2025
5f1aacb
disable env
dianaapredaa Oct 23, 2025
f8e97b4
added test coverage
dianaapredaa Oct 23, 2025
831f619
Merge branch 'main' into a11y-mobile-audit-v1
dianaapredaa Oct 24, 2025
65e4a60
skip only step 2 for mobile audit, enable step 3
dianaapredaa Oct 24, 2025
0d9f5de
fix tests
dianaapredaa Oct 24, 2025
9001bb4
delete unnecessary debug logs
dianaapredaa Oct 24, 2025
56c6aa1
fixed tests
dianaapredaa Oct 24, 2025
8090078
Merge branch 'main' into a11y-mobile-audit-v1
alinalex Oct 29, 2025
ec2e474
update spacecat-shared package version
dianaapredaa Oct 30, 2025
ba5b7fa
update shared package version
dianaapredaa Oct 30, 2025
bcbb484
Merge branch 'main' into a11y-mobile-audit-v1
dianaapredaa Nov 3, 2025
c1951c1
Merge branch 'main' into a11y-mobile-audit-v1
dianaapredaa Nov 3, 2025
0011fc9
fixed test
dianaapredaa Nov 3, 2025
db25c36
Resolve merge conflict with remote
dianaapredaa Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/accessibility/handler-desktop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { Audit } from '@adobe/spacecat-shared-data-access';
import { AuditBuilder } from '../common/audit-builder.js';
import {
processImportStep,
scrapeAccessibilityData,
createProcessAccessibilityOpportunitiesWithDevice,
} from './handler.js';

const { AUDIT_STEP_DESTINATIONS } = Audit;

// Desktop-specific scraping function
async function scrapeAccessibilityDataDesktop(context) {
return scrapeAccessibilityData(context, 'desktop');
}

export default new AuditBuilder()
.addStep(
'processImport',
processImportStep,
AUDIT_STEP_DESTINATIONS.IMPORT_WORKER,
)
.addStep(
'scrapeAccessibilityData',
scrapeAccessibilityDataDesktop,
AUDIT_STEP_DESTINATIONS.CONTENT_SCRAPER,
)
.addStep('processAccessibilityOpportunities', createProcessAccessibilityOpportunitiesWithDevice('desktop'))
.build();
40 changes: 40 additions & 0 deletions src/accessibility/handler-mobile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { Audit } from '@adobe/spacecat-shared-data-access';
import { AuditBuilder } from '../common/audit-builder.js';
import {
processImportStep,
scrapeAccessibilityData,
createProcessAccessibilityOpportunitiesWithDevice,
} from './handler.js';

const { AUDIT_STEP_DESTINATIONS } = Audit;

// Mobile-specific scraping function
async function scrapeAccessibilityDataMobile(context) {
return scrapeAccessibilityData(context, 'mobile');
}

export default new AuditBuilder()
.addStep(
'processImport',
processImportStep,
AUDIT_STEP_DESTINATIONS.IMPORT_WORKER,
)
.addStep(
'scrapeAccessibilityData',
scrapeAccessibilityDataMobile,
AUDIT_STEP_DESTINATIONS.CONTENT_SCRAPER,
)
.addStep('processAccessibilityOpportunities', createProcessAccessibilityOpportunitiesWithDevice('mobile'))
.build();
162 changes: 160 additions & 2 deletions src/accessibility/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function processImportStep(context) {
}

// First step: sends a message to the content scraper to generate accessibility audits
export async function scrapeAccessibilityData(context) {
export async function scrapeAccessibilityData(context, deviceType = 'desktop') {
const {
site, log, finalUrl, env, s3Client, dataAccess,
} = context;
Expand All @@ -60,7 +60,7 @@ export async function scrapeAccessibilityData(context) {
error: errorMsg,
};
}
log.debug(`[A11yAudit] Step 1: Preparing content scrape for accessibility audit for ${site.getBaseURL()} with siteId ${siteId}`);
log.debug(`[A11yAudit] Step 1: Preparing content scrape for ${deviceType} accessibility audit for ${site.getBaseURL()} with siteId ${siteId}`);

let urlsToScrape = [];
urlsToScrape = await getUrlsForAudit(s3Client, bucketName, siteId, log);
Expand Down Expand Up @@ -108,6 +108,7 @@ export async function scrapeAccessibilityData(context) {

// The first step MUST return auditResult and fullAuditRef.
// fullAuditRef could point to where the raw scraped data will be stored (e.g., S3 path).
const storagePrefix = deviceType === 'mobile' ? 'accessibility-mobile' : 'accessibility';
return {
auditResult: {
status: 'SCRAPING_REQUESTED',
Expand All @@ -121,6 +122,8 @@ export async function scrapeAccessibilityData(context) {
jobId: siteId,
processingType: AUDIT_TYPE_ACCESSIBILITY,
options: {
storagePrefix,
deviceType,
accessibilityScrapingParams,
},
};
Expand Down Expand Up @@ -252,6 +255,161 @@ export async function processAccessibilityOpportunities(context) {
};
}

// Factory function to create device-specific processing function
export function createProcessAccessibilityOpportunitiesWithDevice(deviceType) {
return async function processAccessibilityOpportunitiesWithDevice(context) {
const {
site, log, s3Client, env, dataAccess, sqs,
} = context;
const siteId = site.getId();
const version = new Date().toISOString().split('T')[0];
const outputKey = deviceType === 'mobile' ? `accessibility-mobile/${siteId}/${version}-final-result.json` : `accessibility/${siteId}/${version}-final-result.json`;

// Get the S3 bucket name from config or environment
const bucketName = env.S3_SCRAPER_BUCKET_NAME;
if (!bucketName) {
const errorMsg = 'Missing S3 bucket configuration for accessibility audit';
log.error(`[A11yProcessingError] ${errorMsg}`);
return {
status: 'PROCESSING_FAILED',
error: errorMsg,
};
}

log.info(`[A11yAudit] Step 2: Processing scraped data for ${deviceType} on site ${siteId} (${site.getBaseURL()})`);

// Use the accessibility aggregator to process data
let aggregationResult;
try {
aggregationResult = await aggregateAccessibilityData(
s3Client,
bucketName,
siteId,
log,
outputKey,
`${AUDIT_TYPE_ACCESSIBILITY}-${deviceType}`,
version,
);

if (!aggregationResult.success) {
log.error(`[A11yAudit][A11yProcessingError] No data aggregated for ${deviceType} on site ${siteId} (${site.getBaseURL()}): ${aggregationResult.message}`);
return {
status: 'NO_OPPORTUNITIES',
message: aggregationResult.message,
};
}
} catch (error) {
log.error(`[A11yAudit][A11yProcessingError] Error processing accessibility data for ${deviceType} on site ${siteId} (${site.getBaseURL()}): ${error.message}`, error);
return {
status: 'PROCESSING_FAILED',
error: error.message,
};
}

// change status to IGNORED for older opportunities for this device type
await updateStatusToIgnored(dataAccess, siteId, log, deviceType);

try {
await generateReportOpportunities(
site,
aggregationResult,
context,
`${AUDIT_TYPE_ACCESSIBILITY}-${deviceType}`,
deviceType,
);
} catch (error) {
log.error(`[A11yAudit][A11yProcessingError] Error generating report opportunities for ${deviceType} on site ${siteId} (${site.getBaseURL()}): ${error.message}`, error);
return {
status: 'PROCESSING_FAILED',
error: error.message,
};
}

// Step 2c: Create individual opportunities (skip for mobile audits)
if (deviceType !== 'mobile') {
try {
await createAccessibilityIndividualOpportunities(
aggregationResult.finalResultFiles.current,
context,
);
log.debug(`[A11yAudit] Individual opportunities created successfully for ${deviceType} on site ${siteId} (${site.getBaseURL()})`);
} catch (error) {
log.error(`[A11yAudit][A11yProcessingError] Error creating individual opportunities for ${deviceType} on site ${siteId} (${site.getBaseURL()}): ${error.message}`, error);
return {
status: 'PROCESSING_FAILED',
error: error.message,
};
}
} else {
log.info(`[A11yAudit] Skipping individual opportunities (Step 2c) for mobile audit on site ${siteId}`);
}

// Step 3: Save a11y metrics to s3 for ALL device types (desktop and mobile)
try {
// Send message to importer-worker to save a11y metrics
await sendRunImportMessage(
sqs,
env.IMPORT_WORKER_QUEUE_URL,
`${A11Y_METRICS_AGGREGATOR_IMPORT_TYPE}_${deviceType}`,
siteId,
{
scraperBucketName: env.S3_SCRAPER_BUCKET_NAME,
importerBucketName: env.S3_IMPORTER_BUCKET_NAME,
version,
urlSourceSeparator: URL_SOURCE_SEPARATOR,
totalChecks: WCAG_CRITERIA_COUNTS.TOTAL,
deviceType,
options: {},
},
);
log.debug(`[A11yAudit] Sent message to importer-worker to save a11y metrics for ${deviceType} on site ${siteId}`);
} catch (error) {
log.error(`[A11yAudit][A11yProcessingError] Error sending message to importer-worker to save a11y metrics for ${deviceType} on site ${siteId} (${site.getBaseURL()}): ${error.message}`, error);
return {
status: 'PROCESSING_FAILED',
error: error.message,
};
}

// Extract key metrics for the audit result summary, filtered by device type
// Subtract 1 for the 'overall' key to get actual URL count
const urlsProcessed = Object.keys(aggregationResult.finalResultFiles.current).length - 1;

// Calculate device-specific metrics from the aggregated data
let deviceSpecificIssues = 0;

Object.entries(aggregationResult.finalResultFiles.current).forEach(([key, urlData]) => {
if (key === 'overall' || !urlData.violations) return;

['critical', 'serious'].forEach((severity) => {
if (urlData.violations[severity]?.items) {
Object.values(urlData.violations[severity].items).forEach((rule) => {
if (rule.htmlData) {
rule.htmlData.forEach((htmlItem) => {
if (htmlItem.deviceTypes?.includes(deviceType)) {
deviceSpecificIssues += 1;
}
});
}
});
}
});
});

log.info(`[A11yAudit] Found ${deviceSpecificIssues} ${deviceType} accessibility issues across ${urlsProcessed} unique URLs for site ${siteId} (${site.getBaseURL()})`);

// Return the final audit result with device-specific metrics and status
return {
status: deviceSpecificIssues > 0 ? 'OPPORTUNITIES_FOUND' : 'NO_OPPORTUNITIES',
opportunitiesFound: deviceSpecificIssues,
urlsProcessed,
deviceType,
summary: `Found ${deviceSpecificIssues} ${deviceType} accessibility issues across ${urlsProcessed} URLs`,
fullReportUrl: outputKey, // Reference to the full report in S3
};
};
}

export default new AuditBuilder()
.withUrlResolver((site) => site.resolveFinalURL())
.addStep(
Expand Down
4 changes: 4 additions & 0 deletions src/accessibility/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,10 @@ export const URL_SOURCE_SEPARATOR = '?source=';
* Prefixes for different audit types
*/
export const AUDIT_PREFIXES = {
'accessibility-mobile': {
logIdentifier: 'A11yAuditMobile',
storagePrefix: 'accessibility-mobile',
},
[Audit.AUDIT_TYPES.ACCESSIBILITY]: {
logIdentifier: 'A11yAudit',
storagePrefix: 'accessibility',
Expand Down
Loading