├── VERSION ├── src ├── AccessibilityReporter │ ├── client │ │ ├── src │ │ │ ├── vite-env.d.ts │ │ │ ├── Api │ │ │ │ ├── index.ts │ │ │ │ ├── client.gen.ts │ │ │ │ ├── types.gen.ts │ │ │ │ └── sdk.gen.ts │ │ │ ├── Interface │ │ │ │ ├── ITestPage.ts │ │ │ │ ├── IResults.ts │ │ │ │ ├── IViolationPreview.ts │ │ │ │ └── IPageResult.ts │ │ │ ├── Enums │ │ │ │ └── page-state.ts │ │ │ ├── Modals │ │ │ │ ├── manifests.ts │ │ │ │ └── detail │ │ │ │ │ ├── accessibilityreporter.detail.modal.token.ts │ │ │ │ │ └── accessibilityreporter.detail.element.ts │ │ │ ├── Dashboards │ │ │ │ ├── manifests.ts │ │ │ │ └── accessibilityreporter.dashboard.element.ts │ │ │ ├── Services │ │ │ │ ├── accessibility-reporter-api.service.ts │ │ │ │ └── accessibility-reporter.service.ts │ │ │ ├── Conditions │ │ │ │ ├── manifests.ts │ │ │ │ ├── accessibilityreporter.condition.templateset.ts │ │ │ │ └── accessibilityreporter.condition.usergrouphasaccess.ts │ │ │ ├── Components │ │ │ │ ├── ar-logo.ts │ │ │ │ ├── ar-pre-test.ts │ │ │ │ ├── ar-errored.ts │ │ │ │ ├── ar-running-tests.ts │ │ │ │ ├── ar-score.ts │ │ │ │ ├── ar-chart.ts │ │ │ │ └── ar-has-results.ts │ │ │ ├── WorkspaceView │ │ │ │ ├── manifests.ts │ │ │ │ └── accessibilityreporter.workspaceview.element.ts │ │ │ ├── index.ts │ │ │ └── Styles │ │ │ │ └── general.ts │ │ ├── .vscode │ │ │ └── extensions.json │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── public │ │ │ └── umbraco-package.json │ │ ├── vite.config.ts │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── scripts │ │ │ └── generate-openapi.js │ ├── buildTransitive │ │ └── Umbraco.Community.AccessibilityReporter.props │ ├── Controllers │ │ └── Umbraco │ │ │ ├── AccessibilityReporterControllerBase.cs │ │ │ ├── ConfigApiController.cs │ │ │ └── DirectoryApiController.cs │ ├── Infrastructure │ │ ├── Config │ │ │ ├── AccessibilityReporterSettingsFactory.cs │ │ │ └── AccessibilityReporterAppSettings.cs │ │ ├── AccessibilityReporterDashboard.cs │ │ ├── AccessibilityReporterFactory.cs │ │ └── AccessibilityReporterComposer.cs │ ├── appsettings-schema.AccessibilityReporter.json │ └── AccessibilityReporter.csproj ├── AccessibilityReporter.Website │ ├── wwwroot │ │ └── favicon.ico │ ├── Views │ │ ├── Page.cshtml │ │ ├── _ViewImports.cshtml │ │ └── Partials │ │ │ ├── blockgrid │ │ │ ├── default.cshtml │ │ │ ├── areas.cshtml │ │ │ ├── area.cshtml │ │ │ └── items.cshtml │ │ │ └── blocklist │ │ │ └── default.cshtml │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ ├── AccessibilityReporter.Website.csproj │ ├── appsettings.json │ └── .gitignore ├── .editorconfig ├── AccessibilityReporter.Services │ ├── Interfaces │ │ ├── ITestableNodesService.cs │ │ ├── ITestableNodesSummaryService.cs │ │ └── INodeUrlService.cs │ ├── Infrastructure │ │ └── AccessibilityReporterServicesComposer.cs │ ├── TestableNodesSummaryService.cs │ ├── NodeUrlService.cs │ ├── AccessibilityReporter.Services.csproj │ └── DefaultTestableNodesService.cs ├── AccessibilityReporter.Core │ ├── Interfaces │ │ └── IAccessibilityReporterSettings.cs │ ├── Models │ │ └── NodeSummary.cs │ └── AccessibilityReporter.Core.csproj ├── build.ps1 └── AccessibilityReporter.sln ├── logos ├── logo.png ├── logo64.png └── logo.svg ├── screenshots ├── detail.png ├── results.png └── dashboard.png ├── .gitignore ├── umbraco-marketplace.json ├── README.md └── LICENSE /VERSION: -------------------------------------------------------------------------------- 1 | 4.0.0 -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /logos/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbegent/umbraco-accessibility-reporter/HEAD/logos/logo.png -------------------------------------------------------------------------------- /logos/logo64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbegent/umbraco-accessibility-reporter/HEAD/logos/logo64.png -------------------------------------------------------------------------------- /screenshots/detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbegent/umbraco-accessibility-reporter/HEAD/screenshots/detail.png -------------------------------------------------------------------------------- /screenshots/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbegent/umbraco-accessibility-reporter/HEAD/screenshots/results.png -------------------------------------------------------------------------------- /screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbegent/umbraco-accessibility-reporter/HEAD/screenshots/dashboard.png -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "runem.lit-plugin" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | indent_style = tab 5 | indent_size = 4 -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Api/index.ts: -------------------------------------------------------------------------------- 1 | // This file is auto-generated by @hey-api/openapi-ts 2 | export * from './types.gen'; 3 | export * from './sdk.gen'; -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbegent/umbraco-accessibility-reporter/HEAD/src/AccessibilityReporter.Website/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Interface/ITestPage.ts: -------------------------------------------------------------------------------- 1 | interface ITestPage { 2 | guid: string; 3 | id: number, 4 | name: string; 5 | docTypeAlias: string; 6 | url: string; 7 | } 8 | 9 | export default ITestPage; -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/Views/Page.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Cms.Web.Common.PublishedModels; 2 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 3 | @{ 4 | Layout = null; 5 | } 6 |

@Model.Value("title")

-------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Enums/page-state.ts: -------------------------------------------------------------------------------- 1 | enum PageState { 2 | PreTest, 3 | RunningTests, 4 | Errored, 5 | HasResults, 6 | Loading, 7 | ManuallyRun, 8 | Loaded 9 | } 10 | 11 | export default PageState; 12 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Interface/IResults.ts: -------------------------------------------------------------------------------- 1 | import IPageResult from "./IPageResult"; 2 | 3 | interface IResults { 4 | startTime: Date; 5 | endTime: Date; 6 | pages: IPageResult[]; 7 | } 8 | 9 | export default IResults; -------------------------------------------------------------------------------- /src/AccessibilityReporter/buildTransitive/Umbraco.Community.AccessibilityReporter.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _ReSharper.* 2 | [Bb]in/ 3 | [Oo]bj/ 4 | *.suo 5 | *.user 6 | *.userprefs 7 | *.cache 8 | *.orig 9 | Thumbs.db 10 | .DS_Store 11 | *.log 12 | .vs 13 | 14 | node_modules 15 | src/AccessibilityReporter/wwwroot/App_Plugins/AccessibilityReporter 16 | src/build.out 17 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Interface/IViolationPreview.ts: -------------------------------------------------------------------------------- 1 | interface IViolationPreview { 2 | id: string; 3 | impact: string; 4 | tags: string[]; 5 | nodes: string[]; 6 | title?: string; 7 | description?: string; 8 | } 9 | 10 | export default IViolationPreview; 11 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Services/Interfaces/ITestableNodesService.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.PublishedContent; 2 | 3 | namespace AccessibilityReporter.Services.Interfaces 4 | { 5 | public interface ITestableNodesService 6 | { 7 | IEnumerable All(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Services/Interfaces/ITestableNodesSummaryService.cs: -------------------------------------------------------------------------------- 1 | using AccessibilityReporter.Core.Models; 2 | 3 | namespace AccessibilityReporter.Services.Interfaces 4 | { 5 | public interface ITestableNodesSummaryService 6 | { 7 | IEnumerable All(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Services/Interfaces/INodeUrlService.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.PublishedContent; 2 | 3 | namespace AccessibilityReporter.Services.Interfaces 4 | { 5 | public interface INodeUrlService 6 | { 7 | string AbsoluteUrl(IPublishedContent content); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Extensions 2 | @using Umbraco.Cms.Web.Common.PublishedModels 3 | @using Umbraco.Cms.Web.Common.Views 4 | @using Umbraco.Cms.Core.Models.PublishedContent 5 | @using Microsoft.AspNetCore.Html 6 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Interface/IPageResult.ts: -------------------------------------------------------------------------------- 1 | import ITestPage from "./ITestPage"; 2 | import IViolationPreview from "./IViolationPreview"; 3 | 4 | interface IPageResult { 5 | violations: IViolationPreview[]; 6 | score: number; 7 | page: ITestPage; 8 | } 9 | 10 | export default IPageResult; -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Modals/manifests.ts: -------------------------------------------------------------------------------- 1 | const detailModal: UmbExtensionManifest = { 2 | type: 'modal', 3 | alias: 'AccessibilityReporter.Modal.Detail', 4 | name: 'Accessibility Reporter Modal - Detail', 5 | element: () => import('./detail/accessibilityreporter.detail.element') 6 | } 7 | export const manifests = [detailModal]; 8 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/Views/Partials/blockgrid/default.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Extensions 2 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 3 | @{ 4 | if (Model?.Any() != true) { return; } 5 | } 6 | 7 |
10 | @await Html.GetBlockGridItemsHtmlAsync(Model) 11 |
12 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/Views/Partials/blocklist/default.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 2 | @{ 3 | if (Model?.Any() != true) { return; } 4 | } 5 |
6 | @foreach (var block in Model) 7 | { 8 | if (block?.ContentUdi == null) { continue; } 9 | var data = block.Content; 10 | 11 | @await Html.PartialAsync("blocklist/Components/" + data.ContentType.Alias, block) 12 | } 13 |
14 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/public/umbraco-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../AccessibilityReporter.Website/umbraco-package-schema.json", 3 | "id": "AccessibilityReporter", 4 | "name": "Accessibility Reporter", 5 | "version": "4.0.0", 6 | "extensions": [ 7 | { 8 | "name": "Accessibility Reporter EntryPoint", 9 | "alias": "AccessibilityReporter.EntryPoint", 10 | "type": "backofficeEntryPoint", 11 | "js": "/App_Plugins/AccessibilityReporter/accessibility-reporter.js" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/Views/Partials/blockgrid/areas.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Extensions 2 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 3 | @{ 4 | if (Model?.Areas.Any() != true) { return; } 5 | } 6 | 7 |
9 | @foreach (var area in Model.Areas) 10 | { 11 | @await Html.GetBlockGridItemAreaHtmlAsync(area) 12 | } 13 |
14 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Core/Interfaces/IAccessibilityReporterSettings.cs: -------------------------------------------------------------------------------- 1 | namespace AccessibilityReporter.Core.Interfaces 2 | { 3 | public interface IAccessibilityReporterSettings 4 | { 5 | string ApiUrl { get; set; } 6 | 7 | string TestBaseUrl { get; set; } 8 | 9 | bool RunTestsAutomatically { get; set; } 10 | 11 | bool IncludeIfNoTemplate { get; set; } 12 | 13 | int MaxPages { get; set; } 14 | 15 | HashSet UserGroups { get; set; } 16 | 17 | HashSet TestsToRun { get; set; } 18 | 19 | HashSet ExcludedDocTypes { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/Views/Partials/blockgrid/area.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Extensions 2 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 3 | 4 |
9 | @await Html.GetBlockGridItemsHtmlAsync(Model) 10 |
11 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | build: { 5 | lib: { 6 | entry: "src/index.ts", // Entrypoint file (registers other manifests) 7 | formats: ["es"], 8 | fileName: "accessibility-reporter", 9 | }, 10 | outDir: "../wwwroot/App_Plugins/AccessibilityReporter", // your web component will be saved in this location 11 | emptyOutDir: true, 12 | sourcemap: true, 13 | rollupOptions: { 14 | external: [/^@umbraco/], 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/Program.cs: -------------------------------------------------------------------------------- 1 | WebApplicationBuilder builder = WebApplication.CreateBuilder(args); 2 | 3 | builder.CreateUmbracoBuilder() 4 | .AddBackOffice() 5 | .AddWebsite() 6 | .AddDeliveryApi() 7 | .AddComposers() 8 | .Build(); 9 | 10 | WebApplication app = builder.Build(); 11 | 12 | await app.BootUmbracoAsync(); 13 | 14 | 15 | app.UseUmbraco() 16 | .WithMiddleware(u => 17 | { 18 | u.UseBackOffice(); 19 | u.UseWebsite(); 20 | }) 21 | .WithEndpoints(u => 22 | { 23 | u.UseBackOfficeEndpoints(); 24 | u.UseWebsiteEndpoints(); 25 | }); 26 | 27 | await app.RunAsync(); 28 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Core/Models/NodeSummary.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.PublishedContent; 2 | 3 | namespace AccessibilityReporter.Core.Models 4 | { 5 | public class NodeSummary 6 | { 7 | public NodeSummary(IPublishedContent content, string url) 8 | { 9 | Guid = content.Key; 10 | Id = content.Id; 11 | Name = content.Name!; 12 | DocTypeAlias = content.ContentType.Alias; 13 | Url = url; 14 | } 15 | 16 | public Guid Guid { get; } 17 | 18 | public int Id { get; } 19 | 20 | public string Name { get; } 21 | 22 | public string DocTypeAlias { get; } 23 | 24 | public string Url { get; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Services/Infrastructure/AccessibilityReporterServicesComposer.cs: -------------------------------------------------------------------------------- 1 | using AccessibilityReporter.Services.Interfaces; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Umbraco.Cms.Core.Composing; 4 | using Umbraco.Cms.Core.DependencyInjection; 5 | 6 | namespace AccessibilityReporter.Services.Infrastructure 7 | { 8 | internal class AccessibilityReporterServicesComposer : IComposer 9 | { 10 | public void Compose(IUmbracoBuilder builder) 11 | { 12 | builder.Services.AddScoped(); 13 | builder.Services.AddScoped(); 14 | builder.Services.AddScoped(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/Controllers/Umbraco/AccessibilityReporterControllerBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Umbraco.Cms.Api.Common.Attributes; 4 | using Umbraco.Cms.Web.Common.Authorization; 5 | using Umbraco.Cms.Web.Common.Routing; 6 | 7 | namespace AccessibilityReporter.Controllers.Umbraco 8 | { 9 | [ApiController] 10 | [BackOfficeRoute("accessibilityreporter/api/v{version:apiVersion}")] 11 | [Authorize(Policy = AuthorizationPolicies.SectionAccessContent)] // Can call the API if the logged in user has access to the 'content' section 12 | [MapToApi("AccessibilityReporter")] 13 | public class AccessibilityReporterControllerBase : ControllerBase 14 | { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Dashboards/manifests.ts: -------------------------------------------------------------------------------- 1 | const dashboard: UmbExtensionManifest = { 2 | alias: 'AccessibilityReporter.Dashboard', 3 | name: 'Accessibility Reporter Dashboard', 4 | type: 'dashboard', 5 | weight: 1, 6 | element: () => import('./accessibilityreporter.dashboard.element.js'), 7 | meta: { 8 | label: 'Accessibility Reporter', 9 | pathname: 'accessibility-reporter' 10 | }, 11 | conditions: [ 12 | { 13 | alias: 'Umb.Condition.SectionAlias', 14 | match: 'Umb.Section.Content' 15 | }, 16 | { 17 | alias: 'AccessibilityReporter.Condition.UserGroupHasAccess' 18 | } 19 | ] 20 | 21 | } 22 | export const manifests = [dashboard]; 23 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "experimentalDecorators": true, 5 | "useDefineForClassFields": false, 6 | "module": "ESNext", 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "strictPropertyInitialization": false, 23 | 24 | "types": [ 25 | "@umbraco-cms/backoffice/extension-types" 26 | ] 27 | }, 28 | "include": ["src"] 29 | } 30 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Services/accessibility-reporter-api.service.ts: -------------------------------------------------------------------------------- 1 | import { AccessibilityReporterAppSettings } from "../api"; 2 | 3 | export default class AccessibilityReporterAPIService { 4 | 5 | static async getIssues(config: AccessibilityReporterAppSettings, testUrl: string, language: string) { 6 | let requestUrl = new URL(config.apiUrl); 7 | requestUrl.searchParams.append("url", testUrl); 8 | if (language) { 9 | requestUrl.searchParams.append("language", language); 10 | } 11 | if (config.testsToRun) { 12 | for (let index = 0; index < config.testsToRun.length; index++) { 13 | requestUrl.searchParams.append("tags", config.testsToRun[index]); 14 | } 15 | } 16 | const response = await fetch(requestUrl.toString()); 17 | const audit = await response.json(); 18 | return audit; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/Infrastructure/Config/AccessibilityReporterSettingsFactory.cs: -------------------------------------------------------------------------------- 1 | using AccessibilityReporter.Core.Interfaces; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace AccessibilityReporter.Infrastructure.Config 6 | { 7 | internal class AccessibilityReporterSettingsFactory 8 | { 9 | internal static IAccessibilityReporterSettings Make(AccessibilityReporterAppSettings settings) 10 | { 11 | if (settings.UserGroups.Any() == false) 12 | { 13 | settings.UserGroups = new HashSet() { "admin", "administrators", "editor", "writer", "translator", "sensitiveData", "sensitive data" }; 14 | } 15 | 16 | if (settings.TestsToRun.Any() == false) 17 | { 18 | settings.TestsToRun = new HashSet() { "wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice" }; 19 | } 20 | 21 | return settings; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Conditions/manifests.ts: -------------------------------------------------------------------------------- 1 | import { ManifestCondition } from "@umbraco-cms/backoffice/extension-api"; 2 | import { TemplateSetCondition } from "./accessibilityreporter.condition.templateset"; 3 | import { UserGroupHasAccessCondition } from "./accessibilityreporter.condition.usergrouphasaccess"; 4 | 5 | const templateSet: ManifestCondition = { 6 | type: "condition", 7 | name: "Accessibility Reporter - Template Set Condition", 8 | alias: "AccessibilityReporter.Condition.TemplateSet", 9 | api: TemplateSetCondition 10 | }; 11 | 12 | const userGroupHasAccess: ManifestCondition = { 13 | type: "condition", 14 | name: "Accessibility Reporter - User Group Has Access Condition", 15 | alias: "AccessibilityReporter.Condition.UserGroupHasAccess", 16 | api: UserGroupHasAccessCondition 17 | } 18 | 19 | export const manifests = [templateSet, userGroupHasAccess]; 20 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:24066", 8 | "sslPort": 44312 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "Umbraco.Web.UI": { 20 | "commandName": "Project", 21 | "dotnetRunMessages": true, 22 | "launchBrowser": true, 23 | "applicationUrl": "https://localhost:44312;http://localhost:24066", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /logos/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "appsettings-schema.json", 3 | "Serilog": { 4 | "MinimumLevel": { 5 | "Default": "Information" 6 | }, 7 | "WriteTo": [ 8 | { 9 | "Name": "Async", 10 | "Args": { 11 | "configure": [ 12 | { 13 | "Name": "Console" 14 | } 15 | ] 16 | } 17 | } 18 | ] 19 | }, 20 | "Umbraco": { 21 | "CMS": { 22 | "Content": { 23 | "MacroErrors": "Throw" 24 | }, 25 | "Hosting": { 26 | "Debug": true 27 | }, 28 | "RuntimeMinification": { 29 | "UseInMemoryCache": true, 30 | "CacheBuster": "Timestamp" 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/Infrastructure/Config/AccessibilityReporterAppSettings.cs: -------------------------------------------------------------------------------- 1 | using AccessibilityReporter.Core.Interfaces; 2 | using System.Collections.Generic; 3 | 4 | namespace AccessibilityReporter.Infrastructure.Config 5 | { 6 | internal class AccessibilityReporterAppSettings : IAccessibilityReporterSettings 7 | { 8 | public static string SectionName = "AccessibilityReporter"; 9 | 10 | public string ApiUrl { get; set; } = string.Empty; 11 | 12 | public string TestBaseUrl { get; set; } = string.Empty; 13 | 14 | public bool RunTestsAutomatically { get; set; } = true; 15 | 16 | public bool IncludeIfNoTemplate { get; set; } = false; 17 | 18 | public int MaxPages { get; set; } = 50; 19 | 20 | public HashSet UserGroups { get; set; } = new HashSet(); 21 | 22 | public HashSet TestsToRun { get; set; } = new HashSet(); 23 | 24 | public HashSet ExcludedDocTypes { get; set; } = new HashSet(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Api/client.gen.ts: -------------------------------------------------------------------------------- 1 | // This file is auto-generated by @hey-api/openapi-ts 2 | 3 | import type { ClientOptions } from './types.gen'; 4 | import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch'; 5 | 6 | /** 7 | * The `createClientConfig()` function will be called on client initialization 8 | * and the returned object will become the client's initial configuration. 9 | * 10 | * You may want to initialize your client this way instead of calling 11 | * `setConfig()`. This is useful for example if you're using Next.js 12 | * to ensure your client always has the correct values. 13 | */ 14 | export type CreateClientConfig = (override?: Config) => Config & T>; 15 | 16 | export const client = createClient(createConfig({ 17 | baseUrl: 'https://localhost:44312' 18 | })); -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accessibility-reporter-client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsc && vite build --emptyOutDir", 8 | "watch": "vite build --watch --emptyOutDir", 9 | "openapi-ts": "node scripts/generate-openapi.js https://localhost:44312/umbraco/swagger/AccessibilityReporter/swagger.json --output src/Api" 10 | }, 11 | "dependencies": { 12 | "chart.js": "^4.4.2", 13 | "chartjs-plugin-datalabels": "^2.2.0", 14 | "date-fns": "^3.6.0", 15 | "lit": "^3.1.2", 16 | "patternomaly": "^1.3.2", 17 | "xlsx": "^0.18.5" 18 | }, 19 | "devDependencies": { 20 | "@hey-api/client-fetch": "^0.10.1", 21 | "@hey-api/openapi-ts": "^0.67.6", 22 | "@types/xlsx": "^0.0.36", 23 | "@umbraco-cms/backoffice": "^16.0.0", 24 | "cross-env": "^7.0.3", 25 | "chalk": "^5.3.0", 26 | "node-fetch": "^3.3.2", 27 | "typescript": "^5.8.3", 28 | "vite": "^6.3.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Services/TestableNodesSummaryService.cs: -------------------------------------------------------------------------------- 1 | using AccessibilityReporter.Core.Models; 2 | using AccessibilityReporter.Services.Interfaces; 3 | using Umbraco.Extensions; 4 | 5 | namespace AccessibilityReporter.Services 6 | { 7 | internal class TestableNodesSummaryService : ITestableNodesSummaryService 8 | { 9 | private readonly ITestableNodesService _testableNodesService; 10 | private readonly INodeUrlService _nodeUrlService; 11 | 12 | public TestableNodesSummaryService(ITestableNodesService testableNodesService, 13 | INodeUrlService nodeUrlService) 14 | { 15 | _testableNodesService = testableNodesService; 16 | _nodeUrlService = nodeUrlService; 17 | } 18 | 19 | public IEnumerable All() 20 | { 21 | var testableNodes = _testableNodesService.All(); 22 | 23 | return testableNodes.Select(content => new NodeSummary(content, _nodeUrlService.AbsoluteUrl(content))); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/Controllers/Umbraco/ConfigApiController.cs: -------------------------------------------------------------------------------- 1 | using AccessibilityReporter.Core.Interfaces; 2 | using Asp.Versioning; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace AccessibilityReporter.Controllers.Umbraco 6 | { 7 | [ApiVersion("1.0")] 8 | [ApiExplorerSettings(GroupName = "Config")] 9 | public class ConfigApiController : AccessibilityReporterControllerBase 10 | { 11 | private IAccessibilityReporterSettings _settings; 12 | 13 | public ConfigApiController(IAccessibilityReporterSettings settings) 14 | { 15 | _settings = settings; 16 | } 17 | 18 | /// 19 | /// Returns the settings for Accessibility Reporter 20 | /// 21 | /// The Accessibility Reporter Settings 22 | [HttpGet("config/current")] 23 | [ProducesResponseType(200)] 24 | public IAccessibilityReporterSettings Current() 25 | { 26 | return _settings; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Components/ar-logo.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, customElement } from "@umbraco-cms/backoffice/external/lit"; 2 | import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; 3 | 4 | @customElement("ar-logo") 5 | export class ARLogoElement extends UmbElementMixin(LitElement) { 6 | 7 | render() { 8 | return html` 9 | 10 | 11 | 12 | 13 | 14 | `; 15 | } 16 | } 17 | 18 | declare global { 19 | interface HTMLElementTagNameMap { 20 | "ar-logo": ARLogoElement; 21 | } 22 | } -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Modals/detail/accessibilityreporter.detail.modal.token.ts: -------------------------------------------------------------------------------- 1 | 2 | import { UmbModalToken } from "@umbraco-cms/backoffice/modal"; 3 | 4 | // The data we pass into the modal 5 | export interface DetailModalData { 6 | result: { 7 | id: string; 8 | impact: string; 9 | tags: string[]; 10 | description: string; 11 | help: string; 12 | helpUrl: string; 13 | nodes: { 14 | any: IssueNode[]; 15 | all: string[]; 16 | none: string[]; 17 | impact: string; 18 | html: string; 19 | target: string[]; 20 | failureSummary: string; 21 | }[]; 22 | }; 23 | }; 24 | 25 | export interface IssueNode { 26 | id: string; 27 | data: string 28 | relatedNodes: IssueNode[]; 29 | impact: string; 30 | message: string; 31 | html: string; 32 | } 33 | 34 | export interface DetailModalValue { 35 | // If the modal is to return any data back on submission 36 | thing: string; 37 | } 38 | 39 | export const ACCESSIBILITY_REPORTER_MODAL_DETAIL = new UmbModalToken('AccessibilityReporter.Modal.Detail', { 40 | modal: { 41 | type: 'sidebar', 42 | size: 'large' // full, large, medium, small 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/Controllers/Umbraco/DirectoryApiController.cs: -------------------------------------------------------------------------------- 1 | using AccessibilityReporter.Core.Models; 2 | using AccessibilityReporter.Services.Interfaces; 3 | using Asp.Versioning; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Collections.Generic; 6 | 7 | namespace AccessibilityReporter.Controllers.Umbraco 8 | { 9 | [ApiVersion("1.0")] 10 | [ApiExplorerSettings(GroupName = "Directory")] 11 | public class DirectoryApiController : AccessibilityReporterControllerBase 12 | { 13 | private ITestableNodesSummaryService _testableNodesSummaryService; 14 | 15 | public DirectoryApiController(ITestableNodesSummaryService testableNodesSummaryService) 16 | { 17 | _testableNodesSummaryService = testableNodesSummaryService; 18 | } 19 | 20 | /// 21 | /// Returns all pages 22 | /// 23 | /// A collection of NodeSummary objects 24 | [HttpGet("pages")] 25 | [ProducesResponseType>(200)] 26 | public IEnumerable Pages() 27 | { 28 | return _testableNodesSummaryService.All(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Services/NodeUrlService.cs: -------------------------------------------------------------------------------- 1 | using AccessibilityReporter.Core.Interfaces; 2 | using AccessibilityReporter.Services.Interfaces; 3 | using Umbraco.Cms.Core.Models.PublishedContent; 4 | using Umbraco.Cms.Core.Routing; 5 | using Umbraco.Extensions; 6 | 7 | namespace AccessibilityReporter.Services 8 | { 9 | internal class NodeUrlService : INodeUrlService 10 | { 11 | private readonly IPublishedUrlProvider _publishedUrlProvider; 12 | private readonly IAccessibilityReporterSettings _settings; 13 | 14 | public NodeUrlService(IPublishedUrlProvider publishedUrlProvider, 15 | IAccessibilityReporterSettings settings) 16 | { 17 | _publishedUrlProvider = publishedUrlProvider; 18 | _settings = settings; 19 | } 20 | 21 | public string AbsoluteUrl(IPublishedContent content) 22 | { 23 | if (string.IsNullOrWhiteSpace(_settings.TestBaseUrl)) 24 | { 25 | return content.Url(_publishedUrlProvider, mode: UrlMode.Absolute); 26 | } 27 | 28 | return $"{_settings.TestBaseUrl.TrimEnd("/")}{content.Url(_publishedUrlProvider, mode: UrlMode.Relative)}"; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/WorkspaceView/manifests.ts: -------------------------------------------------------------------------------- 1 | //import { ManifestWorkspaceView } from "@umbraco-cms/backoffice/extension-registry"; 2 | //import { TemplateSetConditionConfig } from "../Conditions/accessibilityreporter.condition.templateset.js"; 3 | 4 | const workspaceView: UmbExtensionManifest = { 5 | alias: 'AccessibilityReporter.WorkspaceView', 6 | name: 'Accessibility Reporter Workspace View', 7 | type: 'workspaceView', 8 | element: () => import('./accessibilityreporter.workspaceview.element.js'), 9 | weight: 190, 10 | meta: { 11 | icon: 'icon-people', 12 | label: 'Accessibility', 13 | pathname: 'accessibility-reporter', 14 | }, 15 | conditions: [ 16 | { 17 | alias: 'Umb.Condition.WorkspaceAlias', 18 | match: 'Umb.Workspace.Document', 19 | }, 20 | { 21 | alias: 'AccessibilityReporter.Condition.TemplateSet' // This has no config as we check against user config calling the C# API 22 | }, 23 | { 24 | alias: 'AccessibilityReporter.Condition.UserGroupHasAccess' // This has no config as we check against user config calling the C# API 25 | } 26 | ], 27 | } 28 | export const manifests = [workspaceView]; 29 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/Infrastructure/AccessibilityReporterDashboard.cs: -------------------------------------------------------------------------------- 1 | //using Umbraco.Cms.Core.Composing; 2 | //using Umbraco.Cms.Core.Dashboards; 3 | //using Umbraco.Cms.Core; 4 | //using System.Linq; 5 | //using AccessibilityReporter.Core.Interfaces; 6 | 7 | //namespace AccessibilityReporter.Infrastructure 8 | //{ 9 | // [Weight(50)] 10 | // public class AccessibilityReporterDashboard : IDashboard 11 | // { 12 | // private readonly IAccessibilityReporterSettings _settings; 13 | 14 | // public AccessibilityReporterDashboard(IAccessibilityReporterSettings settings) 15 | // { 16 | // _settings = settings; 17 | // } 18 | 19 | // public string Alias => "accessibilityDashboard"; 20 | 21 | // public string[] Sections => new[] 22 | // { 23 | // Constants.Applications.Content 24 | // }; 25 | 26 | // public string View => "/App_Plugins/AccessibilityReporter/accessibility-reporter-dashboard.html"; 27 | 28 | // public IAccessRule[] AccessRules => _settings.UserGroups 29 | // .Select(userGroup => new AccessRule 30 | // { 31 | // Type = AccessRuleType.Grant, 32 | // Value = userGroup 33 | // }) 34 | // .ToArray(); 35 | 36 | // } 37 | //} 38 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Components/ar-pre-test.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, customElement, property } from "@umbraco-cms/backoffice/external/lit"; 2 | import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; 3 | import './ar-logo'; 4 | import { generalStyles } from "../Styles/general"; 5 | 6 | @customElement("ar-pre-test") 7 | export class ARPreTestElement extends UmbElementMixin(LitElement) { 8 | 9 | @property() 10 | onRunTests = () => { }; 11 | 12 | render() { 13 | return html` 14 | 15 | 16 |
17 | 18 |

Accessibility Reporter

19 |
20 |

Start running accessibility tests against multiple pages by using the button below.

21 |

While the tests are running please stay on this page.

22 | Run tests 23 |
24 |
25 | `; 26 | } 27 | 28 | static styles = [ 29 | generalStyles 30 | ]; 31 | } 32 | 33 | declare global { 34 | interface HTMLElementTagNameMap { 35 | "ar-pre-test": ARPreTestElement; 36 | } 37 | } -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Components/ar-errored.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, customElement, property } from "@umbraco-cms/backoffice/external/lit"; 2 | import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; 3 | import './ar-logo'; 4 | import { generalStyles } from "../Styles/general"; 5 | 6 | @customElement("ar-errored") 7 | export class ARErroredElement extends UmbElementMixin(LitElement) { 8 | 9 | @property() 10 | onRunTests = () => { }; 11 | 12 | render() { 13 | return html` 14 | 15 | 16 |
17 | 18 |

Accessibility Report errored

19 |
20 |

Accessibility Reporter only works for URLs that are accessible publicly.

21 |

If your page is publicly accessible, please try using the "Rerun Tests" button below or refreshing this page to run the accessibility report again.

22 | Rerun tests 23 |
24 |
25 | `; 26 | } 27 | 28 | static styles = [ 29 | generalStyles 30 | ]; 31 | } 32 | 33 | declare global { 34 | interface HTMLElementTagNameMap { 35 | "ar-errored": ARErroredElement; 36 | } 37 | } -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/AccessibilityReporter.Website.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | enable 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | true 22 | 23 | 24 | 25 | 26 | false 27 | false 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "appsettings-schema.json", 3 | "Serilog": { 4 | "MinimumLevel": { 5 | "Default": "Information", 6 | "Override": { 7 | "Microsoft": "Warning", 8 | "Microsoft.Hosting.Lifetime": "Information", 9 | "System": "Warning" 10 | } 11 | } 12 | }, 13 | "Umbraco": { 14 | "CMS": { 15 | "Global": { 16 | "Id": "e0f7402f-5b2f-4f5b-a621-77bc48970960", 17 | "SanitizeTinyMce": true 18 | }, 19 | "Content": { 20 | "AllowEditInvariantFromNonDefault": true, 21 | "ContentVersionCleanupPolicy": { 22 | "EnableCleanup": true 23 | } 24 | }, 25 | "Unattended": { 26 | "InstallUnattended": true, 27 | "UnattendedUserName": "Test User", 28 | "UnattendedUserEmail": "test@test.com", 29 | "UnattendedUserPassword": "password1234", 30 | "UpgradeUnattended": true 31 | }, 32 | "Security": { 33 | "AllowConcurrentLogins": false 34 | } 35 | } 36 | }, 37 | "ConnectionStrings": { 38 | "umbracoDbDSN": "Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", 39 | "umbracoDbDSN_ProviderName": "Microsoft.Data.Sqlite" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/index.ts: -------------------------------------------------------------------------------- 1 | import { UmbEntryPointOnInit, UmbEntryPointOnUnload } from '@umbraco-cms/backoffice/extension-api'; 2 | import { manifests as conditionManifests } from './Conditions/manifests'; 3 | import { manifests as dashboardManifests } from './Dashboards/manifests'; 4 | import { manifests as workspaceViewManifests } from './WorkspaceView/manifests'; 5 | import { manifests as modalManifests } from './Modals/manifests'; 6 | import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; 7 | import { client } from "./api/client.gen.js"; 8 | 9 | // load up the manifests here 10 | export const onInit: UmbEntryPointOnInit = (_host, extensionRegistry) => { 11 | 12 | // We can register many manifests at once via code 13 | // as opposed to a long umbraco-package.json file 14 | extensionRegistry.registerMany([ 15 | ...conditionManifests, 16 | ...dashboardManifests, 17 | ...workspaceViewManifests, 18 | ...modalManifests 19 | ]); 20 | 21 | _host.consumeContext(UMB_AUTH_CONTEXT, async (authContext) => { 22 | // Get the token info from Umbraco 23 | const config = authContext?.getOpenApiConfiguration(); 24 | 25 | client.setConfig({ 26 | auth: config?.token ?? undefined, 27 | baseUrl: config?.base ?? "", 28 | credentials: config?.credentials ?? "same-origin", 29 | }); 30 | }); 31 | }; 32 | 33 | export const onUnload: UmbEntryPointOnUnload = (_host, _extensionRegistry) => { 34 | console.log("Goodbye from Accessibility Reporter 👋"); 35 | }; 36 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/Views/Partials/blockgrid/items.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Cms.Core.Models.Blocks 2 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage> 3 | @{ 4 | if (Model?.Any() != true) { return; } 5 | } 6 | 7 |
8 | @foreach (var item in Model) 9 | { 10 | 11 |
19 | @{ 20 | var partialViewName = "blockgrid/Components/" + item.Content.ContentType.Alias; 21 | try 22 | { 23 | @await Html.PartialAsync(partialViewName, item) 24 | } 25 | catch (InvalidOperationException) 26 | { 27 |

28 | Could not render component of type: @(item.Content.ContentType.Alias) 29 |
30 | This likely happened because the partial view @partialViewName could not be found. 31 |

32 | } 33 | } 34 |
35 | } 36 |
37 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Core/AccessibilityReporter.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Umbraco.Community.AccessibilityReporter.Core 15 | True 16 | Accessibility Reporter Core 17 | Accessibility Reporter for Umbraco helps you to make your website accessible. 18 | MPL-2.0 19 | https://github.com/mattbegent/umbraco-accessibility-reporter 20 | https://github.com/mattbegent/umbraco-accessibility-reporter 21 | README.md 22 | Matt Begent 23 | logo.png 24 | Accessibility Reporter for Umbraco 16+. 25 | git 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/Infrastructure/AccessibilityReporterFactory.cs: -------------------------------------------------------------------------------- 1 | //using System.Collections.Generic; 2 | //using Umbraco.Cms.Core.Models.ContentEditing; 3 | //using Umbraco.Cms.Core.Models.Membership; 4 | //using Umbraco.Cms.Core.Models; 5 | //using System.Linq; 6 | //using AccessibilityReporter.Core.Interfaces; 7 | 8 | //namespace AccessibilityReporter.Infrastructure 9 | //{ 10 | // internal class AccessibilityReporterFactory : IContentAppFactory 11 | // { 12 | // private readonly IAccessibilityReporterSettings _settings; 13 | 14 | // public AccessibilityReporterFactory(IAccessibilityReporterSettings settings) 15 | // { 16 | // _settings = settings; 17 | // } 18 | 19 | // public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) 20 | // { 21 | // var content = source as IContent; 22 | 23 | // if (content == null) 24 | // { 25 | // return null; 26 | // } 27 | 28 | // if(_settings.IncludeIfNoTemplate == false && content.TemplateId.HasValue == false) 29 | // { 30 | // return null; 31 | // } 32 | 33 | // if (_settings.ExcludedDocTypes 34 | // .Contains(content.ContentType.Alias)) 35 | // { 36 | // return null; 37 | // } 38 | 39 | // var userGroupAliases = userGroups.Select(x => x.Alias); 40 | 41 | // if (_settings.UserGroups 42 | // .Intersect(userGroupAliases).Any() == false) 43 | // { 44 | // return null; 45 | // } 46 | 47 | // return new ContentApp 48 | // { 49 | // Alias = "AccessibilityReporter", 50 | // Name = "Accessibility", 51 | // Icon = "icon-globe-alt", 52 | // View = "/App_Plugins/AccessibilityReporter/accessibility-reporter-content-app.html", 53 | // Weight = 0 54 | // }; 55 | // } 56 | // } 57 | //} 58 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Components/ar-running-tests.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, customElement, property } from "@umbraco-cms/backoffice/external/lit"; 2 | import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; 3 | import './ar-logo'; 4 | import { generalStyles } from "../Styles/general"; 5 | 6 | @customElement("ar-running-tests") 7 | export class ARRunningTestsElement extends UmbElementMixin(LitElement) { 8 | 9 | @property({ type: String }) 10 | currentTestUrl: string | undefined; 11 | 12 | @property({ type: Number }) 13 | currentTestNumber: number | undefined; 14 | 15 | @property({ type: Number }) 16 | testPagesTotal: number | undefined; 17 | 18 | @property() 19 | onStopTests = () => { }; 20 | 21 | render() { 22 | return html` 23 | 24 | 25 |
26 | 27 |

Running accessibility tests ${this.currentTestUrl ? html`on` : null} ${this.currentTestUrl} ${this.currentTestNumber ? html`(${this.currentTestNumber}/${this.testPagesTotal})` : ''}

28 |
29 |

Please stay on this page while the tests are running.

30 | Cancel running tests 31 | 32 | 33 |
34 |
35 | `; 36 | } 37 | 38 | static styles = [ 39 | generalStyles 40 | ]; 41 | } 42 | 43 | declare global { 44 | interface HTMLElementTagNameMap { 45 | "ar-running-tests": ARRunningTestsElement; 46 | } 47 | } -------------------------------------------------------------------------------- /src/AccessibilityReporter.Services/AccessibilityReporter.Services.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Umbraco.Community.AccessibilityReporter.Services 19 | True 20 | Accessibility Reporter Services 21 | Accessibility Reporter for Umbraco helps you to make your website accessible. 22 | MPL-2.0 23 | https://github.com/mattbegent/umbraco-accessibility-reporter 24 | https://github.com/mattbegent/umbraco-accessibility-reporter 25 | README.md 26 | Matt Begent 27 | logo.png 28 | Accessibility Reporter for Umbraco 16+. 29 | git 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Api/types.gen.ts: -------------------------------------------------------------------------------- 1 | // This file is auto-generated by @hey-api/openapi-ts 2 | 3 | export type AccessibilityReporterAppSettings = { 4 | apiUrl: string; 5 | testBaseUrl: string; 6 | runTestsAutomatically: boolean; 7 | includeIfNoTemplate: boolean; 8 | maxPages: number; 9 | userGroups: Array; 10 | testsToRun: Array; 11 | excludedDocTypes: Array; 12 | }; 13 | 14 | export type NodeSummaryReadable = { 15 | readonly guid: string; 16 | readonly id: number; 17 | readonly name: string; 18 | readonly docTypeAlias: string; 19 | url: string; 20 | }; 21 | 22 | export type NodeSummaryWritable = { 23 | url: string; 24 | }; 25 | 26 | export type CurrentData = { 27 | body?: never; 28 | path?: never; 29 | query?: never; 30 | url: '/umbraco/accessibilityreporter/api/v1/config/current'; 31 | }; 32 | 33 | export type CurrentErrors = { 34 | /** 35 | * The resource is protected and requires an authentication token 36 | */ 37 | 401: unknown; 38 | }; 39 | 40 | export type CurrentResponses = { 41 | /** 42 | * OK 43 | */ 44 | 200: AccessibilityReporterAppSettings; 45 | }; 46 | 47 | export type CurrentResponse = CurrentResponses[keyof CurrentResponses]; 48 | 49 | export type PagesData = { 50 | body?: never; 51 | path?: never; 52 | query?: never; 53 | url: '/umbraco/accessibilityreporter/api/v1/pages'; 54 | }; 55 | 56 | export type PagesErrors = { 57 | /** 58 | * The resource is protected and requires an authentication token 59 | */ 60 | 401: unknown; 61 | }; 62 | 63 | export type PagesResponses = { 64 | /** 65 | * OK 66 | */ 67 | 200: Array; 68 | }; 69 | 70 | export type PagesResponse = PagesResponses[keyof PagesResponses]; 71 | 72 | export type ClientOptions = { 73 | baseUrl: 'https://localhost:44312' | (string & {}); 74 | }; -------------------------------------------------------------------------------- /src/AccessibilityReporter/appsettings-schema.AccessibilityReporter.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "AccessibilityReporter", 4 | "type": "object", 5 | "properties": { 6 | "AccessibilityReporter": { 7 | "$ref": "#/definitions/AccessibilityReporterDefinition" 8 | } 9 | }, 10 | "definitions": { 11 | "AccessibilityReporterDefinition": { 12 | "type": "object", 13 | "description": "Configuration of Umbraco.Community.AccessibilityReporter settings.", 14 | "properties": { 15 | "ApiUrl": { 16 | "type": "string", 17 | "description": "This is the URL of the API that will run the tests." 18 | }, 19 | "TestBaseUrl": { 20 | "type": "string", 21 | "description": "The base URL to run tests against." 22 | }, 23 | "TestsToRun": { 24 | "type": "array", 25 | "description": "The test categories to run.", 26 | "items": [ 27 | { 28 | "type": "string" 29 | } 30 | ] 31 | }, 32 | "UserGroups": { 33 | "type": "array", 34 | "description": "The user groups that can use Accessibility Reporter.", 35 | "items": [ 36 | { 37 | "type": "string" 38 | } 39 | ] 40 | }, 41 | "ExcludedDocTypes": { 42 | "type": "array", 43 | "description": "Document types to exclude Accessibility Reporter from being displayed.", 44 | "items": [ 45 | { 46 | "type": "string" 47 | } 48 | ] 49 | }, 50 | "RunTestsAutomatically": { 51 | "type": "boolean", 52 | "description": "Set if the tests should run as soon as users open a content node." 53 | }, 54 | "IncludeIfNoTemplate": { 55 | "type": "boolean", 56 | "description": "Set if you want to include content without templates." 57 | }, 58 | "MaxPages": { 59 | "type": "integer", 60 | "description": "The maximum number of pages to run the multipage tests on." 61 | } 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Api/sdk.gen.ts: -------------------------------------------------------------------------------- 1 | // This file is auto-generated by @hey-api/openapi-ts 2 | 3 | import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; 4 | import type { CurrentData, CurrentResponse, PagesData, PagesResponse } from './types.gen'; 5 | import { client as _heyApiClient } from './client.gen'; 6 | 7 | export type Options = ClientOptions & { 8 | /** 9 | * You can provide a client instance returned by `createClient()` instead of 10 | * individual options. This might be also useful if you want to implement a 11 | * custom client. 12 | */ 13 | client?: Client; 14 | /** 15 | * You can pass arbitrary values through the `meta` object. This can be 16 | * used to access values that aren't defined as part of the SDK function. 17 | */ 18 | meta?: Record; 19 | }; 20 | 21 | export class ConfigService { 22 | public static current(options?: Options) { 23 | return (options?.client ?? _heyApiClient).get({ 24 | security: [ 25 | { 26 | scheme: 'bearer', 27 | type: 'http' 28 | } 29 | ], 30 | url: '/umbraco/accessibilityreporter/api/v1/config/current', 31 | ...options 32 | }); 33 | } 34 | 35 | } 36 | 37 | export class DirectoryService { 38 | public static pages(options?: Options) { 39 | return (options?.client ?? _heyApiClient).get({ 40 | security: [ 41 | { 42 | scheme: 'bearer', 43 | type: 'http' 44 | } 45 | ], 46 | url: '/umbraco/accessibilityreporter/api/v1/pages', 47 | ...options 48 | }); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/scripts/generate-openapi.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import chalk from 'chalk'; 3 | import { createClient, defaultPlugins } from '@hey-api/openapi-ts'; 4 | 5 | // Start notifying user we are generating the TypeScript client 6 | console.log(chalk.green("Generating OpenAPI client...")); 7 | 8 | const swaggerUrl = process.argv[2]; 9 | if (swaggerUrl === undefined) { 10 | console.error(chalk.red(`ERROR: Missing URL to OpenAPI spec`)); 11 | console.error(`Please provide the URL to the OpenAPI spec as the first argument found in ${chalk.yellow('package.json')}`); 12 | console.error(`Example: node generate-openapi.js ${chalk.yellow('https://localhost:44331/umbraco/swagger/REPLACE_ME/swagger.json')}`); 13 | process.exit(); 14 | } 15 | 16 | // Needed to ignore self-signed certificates from running Umbraco on https on localhost 17 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 18 | 19 | // Start checking to see if we can connect to the OpenAPI spec 20 | console.log("Ensure your Umbraco instance is running"); 21 | console.log(`Fetching OpenAPI definition from ${chalk.yellow(swaggerUrl)}`); 22 | 23 | fetch(swaggerUrl).then(async (response) => { 24 | if (!response.ok) { 25 | console.error(chalk.red(`ERROR: OpenAPI spec returned with a non OK (200) response: ${response.status} ${response.statusText}`)); 26 | console.error(`The URL to your Umbraco instance may be wrong or the instance is not running`); 27 | console.error(`Please verify or change the URL in the ${chalk.yellow('package.json')} for the script ${chalk.yellow('generate-openapi')}`); 28 | return; 29 | } 30 | 31 | console.log(`OpenAPI spec fetched successfully`); 32 | console.log(`Calling ${chalk.yellow('hey-api')} to generate TypeScript client`); 33 | 34 | await createClient({ 35 | input: swaggerUrl, 36 | output: 'src/api', 37 | plugins: [ 38 | ...defaultPlugins, 39 | '@hey-api/client-fetch', 40 | { 41 | name: '@hey-api/typescript', 42 | enums: 'typescript' 43 | }, 44 | { 45 | name: '@hey-api/sdk', 46 | asClass: true 47 | } 48 | ], 49 | }); 50 | 51 | }) 52 | .catch(error => { 53 | console.error(`ERROR: Failed to connect to the OpenAPI spec: ${chalk.red(error.message)}`); 54 | console.error(`The URL to your Umbraco instance may be wrong or the instance is not running`); 55 | console.error(`Please verify or change the URL in the ${chalk.yellow('package.json')} for the script ${chalk.yellow('generate-openapi')}`); 56 | }); 57 | -------------------------------------------------------------------------------- /src/build.ps1: -------------------------------------------------------------------------------- 1 | # Define the project file and configuration 2 | $mainProjectFile = "./AccessibilityReporter/AccessibilityReporter.csproj" 3 | $coreProjectFile = "./AccessibilityReporter.Core/AccessibilityReporter.Core.csproj" 4 | $servicesProjectFile = "./AccessibilityReporter.Services/AccessibilityReporter.Services.csproj" 5 | 6 | $configuration = "Release" 7 | #$configuration = "Debug" 8 | $outputDirectory = "./build.out" 9 | $version = "4.0.3" 10 | 11 | ## Perhaps need to do a build of the client after updating the version in the source file 12 | $packageJsonPath = "./AccessibilityReporter/wwwroot/App_Plugins/AccessibilityReporter/umbraco-package.json" 13 | 14 | # Delete all files in the output directory 15 | if (Test-Path $outputDirectory) { 16 | Remove-Item "$outputDirectory/*" 17 | } 18 | 19 | # Update the version in umbraco.package.json 20 | if (Test-Path $packageJsonPath) { 21 | $packageJson = Get-Content $packageJsonPath | ConvertFrom-Json 22 | $packageJson.version = $version 23 | $packageJson | ConvertTo-Json -Depth 32 | Set-Content $packageJsonPath 24 | } else { 25 | Write-Output "The file $packageJsonPath does not exist." 26 | } 27 | 28 | # Build the client assets first to ensure static web assets are generated 29 | Write-Output "Building client assets..." 30 | Set-Location "./AccessibilityReporter/client" 31 | Write-Output "Installing npm dependencies..." 32 | npm install 33 | if ($LASTEXITCODE -ne 0) { 34 | Write-Output "npm install failed." 35 | exit $LASTEXITCODE 36 | } 37 | Write-Output "Building client..." 38 | npm run build 39 | if ($LASTEXITCODE -ne 0) { 40 | Write-Output "Client build failed." 41 | exit $LASTEXITCODE 42 | } 43 | Set-Location "../.." 44 | 45 | # Build the main project first to generate static web assets manifest 46 | Write-Output "Building projects..." 47 | dotnet build $mainProjectFile --configuration $configuration 48 | if ($LASTEXITCODE -ne 0) { 49 | Write-Output "Build failed." 50 | exit $LASTEXITCODE 51 | } 52 | 53 | # Pack the project into a NuGet package 54 | dotnet pack $servicesProjectFile --configuration $configuration --output $outputDirectory /p:Version=$version 55 | dotnet pack $coreProjectFile --configuration $configuration --output $outputDirectory /p:Version=$version 56 | dotnet pack $mainProjectFile --configuration $configuration --output $outputDirectory /p:Version=$version 57 | 58 | # Check if the pack was successful 59 | if ($LASTEXITCODE -eq 0) { 60 | Write-Output "Pack successful." 61 | } else { 62 | Write-Output "Pack failed." 63 | exit $LASTEXITCODE 64 | } 65 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Conditions/accessibilityreporter.condition.templateset.ts: -------------------------------------------------------------------------------- 1 | import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; 2 | import { UmbConditionConfigBase, UmbConditionControllerArguments, UmbExtensionCondition } from "@umbraco-cms/backoffice/extension-api"; 3 | import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document'; 4 | import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; 5 | import { tryExecute } from '@umbraco-cms/backoffice/resources'; 6 | import { ConfigService } from '../api'; 7 | 8 | export type TemplateSetConditionConfig = UmbConditionConfigBase<'AccessibilityReporter.Condition.TemplateSet'>; 9 | 10 | export class TemplateSetCondition extends UmbConditionBase implements UmbExtensionCondition 11 | { 12 | constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { 13 | super(host, args); 14 | 15 | this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT , (workspaceCtx) => { 16 | if (!workspaceCtx) { 17 | this.permitted = false; 18 | return; 19 | } 20 | this.observe(workspaceCtx.templateId, async (templateId) => { 21 | 22 | // No template set === null 23 | // Tempate set we get a GUID back 24 | //console.log('templateId', templateId); 25 | 26 | // Look up the value we have from our own C# API 27 | // If we have a match on any then permitted is true 28 | const { data, error } = await tryExecute(this, ConfigService.current()) 29 | if (error) { 30 | console.error('Error fetching config via API', error); 31 | } 32 | 33 | // Value from API controller that is the config value 34 | const includeIfNoTemplateSet = data?.includeIfNoTemplate; 35 | //console.log('includeIfNoTemplateSet', includeIfNoTemplateSet); 36 | 37 | // Passes if config says the doc SHOULD have a template set (aka NOT null) 38 | if(includeIfNoTemplateSet === false && templateId !== null) { 39 | this.permitted = true; 40 | return; 41 | } 42 | 43 | // Config says it passes if template is NOT set (aka IS null) 44 | if(includeIfNoTemplateSet && templateId === null) { 45 | this.permitted = true; 46 | return; 47 | } 48 | 49 | this.permitted = false; 50 | }) 51 | 52 | }); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /umbraco-marketplace.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://marketplace.umbraco.com/umbraco-marketplace-schema.json", 3 | "AlternatePackageNames": [ 4 | "Accessibility Reporter" 5 | ], 6 | "AuthorDetails": { 7 | "Description": "A front-end developer who wants to improve the accessibility of the web.", 8 | "Url": "https://mattbegent.co.uk/", 9 | "ImageUrl": "https://www.gravatar.com/avatar/830b3844a7476427cd3f8225c83833c3?d=404&s=150", 10 | "Contributors": [] 11 | }, 12 | "Category": "Editor Tools", 13 | "DiscussionForumUrl": "https://github.com/mattbegent/umbraco-accessibility-reporter/discussions", 14 | "DocumentationUrl": "https://github.com/mattbegent/umbraco-accessibility-reporter/", 15 | "IssueTrackerUrl": "https://github.com/mattbegent/umbraco-accessibility-reporter/issues", 16 | "LicenseTypes": [ 17 | "Free" 18 | ], 19 | "PackageType": "Package", 20 | "PackagesByAuthor": [], 21 | "RelatedPackages": [], 22 | "Screenshots": [ 23 | { 24 | "ImageUrl": "https://raw.githubusercontent.com/mattbegent/umbraco-accessibility-reporter/main/screenshots/dashboard.png", 25 | "Caption": "Dashboard showing the results of a multipage accessibility test" 26 | }, 27 | { 28 | "ImageUrl": "https://raw.githubusercontent.com/mattbegent/umbraco-accessibility-reporter/main/screenshots/results.png", 29 | "Caption": "Dashboard of accessibility test results for a single page" 30 | }, 31 | { 32 | "ImageUrl": "https://raw.githubusercontent.com/mattbegent/umbraco-accessibility-reporter/main/screenshots/detail.png", 33 | "Caption": "Detailed view showing accessibility issues and how to fix them" 34 | } 35 | ], 36 | "Tags": [ 37 | "accessibility", 38 | "backoffice", 39 | "accessibility reporter", 40 | "WCAG", 41 | "WCAG 2.1", 42 | "WCAG 2.2", 43 | "a11y", 44 | "content app", 45 | "dashboard" 46 | ], 47 | "Title": "Accessibility Reporter", 48 | "Description": "Accessibility Reporter helps you make sure that your website is accessible to all.", 49 | "VersionSpecificPackageIds": [ 50 | { 51 | "UmbracoMajorVersion": 10, 52 | "PackageId": "Umbraco.Community.AccessibilityReporter" 53 | }, 54 | { 55 | "UmbracoMajorVersion": 11, 56 | "PackageId": "Umbraco.Community.AccessibilityReporter" 57 | }, 58 | { 59 | "UmbracoMajorVersion": 12, 60 | "PackageId": "Umbraco.Community.AccessibilityReporter" 61 | }, 62 | { 63 | "UmbracoMajorVersion": 13, 64 | "PackageId": "Umbraco.Community.AccessibilityReporter" 65 | }, 66 | { 67 | "UmbracoMajorVersion": 16, 68 | "PackageId": "Umbraco.Community.AccessibilityReporter" 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /src/AccessibilityReporter/Infrastructure/AccessibilityReporterComposer.cs: -------------------------------------------------------------------------------- 1 | using AccessibilityReporter.Infrastructure.Config; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.OpenApi.Models; 5 | using Swashbuckle.AspNetCore.SwaggerGen; 6 | using Umbraco.Cms.Api.Management.OpenApi; 7 | using Umbraco.Cms.Core.Composing; 8 | using Umbraco.Cms.Core.DependencyInjection; 9 | 10 | namespace AccessibilityReporter.Infrastructure 11 | { 12 | 13 | internal class AccessibilityReporterComposer : IComposer 14 | { 15 | public void Compose(IUmbracoBuilder builder) 16 | { 17 | var config = builder.Config.GetSection(AccessibilityReporterAppSettings.SectionName) 18 | .Get(); 19 | 20 | builder.Services.AddSingleton(AccessibilityReporterSettingsFactory.Make(config ?? new AccessibilityReporterAppSettings())); 21 | 22 | builder.Services.Configure(opt => 23 | { 24 | // Configure the Swagger generation options 25 | // Add in a new Swagger API document solely for our own package that can be browsed via Swagger UI 26 | // Along with having a generated swagger JSON file that we can use to auto generate a TypeScript client 27 | opt.SwaggerDoc("AccessibilityReporter", new OpenApiInfo 28 | { 29 | Title = "Accessibility Reporter Package API", 30 | Version = "1.0" 31 | }); 32 | 33 | // https://docs.umbraco.com/umbraco-cms/v/14.latest-beta/reference/custom-swagger-api 34 | // PR: https://github.com/umbraco/Umbraco-CMS/pull/15699 35 | opt.OperationFilter(); 36 | 37 | // Rather than very verbose names from generated TS client, we simplify them a bit 38 | // https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/README.md#operation-filters 39 | // https://docs.umbraco.com/umbraco-cms/reference/api-versioning-and-openapi#adding-custom-operation-ids 40 | // https://g-mariano.medium.com/generate-readable-apis-clients-by-setting-unique-and-meaningful-operationid-in-swagger-63d404f32ff8 41 | opt.CustomOperationIds(apiDesc => $"{apiDesc.ActionDescriptor.RouteValues["action"]}"); 42 | }); 43 | 44 | 45 | } 46 | } 47 | 48 | // https://docs.umbraco.com/umbraco-cms/v/14.latest-beta/reference/custom-swagger-api 49 | // PR: https://github.com/umbraco/Umbraco-CMS/pull/15699 50 | public class MyBackOfficeSecurityRequirementsOperationFilter : BackOfficeSecurityRequirementsOperationFilterBase 51 | { 52 | protected override string ApiName => "AccessibilityReporter"; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Services/DefaultTestableNodesService.cs: -------------------------------------------------------------------------------- 1 | using AccessibilityReporter.Core.Interfaces; 2 | using AccessibilityReporter.Services.Interfaces; 3 | using Umbraco.Cms.Core.Models.PublishedContent; 4 | using Umbraco.Cms.Core.Services.Navigation; 5 | using Umbraco.Cms.Core.Web; 6 | 7 | namespace AccessibilityReporter.Services 8 | { 9 | public class DefaultTestableNodesService : ITestableNodesService 10 | { 11 | private readonly IUmbracoContextFactory _contextFactory; 12 | private readonly IDocumentNavigationQueryService _documentNavigationQueryService; 13 | private readonly IAccessibilityReporterSettings _settings; 14 | 15 | public DefaultTestableNodesService(IUmbracoContextFactory contextFactory, 16 | IDocumentNavigationQueryService documentNavigationQueryService, 17 | IAccessibilityReporterSettings settings) 18 | { 19 | _contextFactory = contextFactory; 20 | _documentNavigationQueryService = documentNavigationQueryService; 21 | _settings = settings; 22 | } 23 | 24 | public IEnumerable All() 25 | { 26 | using (var contextReference = _contextFactory.EnsureUmbracoContext()) 27 | { 28 | var rootItems = _documentNavigationQueryService.TryGetRootKeys(out var rootKeys) ? rootKeys : Enumerable.Empty(); 29 | 30 | var everything = new List(); 31 | 32 | foreach (var rootKey in rootItems) 33 | { 34 | var rootContent = contextReference.UmbracoContext.Content?.GetById(rootKey); 35 | if (rootContent != null) 36 | { 37 | everything.Add(rootContent); 38 | everything.AddRange(GetDescendants(rootContent)); 39 | } 40 | } 41 | 42 | return everything.Where(DocumentTypeIsApplicable) 43 | .Where(TemplateStateIsApplicable) 44 | .Take(_settings.MaxPages); 45 | 46 | bool DocumentTypeIsApplicable(IPublishedContent content) 47 | => _settings.ExcludedDocTypes.Contains(content.ContentType.Alias) == false; 48 | 49 | bool TemplateStateIsApplicable(IPublishedContent content) 50 | => _settings.IncludeIfNoTemplate || content.TemplateId.HasValue; 51 | } 52 | } 53 | 54 | private IEnumerable GetDescendants(IPublishedContent content) 55 | { 56 | if (_documentNavigationQueryService.TryGetChildrenKeys(content.Key, out var childKeys)) 57 | { 58 | foreach (var childKey in childKeys) 59 | { 60 | // Get the child content from the Umbraco context 61 | using var contextReference = _contextFactory.EnsureUmbracoContext(); 62 | var childContent = contextReference.UmbracoContext.Content?.GetById(childKey); 63 | if (childContent != null) 64 | { 65 | yield return childContent; 66 | // Recursively get descendants 67 | foreach (var descendant in GetDescendants(childContent)) 68 | { 69 | yield return descendant; 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33122.133 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccessibilityReporter", "AccessibilityReporter\AccessibilityReporter.csproj", "{9FC7250D-D3A5-45A6-B9DC-54E633516033}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccessibilityReporter.Services", "AccessibilityReporter.Services\AccessibilityReporter.Services.csproj", "{3C167CC1-08EE-48E5-BD68-3E1BD8AD0810}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccessibilityReporter.Core", "AccessibilityReporter.Core\AccessibilityReporter.Core.csproj", "{1CB005D1-B19E-4098-8015-C7DDB778FEFA}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccessibilityReporter.Website", "AccessibilityReporter.Website\AccessibilityReporter.Website.csproj", "{2B40EFD1-780C-4E28-BDC6-BB0712F7DFE9}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5F332CEA-3434-40F6-999F-9AC570BA9451}" 15 | ProjectSection(SolutionItems) = preProject 16 | build.ps1 = build.ps1 17 | EndProjectSection 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {9FC7250D-D3A5-45A6-B9DC-54E633516033}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {9FC7250D-D3A5-45A6-B9DC-54E633516033}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {9FC7250D-D3A5-45A6-B9DC-54E633516033}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {9FC7250D-D3A5-45A6-B9DC-54E633516033}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {3C167CC1-08EE-48E5-BD68-3E1BD8AD0810}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {3C167CC1-08EE-48E5-BD68-3E1BD8AD0810}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {3C167CC1-08EE-48E5-BD68-3E1BD8AD0810}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {3C167CC1-08EE-48E5-BD68-3E1BD8AD0810}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {1CB005D1-B19E-4098-8015-C7DDB778FEFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {1CB005D1-B19E-4098-8015-C7DDB778FEFA}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {1CB005D1-B19E-4098-8015-C7DDB778FEFA}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {1CB005D1-B19E-4098-8015-C7DDB778FEFA}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {2B40EFD1-780C-4E28-BDC6-BB0712F7DFE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {2B40EFD1-780C-4E28-BDC6-BB0712F7DFE9}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {2B40EFD1-780C-4E28-BDC6-BB0712F7DFE9}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {2B40EFD1-780C-4E28-BDC6-BB0712F7DFE9}.Release|Any CPU.Build.0 = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(SolutionProperties) = preSolution 43 | HideSolutionNode = FALSE 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {13BE4828-050A-4A7C-B521-14A605A3DE80} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/AccessibilityReporter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net9.0 6 | enable 7 | 8 | 9 | 10 | 11 | AccessibilityReporter 12 | Umbraco.Community.AccessibilityReporter 13 | Accessibility Reporter 14 | Accessibility Reporter for Umbraco helps you to make your website accessible. 15 | Accessibility; WCAG; Umbraco; Content App; dashboard; umbraco-marketplace; 16 | MPL-2.0 17 | https://github.com/mattbegent/umbraco-accessibility-reporter 18 | https://github.com/mattbegent/umbraco-accessibility-reporter 19 | README.md 20 | Matt Begent 21 | logo.png 22 | Accessibility Reporter for Umbraco 16+. Fix issue getting correct URLs in the workspace view. 23 | git 24 | 25 | 26 | 27 | 28 | / 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | True 54 | True 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Modals/detail/accessibilityreporter.detail.element.ts: -------------------------------------------------------------------------------- 1 | import { customElement, html, ifDefined } from "@umbraco-cms/backoffice/external/lit"; 2 | import { UmbModalBaseElement, UmbModalRejectReason } from "@umbraco-cms/backoffice/modal"; 3 | import { css } from "lit"; 4 | import { DetailModalData, DetailModalValue } from "./accessibilityreporter.detail.modal.token.ts"; 5 | import AccessibilityReporterService from "../../Services/accessibility-reporter.service.ts"; 6 | 7 | @customElement('accessibility-report-detail-modal') 8 | export class DetailModalElement extends UmbModalBaseElement 9 | { 10 | constructor() { 11 | super(); 12 | } 13 | 14 | connectedCallback() { 15 | super.connectedCallback(); 16 | } 17 | 18 | private handleClose() { 19 | this.modalContext?.reject({ type: "close" } as UmbModalRejectReason); 20 | } 21 | 22 | private addFullStop(sentence: string) { 23 | return sentence.replace(/([^.])$/, '$1.'); 24 | }; 25 | 26 | private formatFailureSummary(summary: string) { 27 | return this.addFullStop(summary.replace('Fix any of the following:', '').replace('Fix all of the following:', '')); 28 | }; 29 | 30 | render() { 31 | 32 | return html` 33 | 34 | 35 | ${this.data?.result.nodes.map((issue, index) => html` 36 | 37 | 38 |
39 |

Violation ${index + 1} ${AccessibilityReporterService.upperCaseFirstLetter(this.data?.result.impact || "")}

40 |
41 | 42 |

Description

43 |

${this.addFullStop(this.data?.result.description || "")}

44 | 45 |

Location

46 |
${issue.target[0]}
47 | 48 |

HTML Source

49 |
${issue.html}
50 | 51 |

Failure Summary

52 |

${this.formatFailureSummary(issue.failureSummary)}

53 | 54 | ${issue.any.length && issue.any[0].relatedNodes.length ? html` 55 |
56 |

Related Nodes

57 |
${issue.any[0].relatedNodes[0].html}
58 |
59 | ` : null} 60 | 61 | 62 |
63 | `)} 64 | 65 |
66 | Close 67 |
68 |
69 | `; 70 | } 71 | 72 | static styles = css` 73 | uui-box { 74 | margin-bottom: 1rem; 75 | } 76 | .code { 77 | padding: 1rem; 78 | background-color: #f4f4f4; 79 | } 80 | `; 81 | } 82 | 83 | export default DetailModalElement; 84 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Components/ar-score.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, customElement, css, property } from "@umbraco-cms/backoffice/external/lit"; 2 | import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; 3 | 4 | @customElement("ar-score") 5 | export class ARScoreElement extends UmbElementMixin(LitElement) { 6 | 7 | @property({type: Number}) 8 | score: number; 9 | 10 | @property({ type: Boolean }) 11 | large: boolean; 12 | 13 | @property({ type: Boolean }) 14 | hideScoreText: boolean; 15 | 16 | private getCSSClass(score: number) { 17 | if (score > 89) { 18 | return 'c-score--pass'; 19 | } 20 | if (score > 49) { 21 | return 'c-score--average'; 22 | } 23 | return 'c-score--fail'; 24 | } 25 | 26 | render() { 27 | return html` 28 |
29 | 30 | 35 | 41 | 42 |
43 | ${this.score} 44 | ${!this.hideScoreText ? html`Score` : ``} 45 |
46 |
47 | `; 48 | } 49 | 50 | static styles = [ 51 | css` 52 | .c-score { 53 | position: relative; 54 | width: 130px; 55 | height: 130px; 56 | margin: 0 auto; 57 | } 58 | 59 | .c-score__inner { 60 | display: block; 61 | width: 100%; 62 | height: 100%; 63 | } 64 | 65 | .c-score__background { 66 | fill: none; 67 | stroke: #eee; 68 | stroke-width: 1.75; 69 | } 70 | 71 | .c-score__fill { 72 | fill: none; 73 | stroke: none; 74 | stroke-width: 1.75; 75 | stroke-linecap: round; 76 | animation: progress 1000ms ease-out forwards; 77 | } 78 | 79 | @keyframes progress { 80 | 0% { 81 | stroke-dasharray: 0 100; 82 | } 83 | } 84 | 85 | .c-score__text { 86 | display: flex; 87 | flex-direction: column; 88 | justify-content: center; 89 | align-items: center; 90 | position: absolute; 91 | top: 0; 92 | bottom: 0; 93 | left: 0; 94 | right: 0; 95 | margin: auto; 96 | z-index: 1; 97 | font-weight: 700; 98 | } 99 | 100 | .c-score__text-number { 101 | font-size: 34px; 102 | } 103 | 104 | .c-score__text-title { 105 | font-size: 16px; 106 | margin-top: 10px; 107 | } 108 | 109 | .c-score--pass .c-score__fill { 110 | stroke: #1C824A; 111 | } 112 | 113 | .c-score--average .c-score__fill { 114 | stroke: #f79c37; 115 | } 116 | 117 | .c-score--fail .c-score__fill { 118 | stroke: #d42054; 119 | } 120 | 121 | .c-score--large { 122 | width: 150px; 123 | height: 150px; 124 | } 125 | .c-score--large .c-score__text-number { 126 | font-size: 4rem; 127 | } 128 | ` 129 | ]; 130 | } 131 | 132 | declare global { 133 | interface HTMLElementTagNameMap { 134 | "ar-score": ARScoreElement; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Components/ar-chart.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, customElement, property } from "@umbraco-cms/backoffice/external/lit"; 2 | import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; 3 | import Chart from 'chart.js/auto'; 4 | import ChartDataLabels from 'chartjs-plugin-datalabels'; 5 | import pattern from 'patternomaly'; 6 | 7 | @customElement("ar-chart") 8 | export class ARChartElement extends UmbElementMixin(LitElement) { 9 | 10 | @property({attribute: false}) 11 | data: object; 12 | 13 | @property() 14 | type: string; 15 | 16 | @property() 17 | height: string; 18 | 19 | @property() 20 | width: string; 21 | 22 | connectedCallback() { 23 | super.connectedCallback(); 24 | setTimeout(()=> { 25 | this.initChart(); 26 | }, 100); 27 | } 28 | 29 | private initChart() { 30 | 31 | if(!this.shadowRoot) { 32 | return; 33 | } 34 | 35 | const ctx = this.shadowRoot.querySelector('canvas') as HTMLCanvasElement; 36 | const labelFontStyles = { 37 | size: '15px', 38 | lineHeight: 1, 39 | family: 'Lato' 40 | }; 41 | 42 | let chartSettings: any = { 43 | type: this.type, 44 | data: this.data, 45 | plugins: [ChartDataLabels], 46 | responsive: true, 47 | maintainAspectRatio: false 48 | }; 49 | 50 | if (this.type === 'pie') { 51 | 52 | for (let index = 0; index < chartSettings.data.datasets.length; index++) { 53 | chartSettings.data.datasets[index].backgroundColor = chartSettings.data.datasets[index].backgroundColor.map((color: string, colorIndex: number) => { 54 | const currentPattern = chartSettings.data.patterns[colorIndex]; 55 | if (!currentPattern) { 56 | return color; 57 | } 58 | return pattern.draw(currentPattern, color); 59 | }); 60 | } 61 | chartSettings.options = {}; 62 | chartSettings.options.plugins = { 63 | tooltip: { 64 | enabled: true 65 | }, 66 | datalabels: { 67 | clip : true, 68 | backgroundColor: '#FFF', 69 | color: '#000', 70 | borderColor: "#000", 71 | borderWidth: 2, 72 | font: labelFontStyles, 73 | align: 'top', 74 | display: 'auto', 75 | formatter: ((context: any, args: any)=> { 76 | if(context) { 77 | const index = args.dataIndex; 78 | return context + " " + args.chart.data.labels[index]; 79 | } else { 80 | return null; 81 | } 82 | }) 83 | } 84 | } 85 | } 86 | 87 | if(this.type === 'bar') { 88 | chartSettings.options = { 89 | scales: { 90 | y: { 91 | ticks: { 92 | precision: 0 93 | } 94 | } 95 | }, 96 | plugins: { 97 | tooltip: { 98 | enabled: true 99 | }, 100 | legend: { 101 | display: false 102 | }, 103 | datalabels: { 104 | backgroundColor: '#FFF', 105 | color: '#000', 106 | font: labelFontStyles, 107 | borderColor: "#000", 108 | borderWidth: 2, 109 | padding: { 110 | left: 6, 111 | right: 6, 112 | top: 2, 113 | bottom: 2 114 | } 115 | } 116 | } 117 | }; 118 | } 119 | 120 | new Chart(ctx, chartSettings); 121 | 122 | } 123 | 124 | render() { 125 | return html` 126 |
127 | 128 |
129 | `; 130 | } 131 | 132 | } 133 | 134 | declare global { 135 | interface HTMLElementTagNameMap { 136 | "ar-chart": ARChartElement; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Conditions/accessibilityreporter.condition.usergrouphasaccess.ts: -------------------------------------------------------------------------------- 1 | import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; 2 | import { UmbConditionConfigBase, UmbConditionControllerArguments, UmbExtensionCondition } from "@umbraco-cms/backoffice/extension-api"; 3 | import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; 4 | import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; 5 | import { UmbUserDetailRepository } from '@umbraco-cms/backoffice/user' 6 | import { UmbUserGroupItemModel, UmbUserGroupItemRepository } from '@umbraco-cms/backoffice/user-group'; 7 | import { ConfigService } from '../api'; 8 | import { tryExecute } from '@umbraco-cms/backoffice/resources'; 9 | 10 | /* Condition Config (The alias matches in the consuming manifest - does not require extra config */ 11 | export type UserGroupHasAccessConditionConfig = UmbConditionConfigBase<'AccessibilityReporter.Condition.UserGroupHasAccess'>; 12 | 13 | export class UserGroupHasAccessCondition extends UmbConditionBase implements UmbExtensionCondition { 14 | config: UserGroupHasAccessConditionConfig; 15 | _userGroups: UmbUserGroupItemModel[] | undefined; 16 | 17 | constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { 18 | super(host, args); 19 | 20 | this.consumeContext(UMB_CURRENT_USER_CONTEXT, (currentUserCtx) => { 21 | if (!currentUserCtx) { 22 | this.permitted = false; 23 | return; 24 | } 25 | 26 | this.observe(currentUserCtx.currentUser, async (currentUser) => { 27 | 28 | if (currentUser === undefined) { 29 | console.warn('Unable to get the current user'); 30 | this.permitted = false; 31 | return; 32 | } 33 | 34 | try { 35 | // Get user details 36 | const userDetailRepository = new UmbUserDetailRepository(this); 37 | const { data: userDetail } = await userDetailRepository.requestByUnique(currentUser.unique); 38 | const userGroupIds = userDetail?.userGroupUniques; 39 | 40 | if (!userGroupIds || userGroupIds.length === 0) { 41 | console.warn('Current User has no user group IDs assigned'); 42 | this.permitted = false; 43 | return; 44 | } 45 | 46 | // Fetch user group details using the GUIDs 47 | const userGroupItemRepository = new UmbUserGroupItemRepository(this); 48 | const userGroupUniqueIds = userGroupIds.map((ref: { unique: string }) => ref.unique); 49 | const { data: userGroups } = await userGroupItemRepository.requestItems(userGroupUniqueIds); 50 | 51 | this._userGroups = userGroups; 52 | 53 | if (!this._userGroups || this._userGroups.length === 0) { 54 | console.warn('No user group details found'); 55 | this.permitted = false; 56 | return; 57 | } 58 | 59 | // Get configuration from API 60 | const { data: config, error } = await tryExecute(this, ConfigService.current()); 61 | if (error) { 62 | console.error('Error fetching config via API', error); 63 | this.permitted = false; 64 | return; 65 | } 66 | 67 | const allowedUserGroups = config?.userGroups; 68 | 69 | if (!allowedUserGroups || allowedUserGroups.length === 0) { 70 | // If no user groups are configured, allow access 71 | this.permitted = true; 72 | return; 73 | } 74 | 75 | // Check if any of the user's groups match the allowed groups 76 | // You can match by name, alias, or unique ID depending on your config 77 | const hasAccess = this._userGroups.some(userGroup => { 78 | return allowedUserGroups.includes(userGroup.name.toLowerCase()) || 79 | allowedUserGroups.includes(userGroup.unique.toLowerCase()); 80 | }); 81 | 82 | this.permitted = hasAccess; 83 | 84 | } catch (error) { 85 | console.error('Error checking user group access:', error); 86 | this.permitted = false; 87 | } 88 | }); 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Styles/general.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@umbraco-cms/backoffice/external/lit"; 2 | 3 | export const generalStyles = css` 4 | 5 | .c-title__group { 6 | display: grid; 7 | grid-template-columns: 40px 1fr; 8 | gap: 10px; 9 | align-items: center; 10 | } 11 | 12 | .c-title { 13 | font-size: 17.25px; 14 | margin: 0; 15 | font-weight: 700; 16 | line-height: 1.3; 17 | } 18 | 19 | .c-title__link { 20 | text-decoration: underline; 21 | text-underline-position: below; 22 | } 23 | 24 | .c-title__link:hover, 25 | .c-title__link:focus { 26 | text-decoration: none; 27 | } 28 | 29 | .c-accordion-header { 30 | display: flex; 31 | width: 100%; 32 | align-items: center; 33 | border: none; 34 | padding: 0; 35 | background-color: transparent; 36 | font-family: inherit; 37 | font-size: 17.25px; 38 | font-weight: inherit; 39 | font-size: inherit; 40 | } 41 | 42 | .c-detail-button { 43 | pointer-events: none; 44 | border: none; 45 | padding: 0; 46 | background-color: transparent; 47 | font-family: inherit; 48 | font-size: inherit; 49 | font-weight: inherit; 50 | font-size: inherit; 51 | white-space: nowrap; 52 | text-decoration: none; 53 | color: inherit; 54 | line-height: 1; 55 | cursor: pointer; 56 | } 57 | 58 | .c-detail-button__group { 59 | display: flex; 60 | align-items: center; 61 | } 62 | 63 | .c-detail-button__text { 64 | margin-left: 5px; 65 | } 66 | 67 | .c-table__container { 68 | overflow-x: auto; 69 | } 70 | 71 | .c-table__container uui-table-head-cell { 72 | font-size: 14px; 73 | } 74 | 75 | .c-summary { 76 | text-align: center; 77 | } 78 | 79 | .c-summary__container { 80 | display: grid; 81 | gap: 20px; 82 | grid-template-columns: 1fr 1fr 1fr 1fr; 83 | max-width: 600px; 84 | overflow-x: auto; 85 | margin-bottom: 20px; 86 | } 87 | 88 | .c-summary__circle { 89 | display: flex; 90 | flex-direction: column; 91 | align-items: center; 92 | justify-content: center; 93 | border: 5px solid #d42054; 94 | height: 117px; 95 | width: 117px; 96 | border-radius: 100%; 97 | margin: 0 auto 10px auto; 98 | font-weight: 700; 99 | font-size: 34px; 100 | } 101 | 102 | .c-summary--passed .c-summary__circle { 103 | border-color: #1C824A; 104 | } 105 | 106 | .c-summary--incomplete .c-summary__circle { 107 | border-color: #f79c37; 108 | } 109 | 110 | .c-summary--info .c-summary__circle { 111 | border-color: #1b264f; 112 | } 113 | 114 | .c-summary__title { 115 | text-align: center; 116 | font-weight: 700; 117 | font-size: 16px; 118 | margin-top: 10px; 119 | } 120 | 121 | @media (max-width: 768px) { 122 | .c-summary__time { 123 | display: block; 124 | margin-top: 10px; 125 | } 126 | } 127 | 128 | .c-summary__button { 129 | margin-right: 10px; 130 | } 131 | 132 | .c-detail__title { 133 | font-size: 15px; 134 | font-weight: 700; 135 | } 136 | 137 | .c-checklist__item { 138 | margin: 0 0 1.5rem 0; 139 | } 140 | 141 | .c-tag { 142 | margin-bottom: 0.5rem; 143 | } 144 | 145 | .c-title__group { 146 | display: grid; 147 | grid-template-columns: 40px 1fr; 148 | gap: 10px; 149 | align-items: center; 150 | } 151 | 152 | .c-title { 153 | font-size: 17.25px; 154 | margin: 0; 155 | font-weight: 700; 156 | line-height: 1.3; 157 | } 158 | 159 | .c-title__link { 160 | text-decoration: underline; 161 | text-underline-position: below; 162 | } 163 | 164 | .c-title__link:hover, 165 | .c-title__link:focus { 166 | text-decoration: none; 167 | } 168 | 169 | .c-paragraph { 170 | margin: 0 0 1rem 0; 171 | } 172 | 173 | .c-paragraph__spaced { 174 | margin: 0 0 2rem 0; 175 | } 176 | 177 | .c-bold { 178 | font-weight: 700;; 179 | } 180 | 181 | .c-circle { 182 | display: flex; 183 | flex-direction: column; 184 | align-items: center; 185 | justify-content: center; 186 | border: 2px solid currentColor; 187 | height: 32px; 188 | width: 32px; 189 | border-radius: 100%; 190 | font-weight: 700; 191 | font-size: 16px; 192 | } 193 | 194 | .c-circle--failed { 195 | border-color: #d42054; 196 | } 197 | 198 | .c-circle--incomplete { 199 | border-color: #f79c37; 200 | } 201 | 202 | .c-circle--passed { 203 | border-color: #1C824A; 204 | } 205 | 206 | /* The default Umbraco font Lato renders differently between operating systems, so we are using the system default to vertically align */ 207 | .c-circle__text { 208 | margin-top: 3px; 209 | } 210 | 211 | .mac .c-circle__text { 212 | margin-top: 0; 213 | } 214 | 215 | .c-incident-number { 216 | display: flex; 217 | align-items: center; 218 | justify-content: center; 219 | border: 1px solid currentColor; 220 | height: 24px; 221 | width: 24px; 222 | border-radius: 100%; 223 | font-weight: 700; 224 | font-size: 14px; 225 | } 226 | 227 | .c-incident-number--serious, 228 | .c-incident-number--critical { 229 | border-color: #d42054; 230 | } 231 | 232 | .c-incident-number--moderate { 233 | border-color: #fad634; 234 | } 235 | 236 | .c-incident-number__text { 237 | margin-top: 2px; 238 | } 239 | 240 | .mac .c-incident-number__text { 241 | margin-top: 0; 242 | } 243 | 244 | /* Fix Umbraco Colors */ 245 | 246 | .umb-sub-views-nav-item .badge.-type-alert { 247 | background-color: #d42054; 248 | } 249 | 250 | .c-uui-tag--positive { 251 | background-color: #1C824A; 252 | } 253 | 254 | /* Dashboard */ 255 | 256 | .c-dashboard-grid { 257 | display: grid; 258 | grid-template-columns: 1fr 1fr 1fr; 259 | gap: 24px; 260 | margin-bottom: 1rem; 261 | } 262 | 263 | .c-dashboard-grid__full-row { 264 | grid-column: 1 / 4; 265 | } 266 | 267 | .c-dashboard-grid__23 { 268 | grid-column: 2 / 4; 269 | } 270 | 271 | .c-dashboard-number { 272 | font-size: 5rem; 273 | line-height: 1; 274 | text-align: center; 275 | font-weight: bold; 276 | margin: 2rem 0 0 0; 277 | } 278 | 279 | .c-dashboard-number__info { 280 | font-size: 1rem; 281 | text-align: center; 282 | font-weight: bold; 283 | margin: 1rem 0 0 0; 284 | } 285 | 286 | .c-detail-button--active { 287 | pointer-events: all; 288 | } 289 | 290 | .c-test-container { 291 | width: 100%; 292 | min-height: 800px; 293 | margin-top: 1rem; 294 | } 295 | 296 | .u-mb20 { 297 | margin-bottom: 20px; 298 | } 299 | 300 | .sr-only { 301 | position: absolute; 302 | width: 1px; 303 | height: 1px; 304 | padding: 0; 305 | margin: -1px; 306 | overflow: hidden; 307 | clip: rect(0, 0, 0, 0); 308 | white-space: nowrap; 309 | border-width: 0; 310 | } 311 | 312 | `; 313 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Accessibility Reporter For Umbraco 2 | 3 | ![Accessibility Reporter logo](https://raw.githubusercontent.com/mattbegent/umbraco-accessibility-reporter/main/logos/logo64.png) 4 | 5 | ## What is it? 6 | 7 | Accessibility Reporter for Umbraco is a content app and dashboard that helps you test the accessibility of your website against common accessibility standards, including the Web Content Accessibility Guidelines (WCAG), Section 508 and best practices, directly in Umbraco. 8 | 9 | ## Why should I use it? 10 | 11 | You want to help make your Umbraco website more accessible by testing it against WCAG success criteria. 12 | 13 | ## How does it work? 14 | 15 | It runs an accessibility audit against the current published version of the page that you are editing and displays a report in a tab called 'Accessibility'. The tests are run in an iframe directly in Umbraco or optionally using an Azure function. 16 | 17 | ## How do I install it? 18 | 19 | You can install Accessibility Reporter using Nuget `https://www.nuget.org/packages/Umbraco.Community.AccessibilityReporter`. Once installed when you build your project the files needed for Accessibility Reporter will be copied into your App_Plugins folder. That's it! 20 | 21 | ## Options 22 | 23 | You can run Accessibility Reporter without adding any configuration options, as it has some sensible defaults. However, you can configure how it runs by adding an `AccessibilityReporter` section to your `appsettings.json` file. 24 | 25 | ### Available options 26 | 27 | - **ApiUrl** - This is the URL of the API that will run the tests. By default the tests are run in an iframe within Umbraco, however if you website is on a different domain to your Umbraco instance to get around iframe security issues, you can host an API on an Azure function by forking `https://github.com/mattbegent/azure-function-accessibility-reporter` and deploying it to Azure. 28 | - **TestBaseUrl** (optional) - If you run Umbraco in a headless way or Accessibility Reporter is having trouble finding the domain to test against, set this to the base URL of your wesbite. If not set Accessibility Reporter will try to infer this from available information in Umbraco. 29 | - **TestsToRun** (optional) - This sets which axe-core rules should be run. For example, you may want to test your website against `wcag2a` only. A full list of supported tags can be found in the [axe-core documentation](https://www.deque.com/axe/core-documentation/api-documentation/#axe-core-tags). If not set Accessibility Reporter defaults to WCAG A and AA tests. 30 | - **UserGroups** (optional) - Use this option if you want to restrict which user groups can see Accessibility Reporter. By default users with admin, editor or writer permissions can see it. 31 | - **ExcludedDocTypes** (optional) - Use this option if you want to exclude Accessibility Reporter from showing on certain document types. 32 | - **RunTestsAutomatically** (optional) - By default Accessibility Reporter runs as soon as you open up a content node. If you instead want Accessibility Reporter to run on demand via a button click, set this option to false. 33 | - **IncludeIfNoTemplate** (optional) - By default Accessibility Reporter does not run on content without templates. However, if you are using Umbraco in a headless way you will was to set this to true. 34 | - **MaxPages** (optional) - This sets the maximum number of pages that the dashboard will test against. The default is set to 50. 35 | 36 | ### Example options 37 | 38 | "AccessibilityReporter": { 39 | "ApiUrl": "https://api.example.com/api/audit", 40 | "TestBaseUrl": "https://example.com", 41 | "TestsToRun": [ 42 | "wcag2a", 43 | "wcag2aa", 44 | "wcag21a", 45 | "wcag21aa", 46 | "wcag22aa" 47 | ], 48 | "UserGroups": [ 49 | "admin", 50 | "editor", 51 | "writer" 52 | ], 53 | "ExcludedDocTypes": [ 54 | "excludedPage" 55 | ], 56 | "RunTestsAutomatically": false, 57 | "MaxPages": 20 58 | } 59 | 60 | ### Defaults 61 | 62 | All options are completely optional and if you don't set them, they default to the following: 63 | 64 | "AccessibilityReporter": { 65 | "ApiUrl": "", 66 | "TestBaseUrl": "", 67 | "TestsToRun": [ 68 | "wcag2a", 69 | "wcag2aa", 70 | "wcag21a", 71 | "wcag21aa", 72 | "wcag22aa", 73 | "best-practice" 74 | ], 75 | "UserGroups": [ 76 | "admin", 77 | "administrators", 78 | "editor", 79 | "writer", 80 | "translator", 81 | "sensitiveData", 82 | "sensitive data" 83 | ], 84 | "RunTestsAutomatically": true, 85 | "IncludeIfNoTemplate": false, 86 | "MaxPages": 50 87 | } 88 | 89 | ## How to use with a headless setup 90 | 91 | If you use Umbraco in a headless way and you do not have a way of previewing the published page within Umbraco, you will have to setup an azure function in order to get Accessibility Reporter working. This is due to cross domain security restrictions within iframes. 92 | 93 | To do this deploying the following azure function https://github.com/mattbegent/azure-function-accessibility-reporter and update your websites `appsettings.json` file. Here is an example: 94 | 95 | "AccessibilityReporter": { 96 | "ApiUrl": "https://api.example.com/api/audit/", // your azure function 97 | "TestBaseUrl": "https://www.example.com", // base url of your website 98 | "RunTestsAutomatically": false, // as running in a function costs a small amount you might not to run automatically 99 | "IncludeIfNoTemplate": true // headless content probably doesn't have a template 100 | } 101 | 102 | It's worth noting that if you are using Accessibility Reporter in this way the tests will take much longer than if you run Umbraco in a non headless way. 103 | 104 | ## Limitations 105 | 106 | The accessibility report runs on the current published page URL you are editing. 107 | 108 | Automated accessibility testing is no substitute for manual testing and testing using real users. In a [UK government blog article](https://accessibility.blog.gov.uk/2017/02/24/what-we-found-when-we-tested-tools-on-the-worlds-least-accessible-webpage/) they created a test page with 143 accessibility issues on it and the best automated tool only discovered 37% of the issues. However, automated accessibility testing does help to find common issues and technical failures. 109 | 110 | ## Roadmap 111 | 112 | - History. This will mean the dashboard is automatically populated. 113 | - Scheduling. 114 | - Support for multisite setups. 115 | - Manual test recommendations. 116 | - Localization - if anyone speaks any languages other than English it would be super to get some help. 117 | 118 | ## Contributors 119 | 120 | - [Matt Begent](https://github.com/mattbegent) 121 | - [Jack Durcan](https://github.com/jdurcan) 122 | - [Warren Buckley](https://github.com/warrenbuckley) 123 | 124 | ## License 125 | 126 | Copyright © [Matt Begent](https://mattbegent.co.uk/). 127 | 128 | All source code is licensed under the [Mozilla Public License](https://github.com/mattbegent/azure-function-accessibility-reporter/blob/main/LICENSE). 129 | 130 | ## Third party licensing 131 | 132 | [axe-core](https://github.com/dequelabs/axe-core) is licensed under the [Mozilla Public License 2.0](https://www.mozilla.org/en-US/MPL/2.0/). 133 | 134 | [Chart.js](https://github.com/chartjs/Chart.js) is licensed under the [MIT License](https://github.com/chartjs/Chart.js/blob/master/LICENSE.md). 135 | 136 | [SheetJS Community Edition](https://docs.sheetjs.com/) is licensed under the [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0). 137 | 138 | [patternomaly](https://github.com/ashiguruma/patternomaly) is licensed under the [MIT License](https://github.com/ashiguruma/patternomaly/blob/master/LICENSE). -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Dashboards/accessibilityreporter.dashboard.element.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, html, customElement, state, ifDefined } from "@umbraco-cms/backoffice/external/lit"; 2 | import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; 3 | import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserModel } from '@umbraco-cms/backoffice/current-user'; 4 | import { tryExecute } from '@umbraco-cms/backoffice/resources'; 5 | import { UMB_NOTIFICATION_CONTEXT, UmbNotificationContext } from "@umbraco-cms/backoffice/notification"; 6 | import { AccessibilityReporterAppSettings, ConfigService, DirectoryService, NodeSummaryReadable } from '../api'; 7 | 8 | import AccessibilityReporterService from "../Services/accessibility-reporter.service"; 9 | 10 | import "../Components/ar-chart"; 11 | import "../Components/ar-score"; 12 | import "../Components/ar-pre-test"; 13 | import "../Components/ar-errored"; 14 | import "../Components/ar-running-tests"; 15 | import "../Components/ar-has-results"; 16 | 17 | import PageState from "../Enums/page-state"; 18 | import IResults from "../Interface/IResults"; 19 | 20 | import { generalStyles } from "../Styles/general"; 21 | import IPageResult from "../Interface/IPageResult"; 22 | import AccessibilityReporterAPIService from "../Services/accessibility-reporter-api.service"; 23 | 24 | @customElement('accessibility-reporter-dashboard') 25 | export class AccessibilityReporterDashboardElement extends UmbElementMixin(LitElement) { 26 | 27 | private DASHBOARD_STORAGE_KEY = "AR.Dashboard"; 28 | 29 | @state() 30 | private pageState: PageState; 31 | 32 | @state() 33 | private currentTestUrl: string; 34 | 35 | @state() 36 | private results: IResults | undefined; 37 | 38 | @state() 39 | private currentTestNumber: number | undefined; 40 | 41 | @state() 42 | private testPages: NodeSummaryReadable[]; 43 | 44 | @state() 45 | config: AccessibilityReporterAppSettings | undefined; 46 | 47 | @state() 48 | currentUser: UmbCurrentUserModel | undefined; 49 | 50 | private _notificationContext?: UmbNotificationContext; 51 | 52 | constructor() { 53 | super(); 54 | this.pageState = PageState.PreTest; 55 | this.init(); 56 | } 57 | 58 | private async init() { 59 | 60 | this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => { 61 | if(!context) { 62 | return; 63 | } 64 | this.observe( 65 | context.currentUser, 66 | (currentUser) => { 67 | this.currentUser = currentUser; 68 | }, 69 | 'currrentUserObserver', 70 | ); 71 | }); 72 | 73 | this.consumeContext(UMB_NOTIFICATION_CONTEXT, (_instance) => { 74 | this._notificationContext = _instance; 75 | }); 76 | 77 | this.config = await this.getConfig(); 78 | 79 | /* Expose config to child iframe for tests */ 80 | /*@ts-ignore*/ 81 | window.ACCESSIBILITY_REPORTER_CONFIG = this.config; 82 | 83 | this.loadDashboard(); 84 | 85 | } 86 | 87 | private loadDashboard() { 88 | 89 | const dashboardResultsFromStorage = AccessibilityReporterService.getItemFromLocalStorage(this.DASHBOARD_STORAGE_KEY); 90 | if (dashboardResultsFromStorage) { 91 | this.results = dashboardResultsFromStorage; 92 | this.pageState = PageState.HasResults; 93 | 94 | if (this.results && 95 | this.results.endTime && 96 | new Date(this.results.endTime).getTime() < Date.now() - 7 * 24 * 60 * 60 * 1000) { 97 | this._notificationContext?.peek('danger', { data: { message: 'The results shown are older than 7 days. Please run a new test to get the latest results.' } }); 98 | } 99 | } 100 | 101 | } 102 | 103 | private async runSingleTest(page: any) { 104 | 105 | const testRun = new Promise(async (resolve, reject) => { 106 | 107 | try { 108 | this.currentTestUrl = page.url; 109 | const currentResult = await this.getTestResult(page.url); 110 | let resultFormatted = this.reduceTestResult(currentResult); 111 | resultFormatted.score = AccessibilityReporterService.getPageScore(resultFormatted); 112 | resultFormatted.page = page; 113 | resolve(resultFormatted); 114 | } catch (error) { 115 | reject(error); 116 | } 117 | }); 118 | 119 | const testTimeout = this.config?.apiUrl ? 30000 : 10000; 120 | const timer = new Promise((_resolve, reject) => setTimeout(() => reject("Test run exceeded timeout"), testTimeout)); 121 | 122 | return await Promise.race([testRun, timer]); 123 | } 124 | 125 | private async runTests() { 126 | this.pageState = PageState.RunningTests; 127 | this.results = undefined; 128 | this.currentTestUrl = ""; 129 | this.currentTestNumber = undefined; 130 | //this.pagesTestResults = []; 131 | this.testPages = []; 132 | 133 | const startTime = new Date(); 134 | 135 | try { 136 | await this.getTestPages(); 137 | } catch (error) { 138 | console.error(error); 139 | return; 140 | } 141 | 142 | if (!this.testPages) { 143 | console.log('error', this.testPages); 144 | this.pageState = PageState.Errored; 145 | return; 146 | } 147 | 148 | var testResults = []; 149 | for (let index = 0; index < this.testPages.length; index++) { 150 | const currentPage = this.testPages[index]; 151 | try { 152 | this.currentTestNumber = index + 1; 153 | const result = await this.runSingleTest(currentPage) as IPageResult; 154 | testResults.push(result); 155 | if (this.pageState !== PageState.RunningTests) { 156 | break; 157 | } 158 | } catch (error) { 159 | continue; 160 | } 161 | } 162 | 163 | if(!testResults.length) { 164 | console.log('error test results', testResults); 165 | this.pageState = PageState.Errored; 166 | return; 167 | } 168 | 169 | 170 | if (this.pageState !== PageState.RunningTests) { 171 | return; 172 | } 173 | 174 | this.results = { 175 | startTime: startTime, 176 | endTime: new Date(), 177 | pages: testResults 178 | }; 179 | AccessibilityReporterService.saveToLocalStorage(this.DASHBOARD_STORAGE_KEY, this.results as object); 180 | this.pageState = PageState.HasResults; 181 | } 182 | 183 | private async getTestResult(testUrl: string) { 184 | return this.config?.apiUrl ? AccessibilityReporterAPIService.getIssues(this.config, testUrl, this.currentUser?.languageIsoCode ?? "") : AccessibilityReporterService.runTest(this.shadowRoot, testUrl, true); 185 | } 186 | 187 | private reduceTestResult(testResult: any) { 188 | const { inapplicable, incomplete, passes, testEngine, testEnvironment, testRunner, toolOptions, url, timestamp, ...resultFormatted } = testResult; 189 | 190 | resultFormatted.violations = resultFormatted.violations.map((violation: any) => { 191 | return { 192 | id: violation.id, 193 | impact: violation.impact, 194 | tags: violation.tags, 195 | title: violation.help, 196 | description: violation.description, 197 | nodes: violation.nodes.map((node: any) => { 198 | return { 199 | impact: node.impact 200 | } 201 | }) 202 | } 203 | 204 | }); 205 | 206 | return resultFormatted; 207 | } 208 | 209 | private stopTests() { 210 | this.pageState = PageState.PreTest; 211 | } 212 | 213 | private async getTestPages(): Promise { 214 | const { data, error } = await tryExecute(this, DirectoryService.pages()) 215 | if (error) { 216 | console.error(error); 217 | this.pageState = PageState.Errored; 218 | return undefined; 219 | } 220 | 221 | if (data) { 222 | this.testPages = data; 223 | } 224 | 225 | return data; 226 | } 227 | 228 | private async getConfig(): Promise { 229 | const { data, error } = await tryExecute(this, ConfigService.current()) 230 | if (error) { 231 | console.error(error); 232 | this.pageState = PageState.Errored; 233 | return undefined; 234 | } 235 | 236 | return data; 237 | } 238 | 239 | render() { 240 | if (this.pageState === PageState.PreTest) { 241 | return html` 242 | 243 | `; 244 | } 245 | 246 | if (this.pageState === PageState.RunningTests) { 247 | return html` 248 | 254 |
255 |
256 | `; 257 | } 258 | 259 | if (this.pageState === PageState.Errored) { 260 | return html` 261 | 262 | `; 263 | } 264 | 265 | if (this.pageState === PageState.HasResults && this.results && this.config) { 266 | return html` 267 | 272 | `; 273 | } 274 | } 275 | 276 | static styles = [ 277 | generalStyles, 278 | css` 279 | :host { 280 | display: block; 281 | padding: 24px; 282 | } 283 | `, 284 | ]; 285 | } 286 | 287 | export default AccessibilityReporterDashboardElement; 288 | 289 | declare global { 290 | interface HTMLElementTagNameMap { 291 | 'accessibility-reporter-dashboard': AccessibilityReporterDashboardElement; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/AccessibilityReporter.Website/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | 367 | ## 368 | ## Visual studio for Mac 369 | ## 370 | 371 | 372 | # globs 373 | Makefile.in 374 | *.userprefs 375 | *.usertasks 376 | config.make 377 | config.status 378 | aclocal.m4 379 | install-sh 380 | autom4te.cache/ 381 | *.tar.gz 382 | tarballs/ 383 | test-results/ 384 | 385 | # Mac bundle stuff 386 | *.dmg 387 | *.app 388 | 389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 390 | # General 391 | .DS_Store 392 | .AppleDouble 393 | .LSOverride 394 | 395 | # Icon must end with two \r 396 | Icon 397 | 398 | 399 | # Thumbnails 400 | ._* 401 | 402 | # Files that might appear in the root of a volume 403 | .DocumentRevisions-V100 404 | .fseventsd 405 | .Spotlight-V100 406 | .TemporaryItems 407 | .Trashes 408 | .VolumeIcon.icns 409 | .com.apple.timemachine.donotpresent 410 | 411 | # Directories potentially created on remote AFP share 412 | .AppleDB 413 | .AppleDesktop 414 | Network Trash Folder 415 | Temporary Items 416 | .apdisk 417 | 418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 419 | # Windows thumbnail cache files 420 | Thumbs.db 421 | ehthumbs.db 422 | ehthumbs_vista.db 423 | 424 | # Dump file 425 | *.stackdump 426 | 427 | # Folder config file 428 | [Dd]esktop.ini 429 | 430 | # Recycle Bin used on file shares 431 | $RECYCLE.BIN/ 432 | 433 | # Windows Installer files 434 | *.cab 435 | *.msi 436 | *.msix 437 | *.msm 438 | *.msp 439 | 440 | # Windows shortcuts 441 | *.lnk 442 | 443 | # JetBrains Rider 444 | .idea/ 445 | *.sln.iml 446 | 447 | ## 448 | ## Visual Studio Code 449 | ## 450 | .vscode/* 451 | !.vscode/settings.json 452 | !.vscode/tasks.json 453 | !.vscode/launch.json 454 | !.vscode/extensions.json 455 | 456 | ## 457 | ## Umbraco CMS 458 | ## 459 | 460 | # JSON schema files for appsettings.json 461 | appsettings-schema.json 462 | appsettings-schema.*.json 463 | 464 | # JSON schema file for umbraco-package.json 465 | umbraco-package-schema.json 466 | 467 | # Packages created from the backoffice (package.xml/package.zip) 468 | /umbraco/Data/CreatedPackages/ 469 | 470 | # Temp folder containing Examine indexes, NuCache, MediaCache, etc. 471 | /umbraco/Data/TEMP/ 472 | 473 | # SQLite database files 474 | /umbraco/Data/*.sqlite.db 475 | /umbraco/Data/*.sqlite.db-shm 476 | /umbraco/Data/*.sqlite.db-wal 477 | 478 | # Log files 479 | /umbraco/Logs/ 480 | 481 | # Media files 482 | /wwwroot/media/ 483 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Services/accessibility-reporter.service.ts: -------------------------------------------------------------------------------- 1 | export default class AccessibilityReporterService { 2 | 3 | static impacts = ["minor", "moderate", "serious", "critical"]; 4 | 5 | static async runTest(rootElement: any, testUrl: string, showWhileRunning: boolean) { 6 | 7 | return new Promise(async (resolve, reject) => { 8 | 9 | try { 10 | const headers = new Headers({ 11 | 'X-User-Agent': 'AccessibilityReporter/1.0' 12 | }); 13 | const testRequest = new Request(testUrl, { 14 | method: 'GET', 15 | headers: headers 16 | }); 17 | await fetch(testRequest); 18 | const iframeId = "arTestIframe" + AccessibilityReporterService.randomUUID(); 19 | const container = showWhileRunning ? rootElement.getElementById('dashboard-ar-tests') : rootElement as HTMLElement; 20 | let testIframe = document.createElement("iframe") as HTMLIFrameElement; 21 | 22 | function cleanUpIframe() { 23 | if (testIframe) { 24 | testIframe.src = ""; 25 | testIframe.remove(); 26 | /*@ts-ignore*/ 27 | testIframe = null; 28 | } 29 | } 30 | 31 | const handleTestResultMessage = function (message: any) { 32 | if (!message.data.testRunner) { 33 | return; 34 | } 35 | if (message.data.testRunner.name !== 'axe') { 36 | return; 37 | } 38 | cleanUpIframe(); 39 | if (message.data) { 40 | resolve(message.data); 41 | } else { 42 | reject(message); 43 | } 44 | message = null; 45 | window.removeEventListener("message", handleTestResultMessage, true); 46 | } 47 | window.addEventListener("message", handleTestResultMessage, true); 48 | 49 | testIframe.setAttribute("src", testUrl); 50 | testIframe.setAttribute("id", iframeId); 51 | testIframe.style.height = "800px"; 52 | if (showWhileRunning) { 53 | testIframe.style.width = container.clientWidth + "px"; 54 | } else { 55 | testIframe.style.width = "1280px"; 56 | testIframe.style.zIndex = "1"; 57 | testIframe.style.position = "absolute"; 58 | } 59 | 60 | setTimeout(() => { 61 | container.appendChild(testIframe); 62 | }, 0); 63 | 64 | testIframe.onload = function () { 65 | if (testIframe?.contentWindow?.document.body) { 66 | let scriptAxe = testIframe.contentWindow.document.createElement("script"); 67 | scriptAxe.type = "text/javascript"; 68 | scriptAxe.src = "/App_Plugins/AccessibilityReporter/libs/axe.min.js"; 69 | testIframe.contentWindow.document.body.appendChild(scriptAxe); 70 | /*@ts-ignore*/ 71 | scriptAxe = null; 72 | } else { 73 | cleanUpIframe(); 74 | reject('Test page has no body.'); 75 | } 76 | }; 77 | 78 | } catch (error) { 79 | // Possible Security Error (another origin) 80 | reject(error); 81 | } 82 | }); 83 | } 84 | 85 | static sortIssuesByImpact(a: any, b: any) { 86 | if (a.impact === b.impact) { 87 | return b.nodes.length - a.nodes.length; 88 | } 89 | if (AccessibilityReporterService.impacts.indexOf(a.impact) > AccessibilityReporterService.impacts.indexOf(b.impact)) { 90 | return -1; 91 | } 92 | if (AccessibilityReporterService.impacts.indexOf(a.impact) < AccessibilityReporterService.impacts.indexOf(b.impact)) { 93 | return 1; 94 | } 95 | return 0; 96 | } 97 | 98 | static sortByViolations(a: any, b: any) { 99 | return b.nodes.length - a.nodes.length; 100 | } 101 | 102 | // https://www.deque.com/axe/core-documentation/api-documentation/ 103 | static mapTagsToStandard(tags: string[]) { 104 | var catTagsRemoved = tags.filter(tag => { 105 | return tag.indexOf('cat.') === -1 && !tag.startsWith('TT') && !tag.startsWith('ACT'); 106 | }); 107 | var formattedTags = catTagsRemoved.map(AccessibilityReporterService.axeTagToStandard); 108 | return formattedTags; 109 | } 110 | 111 | static upperCaseFirstLetter(word: string) { 112 | return word.charAt(0).toUpperCase() + word.slice(1); 113 | } 114 | 115 | static impactToTag(impact: string) { 116 | switch (impact) { 117 | case "serious": 118 | case "critical": 119 | return "danger"; 120 | case "moderate": 121 | return "warning"; 122 | default: 123 | return "default"; 124 | }; 125 | }; 126 | 127 | static axeTagToStandard(tag: string) { 128 | switch (tag) { 129 | case "wcag2a": 130 | return "WCAG 2.0 A"; 131 | case "wcag2aa": 132 | return "WCAG 2.0 AA"; 133 | case "wcag2aaa": 134 | return "WCAG 2.0 AAA"; 135 | case "wcag21a": 136 | return "WCAG 2.1 A"; 137 | case "wcag21aa": 138 | return "WCAG 2.1 AA"; 139 | case "wcag21aaa": 140 | return "WCAG 2.1 AAA"; 141 | case "wcag22a": 142 | return "WCAG 2.2 A"; 143 | case "wcag22aa": 144 | return "WCAG 2.2 AA"; 145 | case "wcag22aaa": 146 | return "WCAG 2.2 AAA"; 147 | case "best-practice": 148 | return "Best Practice"; 149 | case "section508": 150 | return "Section 508"; 151 | default: 152 | break; 153 | } 154 | if (tag.indexOf('wcag') !== -1) { 155 | return tag.toUpperCase(); 156 | } 157 | if (tag.indexOf('section') !== -1) { 158 | return tag.replace('section', 'Section '); 159 | } 160 | return tag; 161 | } 162 | 163 | static getWCAGLevel(tags: string[]) { 164 | for (let index = 0; index < tags.length; index++) { 165 | const tag = tags[index]; 166 | switch (tag) { 167 | case 'wcagaaa': 168 | return 'AAA'; 169 | case 'wcag2aa': 170 | case 'wcag21aa': 171 | case 'wcag22aa': 172 | return 'AA'; 173 | case 'wcag2a': 174 | case 'wcag21a': 175 | case 'wcag22a': 176 | return 'A'; 177 | default: 178 | continue; 179 | } 180 | } 181 | return 'Other'; 182 | } 183 | 184 | static getRule(ruleId: string) { 185 | /*@ts-ignore*/ 186 | const allRules = axe.getRules(); 187 | return allRules.find((rule: any) => rule.ruleId = ruleId); 188 | } 189 | 190 | static getBaseURL() { 191 | return location.protocol + "//" + location.hostname + (location.port ? ":" + location.port : ""); 192 | } 193 | 194 | static formatResultForSaving(result: any, nodeId: string, culture: string) { 195 | 196 | return { 197 | "url": result.url, 198 | "nodeId": nodeId, 199 | "culture": culture, 200 | "date": result.timestamp, 201 | "violations": result.violations.map((test: any) => { 202 | return { 203 | id: test.id, 204 | errors: test.nodes.length 205 | } 206 | }), 207 | "incomplete": result.violations.map((test: any) => { 208 | return { 209 | id: test.id, 210 | errors: test.nodes.length 211 | } 212 | }), 213 | "passes": result.violations.map((test: any) => { 214 | return { 215 | id: test.id, 216 | elements: test.nodes.length 217 | } 218 | }) 219 | } 220 | 221 | } 222 | 223 | static saveToLocalStorage(key: string, value: object) { 224 | try { 225 | localStorage.setItem(key, JSON.stringify(value)); 226 | } catch (error) { 227 | console.error(error); 228 | } 229 | 230 | } 231 | 232 | static getItemFromLocalStorage(key: string) { 233 | const item = localStorage.getItem(key); 234 | if (item) { 235 | return JSON.parse(item); 236 | } else { 237 | return null; 238 | } 239 | } 240 | 241 | static isAbsoluteURL(urlString: string) { 242 | return urlString.indexOf('http://') === 0 || urlString.indexOf('https://') === 0; 243 | } 244 | 245 | static getHostnameFromString(url: string) { 246 | return new URL(url).hostname; 247 | } 248 | 249 | static getPageScore(result: any) { 250 | let score = 100; 251 | for (let index = 0; index < result.violations.length; index++) { 252 | const currentViolation = result.violations[index]; 253 | score -= AccessibilityReporterService.getRuleWeight(currentViolation.id); 254 | } 255 | return Math.max(0, score); 256 | } 257 | 258 | // https://developer.chrome.com/docs/lighthouse/accessibility/scoring/ 259 | static getRuleWeight(ruleId: string) { 260 | 261 | switch (ruleId) { 262 | case "accesskeys": 263 | return 7; 264 | case "aria-allowed-attr": 265 | return 10; 266 | case "aria-allowed-role": 267 | return 1; 268 | case "aria-command-name": 269 | return 7; 270 | case "aria-dialog-name": 271 | return 7; 272 | case "aria-hidden-body": 273 | return 10; 274 | case "aria-hidden-focus": 275 | return 7; 276 | case "aria-input-field-name": 277 | return 7; 278 | case "aria-meter-name": 279 | return 7; 280 | case "aria-progressbar-name": 281 | return 7; 282 | case "aria-required-attr": 283 | return 10; 284 | case "aria-required-children": 285 | return 10; 286 | case "aria-required-parent": 287 | return 10; 288 | case "aria-roles": 289 | return 7; 290 | case "aria-text": 291 | return 7; 292 | case "aria-toggle-field-name": 293 | return 7; 294 | case "aria-tooltip-name": 295 | return 7; 296 | case "aria-treeitem-name": 297 | return 7; 298 | case "aria-valid-attr-value": 299 | return 10; 300 | case "aria-valid-attr": 301 | return 10; 302 | case "button-name": 303 | return 10; 304 | case "bypass": 305 | return 7; 306 | case "color-contrast": 307 | return 7; 308 | case "definition-list": 309 | return 7; 310 | case "dlitem": 311 | return 7; 312 | case "document-title": 313 | return 7; 314 | case "duplicate-id-active": 315 | return 7; 316 | case "duplicate-id-aria": 317 | return 10; 318 | case "form-field-multiple-labels": 319 | return 3; 320 | case "frame-title": 321 | return 7; 322 | case "heading-order": 323 | return 3; 324 | case "html-has-lang": 325 | return 7; 326 | case "html-lang-valid": 327 | return 7; 328 | case "html-xml-lang-mismatch": 329 | return 3; 330 | case "image-alt": 331 | return 10; 332 | case "image-redundant-alt": 333 | return 1; 334 | case "input-button-name": 335 | return 10; 336 | case "input-image-alt": 337 | return 10; 338 | case "label-content-name-mismatch": 339 | return 7; 340 | case "label": 341 | return 7; 342 | case "link-in-text-block": 343 | return 7; 344 | case "link-name": 345 | return 7; 346 | case "list": 347 | return 7; 348 | case "listitem": 349 | return 7; 350 | case "meta-refresh": 351 | return 10; 352 | case "meta-viewport": 353 | return 10; 354 | case "object-alt": 355 | return 7; 356 | case "select-name": 357 | return 7; 358 | case "skip-link": 359 | return 3; 360 | case "tabindex": 361 | return 7; 362 | case "table-duplicate-name": 363 | return 1; 364 | case "table-fake-caption": 365 | return 7; 366 | case "td-has-header": 367 | return 10; 368 | case "td-headers-attr": 369 | return 7; 370 | case "th-has-data-cells": 371 | return 7; 372 | case "valid-lang": 373 | return 7; 374 | case "video-caption": 375 | return 10; 376 | default: 377 | return 0; 378 | }; 379 | 380 | } 381 | 382 | static formatFileName(name: string) { 383 | return name.replace(/\s+/g, '-').toLowerCase(); 384 | } 385 | 386 | static formatNumber(numberToFormat: number) { 387 | return numberToFormat.toLocaleString(); 388 | } 389 | 390 | static randomUUID() { 391 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 392 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 393 | return v.toString(16); 394 | }); 395 | } 396 | 397 | } 398 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/Components/ar-has-results.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, customElement, property, state, unsafeHTML } from "@umbraco-cms/backoffice/external/lit"; 2 | import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; 3 | 4 | import { utils, writeFile } from "xlsx"; 5 | import { format } from 'date-fns'; 6 | 7 | import './ar-logo'; 8 | import './ar-chart'; 9 | import './ar-score'; 10 | 11 | import AccessibilityReporterService from "../Services/accessibility-reporter.service"; 12 | import IResults from "../Interface/IResults"; 13 | 14 | import { generalStyles } from "../Styles/general"; 15 | import { UMB_NOTIFICATION_CONTEXT, UmbNotificationContext } from "@umbraco-cms/backoffice/notification"; 16 | import { AccessibilityReporterAppSettings } from "../api"; 17 | import { UmbDocumentDetailRepository } from "@umbraco-cms/backoffice/document"; 18 | import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserModel } from "@umbraco-cms/backoffice/current-user"; 19 | 20 | @customElement("ar-has-results") 21 | export class ARHasResultsElement extends UmbElementMixin(LitElement) { 22 | 23 | @property() 24 | onRunTests = () => { }; 25 | 26 | @property({ attribute: false }) 27 | public results: IResults | undefined; 28 | 29 | @property({ attribute: false }) 30 | public config: AccessibilityReporterAppSettings; 31 | 32 | @state() 33 | private pageSize = 5; 34 | 35 | @state() 36 | private currentPage = 1; 37 | 38 | @state() 39 | private pagesTestResults: any = []; 40 | 41 | @state() 42 | private totalErrors: number | null = null; 43 | 44 | @state() 45 | private averagePageScore: number | null = null; 46 | 47 | @state() 48 | private pageWithLowestScore: any = null; 49 | 50 | @state() 51 | private numberOfPagesTested: number | null = null; 52 | 53 | @state() 54 | private mostCommonErrors: any = null; 55 | 56 | @state() 57 | private totalViolations: number | null = null; 58 | 59 | @state() 60 | private totalAAAViolations: number | null = null; 61 | 62 | @state() 63 | private totalAAViolations: number | null = null; 64 | 65 | @state() 66 | private totalAViolations: number | null = null; 67 | 68 | @state() 69 | private totalOtherViolations: number | null = null; 70 | 71 | @state() 72 | private severityChartData: any; 73 | 74 | @state() 75 | private topViolationsChartData: any; 76 | 77 | @state() 78 | private pagination: any; 79 | 80 | @state() 81 | private pagesTestResultsCurrentPage: any; 82 | 83 | @state() 84 | private reportSummaryText: string = ""; 85 | 86 | @state() 87 | private pageUrls: Map = new Map(); 88 | 89 | private _notificationContext?: UmbNotificationContext; 90 | 91 | @state() 92 | private _currentUser?: UmbCurrentUserModel; 93 | 94 | constructor() { 95 | super(); 96 | this.consumeContext(UMB_NOTIFICATION_CONTEXT, (_instance) => { 97 | this._notificationContext = _instance; 98 | }); 99 | this.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => { 100 | if (!instance) { 101 | return; 102 | } 103 | this._observeCurrentUser(instance); 104 | }); 105 | } 106 | 107 | private async _observeCurrentUser(instance: typeof UMB_CURRENT_USER_CONTEXT.TYPE) { 108 | this.observe(instance.currentUser, (currentUser) => { 109 | this._currentUser = currentUser; 110 | }); 111 | } 112 | 113 | connectedCallback() { 114 | super.connectedCallback(); 115 | this.setStats(this.results); 116 | } 117 | 118 | private formatTime(dateToFormat: Date) { 119 | return format(dateToFormat, "HH:mm:ss"); 120 | } 121 | 122 | private setStats(testResults: any) { 123 | let totalErrors = 0; 124 | let allErrors: any = []; 125 | let totalViolations = 0; 126 | let totalAViolations = 0; 127 | let totalAAViolations = 0; 128 | let totalAAAViolations = 0; 129 | let totalOtherViolations = 0; 130 | 131 | let pagesTestResults = []; 132 | for (let index = 0; index < testResults.pages.length; index++) { 133 | const currentResult = testResults.pages[index]; 134 | totalErrors += currentResult.violations.length; 135 | allErrors = allErrors.concat(currentResult.violations); 136 | 137 | let totalViolationsForPage = 0; 138 | 139 | for (let indexVoilations = 0; indexVoilations < currentResult.violations.length; indexVoilations++) { 140 | const currentViolation = currentResult.violations[indexVoilations]; 141 | const violationWCAGLevel = AccessibilityReporterService.getWCAGLevel(currentViolation.tags); 142 | switch (violationWCAGLevel) { 143 | case 'AAA': 144 | totalAAAViolations += currentViolation.nodes.length; 145 | break; 146 | case 'AA': 147 | totalAAViolations += currentViolation.nodes.length; 148 | break; 149 | case 'A': 150 | totalAViolations += currentViolation.nodes.length; 151 | break; 152 | case 'Other': 153 | totalOtherViolations += currentViolation.nodes.length; 154 | break; 155 | } 156 | totalViolations += currentViolation.nodes.length; 157 | totalViolationsForPage += currentViolation.nodes.length; 158 | } 159 | 160 | pagesTestResults.push({ 161 | id: currentResult.page.id, 162 | guid: currentResult.page.guid, 163 | name: currentResult.page.name, 164 | url: currentResult.page.url, 165 | score: currentResult.score, 166 | violations: totalViolationsForPage 167 | }); 168 | } 169 | 170 | this.numberOfPagesTested = testResults.pages.length; 171 | this.totalErrors = totalErrors; 172 | 173 | this.totalViolations = totalViolations; 174 | this.totalAAAViolations = totalAAAViolations; 175 | this.totalAAViolations = totalAAViolations; 176 | this.totalAViolations = totalAViolations; 177 | this.totalOtherViolations = totalOtherViolations; 178 | 179 | this.reportSummaryText = this.getReportSummaryText(); 180 | 181 | this.averagePageScore = this.getAveragePageScore(testResults.pages); 182 | this.pageWithLowestScore = this.getPageWithLowestScore(testResults.pages); 183 | 184 | const sortedByImpact = allErrors.sort(AccessibilityReporterService.sortIssuesByImpact); 185 | this.mostCommonErrors = this.getErrorsSortedByViolations(allErrors).slice(0, 6); 186 | 187 | this.pagesTestResults = pagesTestResults.sort(this.sortPageTestResults); 188 | 189 | this.displaySeverityChart(sortedByImpact); 190 | this.topViolationsChart(); 191 | 192 | this.paginateResults(); 193 | 194 | } 195 | 196 | private getAveragePageScore(results: any) { 197 | let totalScore = 0; 198 | for (let index = 0; index < results.length; index++) { 199 | const result = results[index]; 200 | totalScore += result.score; 201 | } 202 | return Math.round(totalScore / results.length); 203 | } 204 | 205 | private getPageWithLowestScore(results: any) { 206 | let lowestScore = 0; 207 | let pageWithLowestScore = null; 208 | for (let index = 0; index < results.length; index++) { 209 | const result = results[index]; 210 | if (!pageWithLowestScore) { 211 | lowestScore = result.score; 212 | pageWithLowestScore = result; 213 | continue; 214 | } 215 | if (result.score < lowestScore) { 216 | lowestScore = result.score; 217 | pageWithLowestScore = result; 218 | } 219 | } 220 | 221 | return pageWithLowestScore; 222 | } 223 | 224 | private getHighestLevelOfNonCompliance() { 225 | if (this.totalAAAViolations !== 0) { 226 | return 'AAA'; 227 | } 228 | if (this.totalAAViolations !== 0) { 229 | return 'AA'; 230 | } 231 | if (this.totalAViolations !== 0) { 232 | return 'A'; 233 | } 234 | return null; 235 | } 236 | 237 | private getReportSummaryText() { 238 | const highestLevelOfNonCompliance = this.getHighestLevelOfNonCompliance(); 239 | if (highestLevelOfNonCompliance) { 240 | return `This website does not comply with WCAG ${highestLevelOfNonCompliance}.`; 241 | } 242 | if (this.totalOtherViolations !== 0) { 243 | return "High 5, you rock! No WCAG violations were found. However, some other issues were found. Please manually test your website to check full compliance."; 244 | } 245 | return "High 5, you rock! No WCAG violations were found. Please manually test your website to check full compliance."; 246 | } 247 | 248 | private displaySeverityChart(sortedAllErrors: any) { 249 | 250 | function countNumberOfTestsWithImpact(errors: any, impact: string) { 251 | var totalViolationsForForImpact = 0; 252 | for (let index = 0; index < errors.length; index++) { 253 | const currentError = errors[index]; 254 | if (currentError.impact === impact) { 255 | totalViolationsForForImpact += currentError.nodes.length; 256 | } 257 | } 258 | return totalViolationsForForImpact; 259 | } 260 | 261 | this.severityChartData = { 262 | labels: [ 263 | 'Critical', 264 | 'Serious', 265 | 'Moderate', 266 | 'Minor' 267 | ], 268 | datasets: [{ 269 | label: 'Violations', 270 | data: [ 271 | countNumberOfTestsWithImpact(sortedAllErrors, 'critical'), 272 | countNumberOfTestsWithImpact(sortedAllErrors, 'serious'), 273 | countNumberOfTestsWithImpact(sortedAllErrors, 'moderate'), 274 | countNumberOfTestsWithImpact(sortedAllErrors, 'minor') 275 | ], 276 | backgroundColor: [ 277 | 'rgb(120,0,0)', 278 | 'rgb(212, 32, 84)', 279 | 'rgb(250, 214, 52)', 280 | 'rgb(49, 68, 142)' 281 | ], 282 | hoverOffset: 4, 283 | rotation: 0 284 | }], 285 | patterns: [ 286 | '', 287 | 'diagonal', 288 | 'zigzag-horizontal', 289 | 'dot' 290 | ] 291 | }; 292 | } 293 | 294 | private topViolationsChart() { 295 | this.topViolationsChartData = { 296 | labels: this.mostCommonErrors.map((error: any) => error.id.replaceAll('-', ' ').replace(/(^\w{1})|(\s+\w{1})/g, (letter: string) => letter.toUpperCase())), 297 | datasets: [{ 298 | label: 'Violations', 299 | data: this.mostCommonErrors.map((error: any) => error.errors), 300 | backgroundColor: [ 301 | 'rgba(255, 99, 132, 1)', 302 | 'rgba(255, 159, 64, 1)', 303 | 'rgba(255, 205, 86, 1)', 304 | 'rgba(75, 192, 192, 1)', 305 | 'rgba(54, 162, 235, 1)', 306 | 'rgba(153, 102, 255, 1)', 307 | 'rgba(201, 203, 207, 1)' 308 | ] 309 | }] 310 | }; 311 | } 312 | 313 | 314 | 315 | private getErrorsSortedByViolations(errors: any) { 316 | 317 | let allErrors: any = []; 318 | for (let index = 0; index < errors.length; index++) { 319 | const currentError = errors[index]; 320 | if (!allErrors.some((error: any) => error.id === currentError.id)) { 321 | allErrors.push({ 322 | id: currentError.id, 323 | errors: currentError.nodes.length 324 | }); 325 | } else { 326 | const errorIndex = allErrors.findIndex(((error: any) => error.id == currentError.id)); 327 | allErrors[errorIndex].errors += currentError.nodes.length; 328 | } 329 | } 330 | 331 | const sortedAllErrors = allErrors.sort((a: any, b: any) => b.errors - a.errors); 332 | return sortedAllErrors; 333 | } 334 | 335 | private sortPageTestResults(a: any, b: any) { 336 | if (a.score === b.score) { 337 | return b.violations - a.violations; 338 | } 339 | if (a.score < b.score) { 340 | return -1; 341 | } 342 | if (a.score > b.score) { 343 | return 1; 344 | } 345 | return 0; 346 | } 347 | 348 | 349 | private showViolationsForLevel(level: any) { 350 | for (let index = 0; index < this.config.testsToRun.length; index++) { 351 | const currentLevel = this.config.testsToRun[index]; 352 | if (currentLevel.endsWith(`2${level}`) || 353 | currentLevel.endsWith(`21${level}`) || 354 | currentLevel.endsWith(`22${level}`)) { 355 | return true; 356 | } 357 | } 358 | return false;; 359 | } 360 | 361 | private getDataForPagination(array: any, page_size: any, page_number: any) { 362 | return array.slice((page_number - 1) * page_size, page_number * page_size); 363 | } 364 | 365 | private paginateResults() { 366 | this.pagination = this.paginate(this.pagesTestResults.length, this.currentPage, this.pageSize); 367 | this.pagesTestResultsCurrentPage = this.getDataForPagination(this.pagesTestResults, this.pageSize, this.currentPage); 368 | } 369 | 370 | private changePage(pageNumber: number) { 371 | this.currentPage = pageNumber; 372 | this.paginateResults(); 373 | } 374 | 375 | private paginate(totalItems: number, currentPage: number, pageSize: number) { 376 | let totalPages = Math.ceil(totalItems / pageSize); 377 | if (currentPage < 1) { 378 | currentPage = 1; 379 | } else if (currentPage > totalPages) { 380 | currentPage = totalPages; 381 | } 382 | return { 383 | currentPage: currentPage, 384 | totalPages: totalPages 385 | }; 386 | } 387 | 388 | private async handleWorkspaceClick(event: Event, pageGuid: string): Promise { 389 | event.preventDefault(); 390 | 391 | // Check if we already have the URL cached 392 | if (this.pageUrls.has(pageGuid)) { 393 | window.location.href = this.pageUrls.get(pageGuid)!; 394 | return; 395 | } 396 | 397 | try { 398 | const url = await this.generateWorkspaceUrl(pageGuid); 399 | 400 | // Cache the URL for future clicks 401 | const newPageUrls = new Map(this.pageUrls); 402 | newPageUrls.set(pageGuid, url); 403 | this.pageUrls = newPageUrls; 404 | 405 | // Navigate to the workspace 406 | window.location.href = url; 407 | 408 | } catch (error) { 409 | console.error('Error generating workspace URL:', error); 410 | // Fallback to invariant URL 411 | const fallbackUrl = `/umbraco/section/content/workspace/document/edit/${pageGuid}/invariant/view/accessibility-reporter`; 412 | window.location.href = fallbackUrl; 413 | } 414 | } 415 | 416 | private async generateWorkspaceUrl(pageGuid: string): Promise { 417 | const baseUrl = `/umbraco/section/content/workspace/document/edit/${pageGuid}`; 418 | 419 | try { 420 | const documentRepository = new UmbDocumentDetailRepository(this); 421 | const { data: document } = await documentRepository.requestByUnique(pageGuid); 422 | 423 | if (!document) { 424 | return `${baseUrl}/invariant/view/accessibility-reporter`; 425 | } 426 | 427 | const availableVariants = document.variants || []; 428 | const availableCultures = availableVariants 429 | .map(variant => variant.culture) 430 | .filter((culture): culture is string => Boolean(culture)); 431 | 432 | let selectedCulture = 'invariant'; 433 | 434 | if (availableCultures.length === 0) { 435 | selectedCulture = 'invariant'; 436 | } else if (availableCultures.length === 1) { 437 | selectedCulture = availableCultures[0]; 438 | } else { 439 | // Get user's culture from Umbraco user context 440 | let userLanguage = 'en-US'; // Default to Umbraco default culture 441 | if (this._currentUser) { 442 | const currentUser = this._currentUser; 443 | // Check if user has a language/culture preference 444 | if (currentUser && typeof currentUser === 'object' && 'languageIsoCode' in currentUser) { 445 | const userLangCode = (currentUser as any).languageIsoCode; 446 | if (typeof userLangCode === 'string' && userLangCode) { 447 | userLanguage = userLangCode; 448 | } 449 | } 450 | } 451 | 452 | const exactMatch = availableCultures.find(culture => 453 | culture && culture.toLowerCase() === userLanguage.toLowerCase() 454 | ); 455 | 456 | if (exactMatch) { 457 | selectedCulture = exactMatch; 458 | } else { 459 | const userLanguageCode = userLanguage.split('-')[0]; 460 | const languageMatch = availableCultures.find(culture => 461 | culture && culture.split('-')[0].toLowerCase() === userLanguageCode.toLowerCase() 462 | ); 463 | 464 | selectedCulture = languageMatch || availableCultures[0] || 'invariant'; 465 | } 466 | } 467 | 468 | return `${baseUrl}/${selectedCulture}/view/accessibility-reporter`; 469 | 470 | } catch (error) { 471 | console.error('Error getting document details:', error); 472 | return `${baseUrl}/invariant/view/accessibility-reporter`; 473 | } 474 | } 475 | 476 | 477 | 478 | private exportResults() { 479 | 480 | if (!this.results) { 481 | return; 482 | } 483 | 484 | try { 485 | 486 | const workbook = utils.book_new(); 487 | 488 | const pagesRows = this.pagesTestResults.map((page: any) => ({ 489 | name: page.name, 490 | url: page.url, 491 | score: page.score, 492 | violations: page.violations 493 | })); 494 | 495 | const pagesWorksheet = utils.json_to_sheet(pagesRows); 496 | utils.book_append_sheet(workbook, pagesWorksheet, "Pages Summary"); 497 | 498 | const pagesHeaders = [["Name", "URL", "Accessibility Score", "Total Violations"]]; 499 | utils.sheet_add_aoa(pagesWorksheet, pagesHeaders, { origin: "A1" }); 500 | 501 | pagesWorksheet["!cols"] = [ 502 | { width: 30 }, // Name 503 | { width: 40 }, // URL 504 | { width: 20 }, // Score 505 | { width: 15 } // Violations 506 | ]; 507 | 508 | 509 | let allViolations: any[] = []; 510 | 511 | this.results.pages.forEach(pageResult => { 512 | const pageName = pageResult.page.name; 513 | const pageUrl = pageResult.page.url; 514 | 515 | pageResult.violations.forEach(violation => { 516 | allViolations.push({ 517 | pageName: pageName, 518 | pageUrl: pageUrl, 519 | impact: violation.impact ? AccessibilityReporterService.upperCaseFirstLetter(violation.impact) : '', 520 | title: violation.title || '', 521 | description: violation.description || '', 522 | standard: AccessibilityReporterService.mapTagsToStandard(violation.tags).join(', '), 523 | nodeCount: violation.nodes ? violation.nodes.length : 0 524 | }); 525 | }); 526 | }); 527 | 528 | if (allViolations.length > 0) { 529 | const violationsWorksheet = utils.json_to_sheet(allViolations); 530 | utils.book_append_sheet(workbook, violationsWorksheet, "All Violations"); 531 | 532 | const violationsHeaders = [["Name", "URL", "Impact", "Title", "Description", "Accessibility Standard", "Instances"]]; 533 | utils.sheet_add_aoa(violationsWorksheet, violationsHeaders, { origin: "A1" }); 534 | 535 | const titleWidth = allViolations.reduce((w, r) => Math.max(w, r.title ? r.title.length : 0), 40); 536 | violationsWorksheet["!cols"] = [ 537 | { width: 25 }, // Name 538 | { width: 40 }, // URL 539 | { width: 10 }, // Impact 540 | { width: titleWidth }, // Title 541 | { width: 50 }, // Description 542 | { width: 25 }, // Standard 543 | { width: 10 } // Count 544 | ]; 545 | } 546 | 547 | writeFile(workbook, AccessibilityReporterService.formatFileName(`website-accessibility-report-${format(this.results.endTime, "yyyy-MM-dd")}`) + ".xlsx", { compression: true }); 548 | 549 | } catch (error) { 550 | console.error(error); 551 | this._notificationContext?.peek('danger', { data: { message: 'An error occurred exporting the report. Please try again later.' } }); 552 | } 553 | 554 | }; 555 | 556 | render() { 557 | return html` 558 | 559 |
560 | 561 | 562 |
563 |

Accessibility Report

564 |
565 |
566 |

${unsafeHTML(this.reportSummaryText)}

567 |
568 | ${this.showViolationsForLevel('a') ? 569 | html` 570 |
571 |
572 | ${AccessibilityReporterService.formatNumber(this.totalAViolations || 0)} 573 | A Issues 574 |
575 |
576 | ` : null} 577 | ${this.showViolationsForLevel('aa') ? 578 | html` 579 |
580 |
581 | ${AccessibilityReporterService.formatNumber(this.totalAAViolations || 0)} 582 | AA Issues 583 |
584 |
585 | ` : null} 586 | ${this.showViolationsForLevel('aaa') ? 587 | html` 588 |
589 |
590 | ${AccessibilityReporterService.formatNumber(this.totalAAAViolations || 0)} 591 | AAA Issues 592 |
593 |
594 | ` : null} 595 |
596 |
597 | ${AccessibilityReporterService.formatNumber(this.totalOtherViolations || 0)} 598 | Other Issues 599 |
600 |
601 |
602 | Rerun tests 603 | Export results 604 | ${this.results ? 605 | html`Started at ${this.formatTime(this.results.startTime)} and ended at ${this.formatTime(this.results.endTime)}` 606 | : null} 607 |
608 |
609 | 610 | 611 |
612 |

Total Violations

613 |
614 |

${AccessibilityReporterService.formatNumber(this.totalViolations || 0)}

615 |

Across ${AccessibilityReporterService.formatNumber(this.totalErrors || 0)} different failed tests

616 |
617 | 618 | 619 |
620 |

Average Page Score

621 |
622 |
623 | 624 |

${AccessibilityReporterService.formatNumber(this.numberOfPagesTested || 0)} pages tested

625 |
626 |
627 | 628 | 629 |
630 |

Lowest Page Score

631 |
632 |
633 | 634 |

${this.pageWithLowestScore.page.name}

635 |
636 |
637 | 638 | 639 |
640 |

Violation Severity

641 |
642 | 643 |
644 | 645 | 646 |
647 |

Top Violations

648 |
649 | 650 |
651 | 652 | 653 |
654 |

Pages Sorted By Lowest Score

655 |
656 | 657 | 658 | Name 659 | URL 660 | Score 661 | Violations 662 | Action 663 | 664 | ${this.pagesTestResultsCurrentPage.map((page: any) => 665 | html` 666 | ${page.name} 667 | ${page.url} Opens in a new window 668 | ${page.score} 669 | ${page.violations} 670 | 671 | 681 | 682 | ` 683 | )} 684 | 685 | 692 | 693 |
694 | 695 |
696 |
697 | `; 698 | } 699 | 700 | static styles = [ 701 | generalStyles 702 | ]; 703 | } 704 | 705 | declare global { 706 | interface HTMLElementTagNameMap { 707 | "ar-has-results": ARHasResultsElement; 708 | } 709 | } 710 | -------------------------------------------------------------------------------- /src/AccessibilityReporter/client/src/WorkspaceView/accessibilityreporter.workspaceview.element.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, html, customElement, state } from "@umbraco-cms/backoffice/external/lit"; 2 | import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; 3 | import { format } from 'date-fns' 4 | import PageState from "../Enums/page-state"; 5 | import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserModel } from "@umbraco-cms/backoffice/current-user"; 6 | import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document'; 7 | import { tryExecute } from "@umbraco-cms/backoffice/resources"; 8 | import { AccessibilityReporterAppSettings, ConfigService } from "../api"; 9 | import { UmbDocumentUrlRepository } from "@umbraco-cms/backoffice/document"; 10 | import type { UmbDocumentUrlModel } from "@umbraco-cms/backoffice/document"; 11 | import { generalStyles } from "../Styles/general"; 12 | import AccessibilityReporterAPIService from "../Services/accessibility-reporter-api.service"; 13 | import AccessibilityReporterService from "../Services/accessibility-reporter.service"; 14 | import { UMB_MODAL_MANAGER_CONTEXT } from "@umbraco-cms/backoffice/modal"; 15 | import { ACCESSIBILITY_REPORTER_MODAL_DETAIL } from "../Modals/detail/accessibilityreporter.detail.modal.token"; 16 | import { utils, writeFile } from "xlsx"; 17 | import { UMB_NOTIFICATION_CONTEXT, UmbNotificationContext } from "@umbraco-cms/backoffice/notification"; 18 | import '../Components/ar-score'; 19 | 20 | @customElement('accessibility-reporter-workspaceview') 21 | export class AccessibilityReporterWorkspaceViewElement extends UmbElementMixin(LitElement) { 22 | 23 | @state() 24 | private pageState: PageState; 25 | 26 | @state() 27 | config: AccessibilityReporterAppSettings | undefined; 28 | 29 | @state() 30 | currentUser: UmbCurrentUserModel | undefined; 31 | 32 | @state() 33 | private _urls?: Array; 34 | 35 | @state() 36 | private pageName: string = ""; 37 | 38 | @state() 39 | private testURL: string = ""; 40 | 41 | @state() 42 | private results: any; 43 | 44 | @state() 45 | private score: number; 46 | 47 | @state() 48 | private testTime: string; 49 | 50 | @state() 51 | private testDate: string; 52 | 53 | @state() 54 | private violationsOpen: boolean = true; 55 | 56 | @state() 57 | private incompleteOpen: boolean = true; 58 | 59 | @state() 60 | private passesOpen: boolean = false; 61 | 62 | private _workspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE; 63 | 64 | private _modalManagerContext: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE; 65 | 66 | private _notificationContext?: UmbNotificationContext; 67 | 68 | private _documentUrlRepository = new UmbDocumentUrlRepository(this); 69 | 70 | constructor() { 71 | super(); 72 | this.pageState = PageState.ManuallyRun; 73 | this.init(); 74 | } 75 | 76 | private async init() { 77 | 78 | this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => { 79 | if(!context) { 80 | return; 81 | } 82 | this.observe( 83 | context.currentUser, 84 | (currentUser) => { 85 | this.currentUser = currentUser; 86 | }, 87 | 'currrentUserObserver', 88 | ); 89 | }); 90 | 91 | this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => { 92 | this._workspaceContext = context; 93 | this._observeContent(); 94 | }); 95 | 96 | this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (context) => { 97 | if(!context) { 98 | return; 99 | } 100 | this._modalManagerContext = context; 101 | }); 102 | 103 | this.consumeContext(UMB_NOTIFICATION_CONTEXT, (_instance) => { 104 | this._notificationContext = _instance; 105 | }); 106 | 107 | this.config = await this.getConfig(); 108 | 109 | /* Expose config to child iframe for tests */ 110 | /*@ts-ignore*/ 111 | window.ACCESSIBILITY_REPORTER_CONFIG = this.config; 112 | 113 | if (!this.config) { 114 | this.pageState = PageState.Errored; 115 | return; 116 | } 117 | 118 | if (!this.config?.testBaseUrl) { 119 | this.config.testBaseUrl = this.getFallbackBaseUrl(); 120 | } 121 | 122 | if(this.config.runTestsAutomatically) { 123 | this.runTests(false); 124 | } 125 | 126 | } 127 | 128 | private getLocalHostname() { 129 | return location.hostname + (location.port ? ":" + location.port : ""); 130 | } 131 | 132 | private getFallbackBaseUrl() { 133 | return location.protocol + "//" + this.getHostname(this?._urls); 134 | } 135 | 136 | private getHostname(possibleUrls: any) { 137 | if (!this.config?.apiUrl) { 138 | // so we don't get iframe CORS issues 139 | return this.getLocalHostname(); 140 | } 141 | for (let index = 0; index < possibleUrls.length; index++) { 142 | var possibleCurrentUrl = possibleUrls[index].text; 143 | if (AccessibilityReporterService.isAbsoluteURL(possibleCurrentUrl)) { 144 | return AccessibilityReporterService.getHostnameFromString(possibleCurrentUrl); 145 | } 146 | } 147 | // fallback if hostnames not set assume current host 148 | return this.getLocalHostname(); 149 | } 150 | 151 | private _observeContent() { 152 | if (!this._workspaceContext) return; 153 | 154 | this.pageName = this._workspaceContext.getName() as string; 155 | 156 | this.observe(this._workspaceContext.unique, async (unique) => { 157 | if (unique) { 158 | await this._fetchDocumentUrls(unique); 159 | } 160 | }); 161 | 162 | } 163 | 164 | private async _fetchDocumentUrls(documentUnique: string) { 165 | try { 166 | const { data } = await this._documentUrlRepository.requestItems([documentUnique]); 167 | if (data && data.length > 0) { 168 | this._urls = data[0].urls; 169 | } else { 170 | this._urls = []; 171 | } 172 | } catch (error) { 173 | console.error('Error fetching document URLs:', error); 174 | this._urls = []; 175 | } 176 | } 177 | 178 | private async getConfig(): Promise { 179 | const { data, error } = await tryExecute(this, ConfigService.current()) 180 | if (error) { 181 | console.error(error); 182 | this.pageState = PageState.Errored; 183 | return undefined; 184 | } 185 | 186 | return data; 187 | } 188 | 189 | private async getTestResult(testUrl: string, showTestRunning: boolean = true) { 190 | return this.config?.apiUrl ? AccessibilityReporterAPIService.getIssues(this.config, testUrl, this.currentUser?.languageIsoCode ?? "") : AccessibilityReporterService.runTest(this.shadowRoot, testUrl, showTestRunning); 191 | } 192 | 193 | private async runTests(showTestRunning: boolean): Promise { 194 | 195 | this.pageState = PageState.Loading; 196 | 197 | // Ensure we have document URLs before running tests 198 | if (!this._urls || this._urls.length === 0) { 199 | // Try to fetch URLs if we have a workspace context 200 | if (this._workspaceContext?.getUnique()) { 201 | try { 202 | await this._fetchDocumentUrls(this._workspaceContext.getUnique()!); 203 | } catch (error) { 204 | console.error('Failed to fetch document URLs before testing:', error); 205 | // Continue with fallback URL if fetching fails 206 | } 207 | } 208 | } 209 | 210 | const pathToTest = this._urls?.[0]?.url || "/"; 211 | this.testURL = new URL(pathToTest, this.config?.testBaseUrl).toString(); 212 | 213 | try { 214 | const testResponse = await this.getTestResult(this.testURL, showTestRunning); // TODO: Add types 215 | this.results = this.sortResponse(testResponse); 216 | this.score = AccessibilityReporterService.getPageScore(testResponse); 217 | this.pageState = PageState.Loaded; 218 | this.testTime = format(testResponse.timestamp, "HH:mm:ss"); 219 | this.testDate = format(testResponse.timestamp, "MMMM do yyyy"); 220 | } catch (error) { 221 | this.pageState = PageState.Errored; 222 | console.error(error); 223 | } 224 | 225 | } 226 | 227 | private sortResponse(results: any) { 228 | const sortedViolations = results.violations.sort(AccessibilityReporterService.sortIssuesByImpact); 229 | results.violations = sortedViolations; 230 | const sortedIncomplete = results.incomplete.sort(AccessibilityReporterService.sortIssuesByImpact); 231 | results.incomplete = sortedIncomplete; 232 | return results; 233 | } 234 | 235 | private totalIssues() { 236 | if (!this.results) { 237 | return 0; 238 | } 239 | let total = 0; 240 | for (let index = 0; index < this.results.violations.length; index++) { 241 | total += this.results.violations[index].nodes.length; 242 | } 243 | return total.toString(); 244 | }; 245 | 246 | private totalIncomplete() { 247 | if (!this.results) { 248 | return 0; 249 | } 250 | let total = 0; 251 | for (let index = 0; index < this.results.incomplete.length; index++) { 252 | total += this.results.incomplete[index].nodes.length; 253 | } 254 | return total.toString(); 255 | }; 256 | 257 | private toggleViolations() { 258 | this.violationsOpen = !this.violationsOpen; 259 | }; 260 | 261 | private togglePasses() { 262 | this.passesOpen = !this.passesOpen; 263 | }; 264 | 265 | private toggleIncomplete() { 266 | this.incompleteOpen = !this.incompleteOpen; 267 | }; 268 | 269 | private async openDetail(result: any) { 270 | this._modalManagerContext?.open(this, ACCESSIBILITY_REPORTER_MODAL_DETAIL, { 271 | data: { 272 | result: result 273 | } 274 | }); 275 | }; 276 | 277 | private failedTitle() { 278 | let title = 'Failed Test'; 279 | if (this.results.violations.length !== 1) { 280 | title += 's'; 281 | } 282 | if (this.totalIssues() !== "0") { 283 | title += ` due to ${this.totalIssues()} Violation`; 284 | if (this.totalIssues() !== "1") { 285 | title += 's'; 286 | } 287 | } 288 | return title; 289 | }; 290 | 291 | private incompleteTitle() { 292 | let title = 'Incomplete Test'; 293 | if (this.results.violations.length !== 1) { 294 | title += 's'; 295 | } 296 | if (this.totalIncomplete() !== "0") { 297 | title += ` due to ${this.totalIncomplete()} Violation`; 298 | if (this.totalIncomplete() !== "1") { 299 | title += 's'; 300 | } 301 | } 302 | return title; 303 | }; 304 | 305 | 306 | private formattedResultsForExport(results: any) { 307 | let formattedRows = []; 308 | for (let index = 0; index < results.length; index++) { 309 | const currentResult = results[index]; 310 | formattedRows.push({ 311 | impact: currentResult.impact ? AccessibilityReporterService.upperCaseFirstLetter(currentResult.impact) : '', 312 | title: currentResult.help, 313 | description: currentResult.description, 314 | standard: AccessibilityReporterService.mapTagsToStandard(currentResult.tags).join(', '), 315 | errors: currentResult.nodes.length 316 | }); 317 | } 318 | return formattedRows; 319 | } 320 | 321 | private exportResults() { 322 | 323 | try { 324 | 325 | const failedRows = this.formattedResultsForExport(this.results.violations); 326 | const incompleteRows = this.formattedResultsForExport(this.results.incomplete); 327 | const passedRows = this.formattedResultsForExport(this.results.passes); 328 | 329 | const failedWorksheet = utils.json_to_sheet(failedRows); 330 | const incompleteWorksheet = utils.json_to_sheet(incompleteRows); 331 | const passedWorksheet = utils.json_to_sheet(passedRows); 332 | const workbook = utils.book_new(); 333 | utils.book_append_sheet(workbook, failedWorksheet, "Failed Tests"); 334 | utils.book_append_sheet(workbook, incompleteWorksheet, "Incomplete Tests"); 335 | utils.book_append_sheet(workbook, passedWorksheet, "Passed Tests"); 336 | 337 | const headers = [["Impact", "Title", "Description", "Accessibility Standard", "Violations"]]; 338 | const passedHeaders = [["Impact", "Title", "Description", "Accessibility Standard", "Elements"]]; 339 | utils.sheet_add_aoa(failedWorksheet, headers, { origin: "A1" }); 340 | utils.sheet_add_aoa(incompleteWorksheet, headers, { origin: "A1" }); 341 | utils.sheet_add_aoa(passedWorksheet, passedHeaders, { origin: "A1" }); 342 | 343 | const failedTitleWidth = failedRows.reduce((w, r) => Math.max(w, r.title.length), 40); 344 | const incompleteTitleWidth = incompleteRows.reduce((w, r) => Math.max(w, r.title.length), 40); 345 | const passedTitleWidth = passedRows.reduce((w, r) => Math.max(w, r.title.length), 40); 346 | failedWorksheet["!cols"] = [{ width: 10 }, { width: failedTitleWidth }, { width: 40 }, { width: 25 }, { width: 8 } ]; 347 | incompleteWorksheet["!cols"] = [{ width: 10 }, { width: incompleteTitleWidth }, { width: 40 }, { width: 25 }, { width: 8 } ]; 348 | passedWorksheet["!cols"] = [{ width: 10 }, { width: passedTitleWidth }, { width: 40 }, { width: 25 }, { width: 8 } ]; 349 | 350 | writeFile(workbook, 351 | AccessibilityReporterService.formatFileName(`accessibility-report-${this.pageName}-${format(this.results.timestamp, "yyyy-MM-dd")}`) + ".xlsx", { compression: true }); 352 | 353 | } catch(error) { 354 | console.error(error); 355 | this._notificationContext?.peek('danger', { data: { message: 'An error occurred exporting the report. Please try again later.' } }); 356 | } 357 | 358 | }; 359 | 360 | 361 | render() { 362 | 363 | if (this.pageState === PageState.ManuallyRun) { 364 | return html` 365 | 366 |
367 | 368 | 369 | 370 | 371 | 372 |

Accessibility Reporter

373 |
374 |

Start running accessibility tests on the current published version of ${this.testURL} by using the button below.

375 | Run tests 376 |
377 | `; 378 | } 379 | 380 | if (this.pageState === PageState.Loading) { 381 | return html` 382 | 383 |
384 | 385 | 386 | 387 | 388 | 389 |

Running Accessibility Tests on ${this.pageName} (opens in a new window)

390 |
391 | 392 |
393 |
`; 394 | } 395 | 396 | if (this.pageState === PageState.Errored) { 397 | return html` 398 | 399 |
400 | 401 | 402 | 403 | 404 | 405 |

Accessibility Report for ${this.pageName} (opens in a new window) errored

406 |
407 |

Accessibility Reporter only works for URLs that are accessible publicly.

408 |

If your page is publicly accessible, please try using the "Rerun Tests" button below or refreshing this page to run the accessibility report again.

409 | Rerun tests 410 |
411 | `; 412 | } 413 | 414 | if (this.pageState === PageState.Loaded) { 415 | return html` 416 |
417 | 418 | 419 |
420 | 421 | 422 | 423 | 424 | 425 |

Accessibility Report for ${this.pageName} (opens in a new window)

426 |
427 | 428 |
429 |
430 | 431 |
432 |
433 |
434 | ${this.results.violations.length} 435 | Failed 436 |
437 |
438 |
439 |
440 | ${this.results.incomplete.length} 441 | Incomplete 442 |
443 |
444 |
445 |
446 | ${this.results.passes.length} 447 | Passed 448 |
449 |
450 |
451 |

452 | Rerun tests 453 | Export results 454 | ${this.testTime} on ${this.testDate} 455 |

456 | 457 |
458 | 459 | 460 | 477 | ${this.results.violations.length ? html` 478 |
479 |

All of the following need fixing to improve the accessibility of this page.

480 |
481 | ${this.violationsOpen ? html` 482 | 483 | 484 | Impact 485 | Title 486 | Description 487 | Accessibility Standard 488 | Violations 489 | Action 490 | 491 | ${this.results.violations.map((result: any) => html` 492 | 493 | ${AccessibilityReporterService.upperCaseFirstLetter(result.impact)} 494 | ${result.help} 495 | ${result.description} 496 | 497 | ${AccessibilityReporterService.mapTagsToStandard(result.tags).map((tag: any) => html` 498 | ${tag} 499 | `)} 500 | 501 |
${result.nodes.length}
502 | 503 | 513 | 514 |
515 | `)} 516 |
517 | `: null} 518 |
519 |
520 | `: html`

No tests failed! High 5, you rock!

`} 521 |
522 | 523 | 524 | 541 | ${this.results.incomplete.length ? html` 542 |
543 |

These tests could not be definitively passed or failed. Please manually review these tests.

544 |
545 | ${this.incompleteOpen ? html` 546 | 547 | 548 | Impact 549 | Title 550 | Description 551 | Accessibility Standard 552 | Violations 553 | Action 554 | 555 | ${this.results.incomplete.map((result: any) => html` 556 | 557 | ${AccessibilityReporterService.upperCaseFirstLetter(result.impact)} 558 | ${result.help} 559 | ${result.description} 560 | 561 | ${AccessibilityReporterService.mapTagsToStandard(result.tags).map((tag: any) => html` 562 | ${tag} 563 | `)} 564 | 565 |
${result.nodes.length}
566 | 567 | 577 | 578 |
579 | `)} 580 |
` 581 | : null} 582 |
583 |
584 | `: html`

All automated tests ran successfully.

`} 585 |
586 | 587 | 588 | 604 |

All these tests have passed successfully! High 5, you rock!

605 |
606 | ${this.passesOpen ? html` 607 | 608 | 609 | Impact 610 | Title 611 | Description 612 | Accessibility Standard 613 | Elements 614 | 615 | ${this.results.passes.map((result: any) => html` 616 | 617 | Passed 618 | ${result.help} 619 | ${result.description} 620 | 621 | ${AccessibilityReporterService.mapTagsToStandard(result.tags).map((tag: any) => html` 622 | ${tag} 623 | `)} 624 | 625 | ${result.nodes.length} 626 | 627 | `)} 628 | 629 | `: null} 630 |
631 |
632 | 633 | 634 |
635 |
636 | 637 |
638 |

Manual Tests

639 |
640 |

Automated accessibility tests can only catch up to 37% of accessibility issues. Manual testing is needed to ensure that this page is fully accessible.

641 |

As a minimum it is recommended that the following manual tests are run on ${this.pageName} (opens in a new window) every time that the automated tests are run.

642 |
643 |
644 | 645 |
646 |
647 | 648 |
649 |
650 | 651 |
652 |
653 | 654 |
655 |
656 | 657 |
658 | ${this.results.incomplete.length ? html` 659 |
660 | 661 |
662 | ` : null} 663 |
664 |
665 |
666 | `; 667 | } 668 | } 669 | 670 | 671 | static styles = [ 672 | generalStyles, 673 | css` 674 | :host { 675 | display: block; 676 | padding: 24px; 677 | } 678 | `, 679 | ]; 680 | } 681 | 682 | export default AccessibilityReporterWorkspaceViewElement; 683 | 684 | declare global { 685 | interface HTMLElementTagNameMap { 686 | 'accessibility-reporter-workspaceview': AccessibilityReporterWorkspaceViewElement; 687 | } 688 | } 689 | --------------------------------------------------------------------------------