├── test ├── configs │ ├── empty.js │ ├── syntaxError.js │ ├── viewportWidth.js │ ├── simple.js │ └── skipAndOnly.js ├── runnerSpec │ └── a11y.js ├── windowSpec.js ├── commandLineArgsSpec.js ├── minimumTextSizeStandardSpec.js ├── sectionsSpec.js ├── standardsSpec.js ├── xpathSpec.js ├── a11ySpec.js └── iframeSpec.js ├── cucumber ├── mocha ├── cucumber.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ └── config.yml ├── DISCUSSION_TEMPLATE │ ├── help.yml │ └── feature-request.yml ├── SECURITY.md ├── workflows │ ├── test.yml │ ├── stale.yml │ ├── lock-threads.yml │ └── codeql.yml ├── PULL_REQUEST_TEMPLATE.md └── CONTRIBUTING.md ├── features ├── support │ ├── timeouts.js │ ├── nullReporter.js │ ├── parameter_types.js │ ├── nullWindowAdapter.js │ └── helpers.js ├── README.md ├── cli │ ├── interactive_mode.feature │ ├── specify_url.feature │ ├── missing_config_file_warning.feature │ ├── specify_url_via_config.feature │ ├── exit_status.feature │ ├── specify_path_to_config_file.feature │ ├── coloured_output.feature │ ├── json_reporter.feature │ ├── command_line_help.feature │ ├── skipping_standards.feature │ ├── report_configuration_errors.feature │ ├── display_result_summary.feature │ ├── custom_reporter.feature │ ├── viewport_widths.feature │ └── display_failing_result.feature ├── ignore_x_frame_options.feature ├── standards │ └── mag │ │ ├── audio_and_video │ │ ├── 03_metadata.feature │ │ ├── 05_audio_conflict.feature │ │ ├── 04_volume_control.feature │ │ ├── 02_autoplay.feature │ │ └── 01_alternatives_for_audio_and_visual_content.feature │ │ ├── focus │ │ ├── 03_content_order.feature │ │ ├── 06_alternative_input_methods.feature │ │ ├── 02_keyboard_trap.feature │ │ ├── 05_user_interactions.feature │ │ └── 04_focus_order.feature │ │ ├── scripts_and_dynamic_content │ │ ├── 03_page_refreshes.feature │ │ ├── 04_timeouts.feature │ │ ├── 05_input_control.feature │ │ ├── 02_controlling_media.feature │ │ └── 01_progressive_functionality.feature │ │ ├── design │ │ ├── 05_spacing.feature │ │ ├── 03_styling_and_readability.feature │ │ ├── 12_flicker.feature │ │ ├── 10_choice.feature │ │ ├── 04_touch_target_size.feature │ │ ├── 09_consistency.feature │ │ ├── 07_actionable_elements.feature │ │ ├── 11_adjustability.feature │ │ ├── 01_colour_contrast.feature │ │ └── 02_colour_and_meaning.feature │ │ ├── text_equivalents │ │ ├── 02_decorative_content.feature │ │ └── 04_roles_traits_and_properties.feature │ │ ├── links │ │ ├── 02_links_to_alternative_formats.feature │ │ ├── 03_combining_repeated_links.feature │ │ └── 01_descriptive_links.feature │ │ ├── notifications │ │ ├── 04_feedback_and_assistance.feature │ │ ├── 02_standard_operating_system_notifications.feature │ │ ├── 03_error_messages_and_correction.feature │ │ └── 01_inclusive_notifications.feature │ │ ├── forms │ │ ├── 02_form_inputs.feature │ │ ├── 03_form_layout.feature │ │ └── 04_grouping_form_elements.feature │ │ ├── images │ │ ├── 01_images_of_text.feature │ │ └── 02_background_images.feature │ │ ├── editorial │ │ ├── 01_consistent_labelling.feature │ │ └── 03_instructions.feature │ │ └── structure │ │ ├── 04_grouped_elements.feature │ │ └── 03_containers_and_landmarks.feature ├── disabled_caching.feature ├── setting_up_the_browser_window.feature ├── setting_cookies.feature ├── hiding_warnings.feature └── hiding_errors.feature ├── electron-mocha.config.json ├── .gitignore ├── lib ├── electron │ ├── storage.js │ └── cookies.js ├── standards │ ├── tests │ │ ├── contentOrderMustBeLogical.js │ │ ├── linksMustHaveUnderlinesAndPointers.js │ │ ├── thereMustNotBeAKeyboardTrap.js │ │ ├── interactiveElementsMustBeFocusable.js │ │ ├── textLinksMustHaveMouseOverStateChange.js │ │ ├── consistentLabellingShouldBeUsed.js │ │ ├── imagesOfTextShouldBeAvoided.js │ │ ├── pageTitlesMustBeUniquelyAndClearlyIdentifiable.js │ │ ├── clearErrorMessagesMustBeProvided.js │ │ ├── userExperienceShouldBeConsistent.js │ │ ├── containersShouldBeUsedToDescribePageStructure.js │ │ ├── corePurposeMustBeDefined.js │ │ ├── timedResponsesMustBeAdjustable.js │ │ ├── actionableContentMustBeNavigableInAMeaningfulSequence.js │ │ ├── anchorsMustHaveHrefs.js │ │ ├── htmlMustHaveLangAttribute.js │ │ ├── interactionInputControlShouldBeAdaptable.js │ │ ├── alternativeInputMethodsMustBeSupported.js │ │ ├── imagesMustHaveAltAttributes.js │ │ ├── actionsMustBeTriggeredWhenAppropriate.js │ │ ├── relevantMetadataShouldBeProvidedForAllMedia.js │ │ ├── focusedElementsMustVisiblyChangeState.js │ │ ├── changesToLanguageMustBeIndicated.js │ │ ├── focusedElementsMustBeIdentifiable.js │ │ ├── interfacesMustProvideMultipleWaysToInteractWithContent.js │ │ ├── automaticPageRefreshesMustNotBeUsedWithoutWarning.js │ │ ├── touchTargetsMustBeLargeEnoughToTouchAccurately.js │ │ ├── aDefaultInputFormatMustBeIndicatedAndSupported.js │ │ ├── visualFormattingAloneMustNotBeUsedToConveyMeaning.js │ │ ├── repeatedLinksToTheSameResourceMustBeCombined.js │ │ ├── additionalInstructionsShouldBeProvided.js │ │ ├── contentMustNotFlickerOrFlash.js │ │ ├── decorativeImagesMustBeHiddenFromAssistiveTechnology.js │ │ ├── notificationsMustBeBothVisibleAndAudible.js │ │ ├── tooltipsMustNotRepeatLinkTextOrOtherAlternatives.js │ │ ├── elementsMustHaveAccessibilityPropertiesSetAppropriately.js │ │ ├── inactiveSpaceShouldBeProvidedAroundActionableElements.js │ │ ├── volumeControlsShouldBeProvidedForInteractiveMedia.js │ │ ├── controlsLabelsAndOtherFormElementsMustBeProperlyGrouped.js │ │ ├── labelsMustBeCloseAndLaidOutAppropriately.js │ │ ├── scriptsAndDynamicContentMustBeBuiltInAProgressiveManner.js │ │ ├── allDocumentsMustHaveAW3cRecommendedDoctype.js │ │ ├── preferStandardOperatingSystemNotifications.js │ │ ├── focusOrContextMustNotAutomaticallyChangeDuringUserInput.js │ │ ├── linkAndNavigationTextMustUniquelyDescribeTheTargetOrFunction.js │ │ ├── nonCriticalFeedbackOrAssistanceShouldBeProvidedWhenAppropriate.js │ │ ├── audioMustNotPlayAutomaticallyWithoutControls.js │ │ ├── narrativeAudioShouldNotConflictWithAssistiveTechnology.js │ │ ├── linksToAlternativeFormatsMustIndicateThatAnAlternativeIsOpening.js │ │ ├── mediaThatUpdatesAndAnimationMustHaveAPauseStopOrHideControl.js │ │ ├── interactiveMediaShouldBeAdjustableForUserAbilityAndPreference.js │ │ ├── alternativeDeliveryForEmbeddedMediaMustBeProvided.js │ │ ├── colourCombinationsMustPassColourContrastCheck.js │ │ ├── alternativesMustBrieflyDescribeEditorialIntent.js │ │ ├── meaningfulBackgroundImagesMustHaveAccessibleAlternatives.js │ │ ├── titleAttributesMustNotDuplicateContent.js │ │ ├── groupedInterfaceElementsMustBeRepresentedAsASingleComponent.js │ │ ├── titleAttributesOnlyOnInputs.js │ │ ├── exactlyOneMainHeading.js │ │ ├── editorialLinksMustBeSelfEvident.js │ │ ├── informationConveyedWithColourMustAlsoBeIdentifiableFromContextOrMarkup.js │ │ ├── exactlyOneMainLandmark.js │ │ ├── elementsWithZeroTabIndexMustBeFocusableByDefault.js │ │ ├── formsMustHaveSubmitButtons.js │ │ ├── useTablesForData.js │ │ ├── elementsMustBeVisibleOnFocus.js │ │ ├── textCannotBeTooSmall.js │ │ ├── titleElementMustIdentifyMainContent.js │ │ ├── headingsMustBeInAscendingOrder.js │ │ ├── contentMustBeVisibleAndUsableWithPageZoomed.js │ │ ├── fieldsMustHaveLabelsOrTitles.js │ │ ├── coreContentMustBeAccessibleWhenStylingIsRemoved.js │ │ ├── documentMustNotRequireJavaScriptOrCssToFunction.js │ │ ├── support │ │ │ └── detectTableType.js │ │ ├── contentMustBeVisibleAndUsableWithTextResized.js │ │ ├── markupMustValidateAgainstDoctype.js │ │ ├── textMustBeStyledWithUnitsThatAreResizableInAllBrowsers.js │ │ └── contentMustFollowHeadings.js │ └── index.js ├── a11y.js ├── reporter.js ├── cli │ └── args.js ├── config │ └── loader.js ├── reporters │ └── json.js └── results │ ├── xpath.js │ └── standardResult.js ├── a11y.js ├── guides ├── contributing │ ├── suggesting-improvements.md │ ├── raising-issues-with-standards-checks.md │ └── code-changes.md └── using │ ├── semi-automated-tests.md │ └── checking-a-website.md ├── electron ├── index.html ├── windowAdapter.js ├── mainWindow.css ├── answerFrame.css ├── renderer.js └── bbc-a11y.js ├── docker ├── Dockerfile └── README.md ├── scripts └── generate-coverage ├── xvfb ├── bin └── bbc-a11y.js ├── package.json └── README.md /test/configs/empty.js: -------------------------------------------------------------------------------- 1 | /* empty config */ 2 | -------------------------------------------------------------------------------- /cucumber: -------------------------------------------------------------------------------- 1 | ./node_modules/.bin/cucumber-electron "$@" 2 | -------------------------------------------------------------------------------- /mocha: -------------------------------------------------------------------------------- 1 | node_modules/.bin/electron-mocha --renderer "$@" 2 | -------------------------------------------------------------------------------- /test/configs/syntaxError.js: -------------------------------------------------------------------------------- 1 | /* syntax error... */ 2 | -> e 3 | -------------------------------------------------------------------------------- /test/configs/viewportWidth.js: -------------------------------------------------------------------------------- 1 | /* global page */ 2 | page('https://www.bbc.co.uk', { 3 | width: 789 4 | }) 5 | -------------------------------------------------------------------------------- /cucumber.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: '--tags \'not @keep-ansi-escape-sequences and not @todo\'' 3 | } 4 | -------------------------------------------------------------------------------- /test/configs/simple.js: -------------------------------------------------------------------------------- 1 | /* global page */ 2 | page('https://www.bbc.co.uk') 3 | page('https://www.bbc.co.uk/news') 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # All changes should be reviewed by codeowners 2 | * @bbc/open-dev @bbc/audience-facing-accessibility -------------------------------------------------------------------------------- /features/support/timeouts.js: -------------------------------------------------------------------------------- 1 | const { setDefaultTimeout } = require('@cucumber/cucumber') 2 | 3 | setDefaultTimeout(10000) 4 | -------------------------------------------------------------------------------- /electron-mocha.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "webPreferences": { 3 | "webSecurity": false, 4 | "enableRemoteModule": true 5 | } 6 | } -------------------------------------------------------------------------------- /test/runnerSpec/a11y.js: -------------------------------------------------------------------------------- 1 | /* global page */ 2 | page('https://a11ytests.com/perfect') 3 | page('https://a11ytests.com/perfect') 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | Gemfile.lock 3 | pkg 4 | tags 5 | .DS_Store 6 | /a11y.js 7 | node_modules 8 | lib/bundle.js 9 | .idea 10 | yarn.lock 11 | dist 12 | -------------------------------------------------------------------------------- /test/windowSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert') 3 | 4 | describe('window', () => { 5 | it('should exist', () => { 6 | assert(window) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /features/README.md: -------------------------------------------------------------------------------- 1 | These are the acceptance tests for the a11y tool itself. 2 | 3 | To read the accessibility standards checks that this tool will run against your app, 4 | you need to look in the `standards` directory. 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature Requests & Questions 4 | url: https://github.com/bbc/sqs-consumer/discussions 5 | about: Please ask and answer questions here. -------------------------------------------------------------------------------- /lib/electron/storage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clear: function () { 3 | const electronRemote = require('electron').remote 4 | 5 | const session = electronRemote.session.defaultSession 6 | return session.clearStorageData() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /features/cli/interactive_mode.feature: -------------------------------------------------------------------------------- 1 | Feature: Interactive Mode 2 | 3 | Scenario: Running a11y interactively 4 | Given a website running on a11ytests.com 5 | When I run `bbc-a11y https://a11ytests.com/perfect --interactive` 6 | And the window should remain open 7 | -------------------------------------------------------------------------------- /features/ignore_x_frame_options.feature: -------------------------------------------------------------------------------- 1 | Feature: Ignore x-frame-options header 2 | 3 | Scenario: Page sets x-frame-options=SAMEORIGIN 4 | Given a website running on a11ytests.com 5 | When I run `bbc-a11y https://a11ytests.com/x-frame-options` 6 | Then it passes 7 | -------------------------------------------------------------------------------- /lib/standards/tests/contentOrderMustBeLogical.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Content order must be logical', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with illogical content order', 7 | 8 | manualTest: { 9 | question: 'Is content logically ordered?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /a11y.js: -------------------------------------------------------------------------------- 1 | /* global page */ 2 | page('https://www.bbc.co.uk', { 3 | skip: [ 4 | 'Headings: Content must follow headings', 5 | 'Minimum Text Size: Text cannot be too small' 6 | ] 7 | }) 8 | page('https://www.bbc.co.uk/news', { 9 | skip: 'Headings: Content must follow headings' 10 | }) 11 | -------------------------------------------------------------------------------- /test/configs/skipAndOnly.js: -------------------------------------------------------------------------------- 1 | /* global page */ 2 | page('https://www.bbc.co.uk', { 3 | skip: 'x' 4 | }) 5 | page('https://www.bbc.co.uk/news', { 6 | skip: ['y', 'z'] 7 | }) 8 | page('https://www.bbc.co.uk/sport', { 9 | only: 'a' 10 | }) 11 | page('https://www.bbc.co.uk/weather', { 12 | only: ['b', 'c'] 13 | }) 14 | -------------------------------------------------------------------------------- /guides/contributing/suggesting-improvements.md: -------------------------------------------------------------------------------- 1 | # Suggesting improvements to bbc-a11y 2 | 3 | If you believe there is a bug in bbc-a11y, please [submit an issue](https://github.com/bbc/bbc-a11y/issues/new) 4 | with as much relevant detail as possible. There is a template on the issue page 5 | that should help you describe the problem. 6 | -------------------------------------------------------------------------------- /features/cli/specify_url.feature: -------------------------------------------------------------------------------- 1 | Feature: Specify URL 2 | 3 | Scenario: No config, just pass page URL on command-line 4 | Given a website running on a11ytests.com 5 | When I run `bbc-a11y https://a11ytests.com/perfect` 6 | Then it should pass with: 7 | """ 8 | ✓ https://a11ytests.com/perfect 9 | """ 10 | -------------------------------------------------------------------------------- /lib/standards/tests/linksMustHaveUnderlinesAndPointers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Links must have underlines and pointers', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that links with no underlines or pointers', 7 | 8 | manualTest: { 9 | question: 'Do all links have underlines and pointers?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /features/support/nullReporter.js: -------------------------------------------------------------------------------- 1 | module.exports = class NullReporter { 2 | runStarted () { 3 | this.results = {} 4 | } 5 | 6 | unexpectedError (e) { 7 | console.log(e) 8 | throw e 9 | } 10 | 11 | pageChecked (page, results) { 12 | this.results[page.url] = results 13 | } 14 | 15 | runEnded () { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/standards/tests/thereMustNotBeAKeyboardTrap.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'There must not be a keyboard trap', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that has a keyboard trap', 7 | 8 | manualTest: { 9 | question: 'Is anything a keyboard trap?', 10 | passText: 'No', 11 | failText: 'Yes' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/standards/tests/interactiveElementsMustBeFocusable.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Interactive elements must be focusable', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with interactive elements that are not focusable', 7 | 8 | manualTest: { 9 | question: 'Are all (and only) interactive elements focusable?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/textLinksMustHaveMouseOverStateChange.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Text links must have mouse over state change', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'text link that has no mouse over state change', 7 | 8 | manualTest: { 9 | question: 'Do all text links have a mouse over state change?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/consistentLabellingShouldBeUsed.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Consistent labelling should be used', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with inconsistent labelling', 7 | 8 | manualTest: { 9 | question: 'Are consistent editorial labels used?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/imagesOfTextShouldBeAvoided.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Images of text should be avoided', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with unnecessary images of text', 7 | 8 | manualTest: { 9 | question: 'Are there any unnecessary images of text?', 10 | passText: 'No', 11 | failText: 'Yes' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/standards/tests/pageTitlesMustBeUniquelyAndClearlyIdentifiable.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Page titles must be uniquely and clearly identifiable', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page title that is not uniquely and clearly identifiable', 7 | 8 | manualTest: { 9 | question: 'Is the page title uniquely and clearly identifiable?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /features/cli/missing_config_file_warning.feature: -------------------------------------------------------------------------------- 1 | Feature: Missing configuration file warning 2 | 3 | Scenario: Running the tool with no arguments and no config file 4 | When I run `bbc-a11y` 5 | Then it should fail with: 6 | """ 7 | Missing configuration file (a11y.js). 8 | 9 | Please visit http://github.com/bbc/bbc-a11y for more information. 10 | """ 11 | -------------------------------------------------------------------------------- /lib/standards/tests/clearErrorMessagesMustBeProvided.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Clear error messages must be provided', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that provides unclear error messages', 7 | 8 | manualTest: { 9 | question: 'Are clear accessible error messages provided when needed?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/userExperienceShouldBeConsistent.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'User experience should be consistent', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that provides inconsitent user experience', 7 | 8 | manualTest: { 9 | question: 'Is the user experience consistent across pages and screens, eg. layout, labels, focus states etc.?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/containersShouldBeUsedToDescribePageStructure.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Containers should be used to describe page structure', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that does not use containers to describe page structure', 7 | 8 | manualTest: { 9 | question: 'Are suitable containers used to help describe page structure?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/corePurposeMustBeDefined.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Core purpose must be defined', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page whose core purpose (to inform, educate, or entertain) is not clearly defined', 7 | 8 | manualTest: { 9 | question: 'Is the core purpose (to inform, educate, or entertain) of the page clearly defined?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/timedResponsesMustBeAdjustable.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Timed responses must be adjustable', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with timed responses that are not adjustable', 7 | 8 | manualTest: { 9 | question: 'Are there any timed responses that cannot be adjusted?', 10 | passText: 'No', 11 | failText: 'Yes' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /electron/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BBC a11y 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /lib/standards/tests/actionableContentMustBeNavigableInAMeaningfulSequence.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Actionable content must be navigable in a meaningful sequence', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that is not navigable in a meaningful sequence', 7 | 8 | manualTest: { 9 | question: 'Is actionable content navigable in a meaningful sequence?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/anchorsMustHaveHrefs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Anchors must have hrefs', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: 'visible element with no href attribute', 7 | 8 | test: function ({ $, fail }) { 9 | $('a:not([href]):visible').each(function (index, anchor) { 10 | fail('Anchor has no href attribute:', anchor) 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/standards/tests/htmlMustHaveLangAttribute.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Html must have lang attribute', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: ' element with no lang attribute', 7 | 8 | test: function ({ $, fail }) { 9 | $('html:not([lang])').each(function (index, html) { 10 | fail('html tag has no lang attribute:', html) 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/standards/tests/interactionInputControlShouldBeAdaptable.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Interaction input control should be adaptable', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page whose interaction input control is not adaptable', 7 | 8 | manualTest: { 9 | question: 'Is interaction input control adaptable?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/alternativeInputMethodsMustBeSupported.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Alternative input methods must be supported', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with navigable content that does not support alternative input methods', 7 | 8 | manualTest: { 9 | question: 'Are alternative input methods, such as keyboard or voice, supported?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/imagesMustHaveAltAttributes.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Images must have alt attributes', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: 'visible element that has no alt attribute', 7 | 8 | test: function ({ $, fail }) { 9 | $('img:not([alt])').each(function (index, img) { 10 | fail('Image has no alt attribute:', img) 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/help.yml: -------------------------------------------------------------------------------- 1 | body: 2 | - type: textarea 3 | attributes: 4 | label: Summary 5 | description: What do you need help with? 6 | validations: 7 | required: true 8 | - type: input 9 | attributes: 10 | label: Example 11 | description: A link to a minimal reproduction is helpful for debugging! 12 | validations: 13 | required: false 14 | -------------------------------------------------------------------------------- /lib/standards/tests/actionsMustBeTriggeredWhenAppropriate.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Actions must be triggered when appropriate', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page without actions triggered when appropriate for the type of user interaction', 7 | 8 | manualTest: { 9 | question: 'Are actions triggered when appropriate for the type of user interaction?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/relevantMetadataShouldBeProvidedForAllMedia.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Relevant metadata should be provided for all media', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with media that is missing relevant metadata', 7 | 8 | manualTest: { 9 | question: 'Is relevant metadata provided for all media?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/focusedElementsMustVisiblyChangeState.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Focused elements must visibly change state', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with actionable and focusable elements that do not have a visible state change', 7 | 8 | manualTest: { 9 | question: 'Do all actionable and focusable elements visibly change state when focused?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/changesToLanguageMustBeIndicated.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Changes to language must be indicated', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that changes language with no indication', 7 | 8 | manualTest: { 9 | question: 'Are any changes to the defined language of the page indicated programmatically?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /features/cli/specify_url_via_config.feature: -------------------------------------------------------------------------------- 1 | Feature: Specify URL via config 2 | 3 | Scenario: Specify a single page 4 | Given a website running on a11ytests.com 5 | And a file named "a11y.js" with: 6 | """ 7 | page('https://a11ytests.com/perfect') 8 | """ 9 | When I run `bbc-a11y` 10 | Then it should pass with: 11 | """ 12 | ✓ https://a11ytests.com/perfect 13 | """ 14 | -------------------------------------------------------------------------------- /lib/standards/tests/focusedElementsMustBeIdentifiable.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Focused elements must be identifiable', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'focusable element that does not have a clearly identifiable visual style when it has focus', 7 | 8 | manualTest: { 9 | question: 'Do all focusable elements have a clearly identifiable visual style when they have focus?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/interfacesMustProvideMultipleWaysToInteractWithContent.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Interfaces must provide multiple ways to interact with content', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with interface elements that only provide one way to interact', 7 | 8 | manualTest: { 9 | question: 'Does the interface provide multiple ways to interact with the content?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/automaticPageRefreshesMustNotBeUsedWithoutWarning.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Automatic page refreshes must not be used without warning', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that refreshes automatically without warning', 7 | 8 | manualTest: { 9 | question: 'Does the page refresh automatically without warning?', 10 | passText: 'No', 11 | failText: 'Yes' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/standards/tests/touchTargetsMustBeLargeEnoughToTouchAccurately.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Touch targets must be large enough to touch accurately', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that includes touch targets that are too small to touch accurately', 7 | 8 | manualTest: { 9 | question: 'Are all touch targets large enough for accurate interaction (larger than 7mm x 7mm)?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/aDefaultInputFormatMustBeIndicatedAndSupported.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'A default input format must be indicated and supported', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page where a default input format is not indicated or supported', 7 | 8 | manualTest: { 9 | question: 'Is a default input format indicated/implied and supported?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/visualFormattingAloneMustNotBeUsedToConveyMeaning.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Visual formatting alone must not be used to convey meaning', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page where visual formatting alone is used to convey meaning', 7 | 8 | manualTest: { 9 | question: 'Is visual formatting alone used to convey meaning?', 10 | passText: 'No', 11 | failText: 'Yes' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /electron/windowAdapter.js: -------------------------------------------------------------------------------- 1 | const win = require('electron').remote.getCurrentWindow() 2 | 3 | module.exports = class ElectronWindowAdapter { 4 | getContentSize () { 5 | const [width, height] = win.getContentSize() 6 | return { width, height } 7 | } 8 | 9 | setContentSize (width, height) { 10 | win.setContentSize(width, height, false) 11 | } 12 | 13 | measureInnerWidth () { 14 | return window.innerWidth 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/standards/tests/repeatedLinksToTheSameResourceMustBeCombined.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Repeated links to the same resource must be combined', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with repeated links to the same resource', 7 | 8 | manualTest: { 9 | question: 'Are neighbouring elements linking to the same resource combined within a single link?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/additionalInstructionsShouldBeProvided.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Additional instructions should be provided', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that is missing a w3c recommended doctype (e.g. )', 7 | 8 | manualTest: { 9 | question: 'Are suitable additional instructions provided to supplement visual and audio cues?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/contentMustNotFlickerOrFlash.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Content must not flicker or flash', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with flickering or flashing content', 7 | 8 | manualTest: { 9 | question: 'Does any content visibly or intentionally flicker or flash more than three times in any one-second period?', 10 | passText: 'No (or not applicable)', 11 | failText: 'Yes' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/standards/tests/decorativeImagesMustBeHiddenFromAssistiveTechnology.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Decorative images must be hidden from assistive technology', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page where decorative images are not hidden from assistive technology', 7 | 8 | manualTest: { 9 | question: 'Are decorative images hidden from assistive technology?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/notificationsMustBeBothVisibleAndAudible.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Notifications must be both visible and audible', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with notifications that are only visible or audible, but not both', 7 | 8 | manualTest: { 9 | question: 'Are notifications both visible and audible (via assistive technology if needed)?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /features/support/parameter_types.js: -------------------------------------------------------------------------------- 1 | const { defineParameterType } = require('@cucumber/cucumber') 2 | 3 | defineParameterType({ 4 | name: 'url', 5 | regexp: /http\S+/, 6 | transformer: str => str 7 | }) 8 | 9 | defineParameterType({ 10 | name: 'reporter', 11 | regexp: /(json|\S+\.js)/, 12 | transformer: str => str 13 | }) 14 | 15 | defineParameterType({ 16 | name: 'configPath', 17 | regexp: /\S+\.js/, 18 | transformer: str => str 19 | }) 20 | -------------------------------------------------------------------------------- /lib/standards/tests/tooltipsMustNotRepeatLinkTextOrOtherAlternatives.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Tooltips must not repeat link text or other alternatives', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page where tooltips repeat link text or other alternatives', 7 | 8 | manualTest: { 9 | question: 'Do tooltips repeat link text or other alternatives?', 10 | passText: 'No (or not applicable)', 11 | failText: 'Yes' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/standards/tests/elementsMustHaveAccessibilityPropertiesSetAppropriately.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Elements must have accessibility properties set appropriately', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page where elements do not have accessibility properties set appropriately', 7 | 8 | manualTest: { 9 | question: 'Do elements have accessibility properties set appropriately?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/inactiveSpaceShouldBeProvidedAroundActionableElements.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'An inactive space should be provided around actionable elements', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that includes actionable elements with no inactive space around them', 7 | 8 | manualTest: { 9 | question: 'Is an inactive space provided around every actionable element?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /features/cli/exit_status.feature: -------------------------------------------------------------------------------- 1 | Feature: Exit status 2 | 3 | For CI, we need to make sure the process exits with a non-zero 4 | status when there's been a failure. 5 | 6 | Scenario: Passing tests 7 | When all the tests pass 8 | Then the exit status should be 0 9 | 10 | Scenario: Failing test 11 | Given a website running on a11ytests.com 12 | When I run `bbc-a11y https://a11ytests.com/missing_main_heading` 13 | Then the exit status should be 1 14 | -------------------------------------------------------------------------------- /features/cli/specify_path_to_config_file.feature: -------------------------------------------------------------------------------- 1 | Feature: Specify path to config file 2 | 3 | Scenario: Config file in alternative location 4 | Given a website running on a11ytests.com 5 | And a file named "path/to/a11y.js" with: 6 | """ 7 | page('https://a11ytests.com/perfect') 8 | """ 9 | When I run `bbc-a11y --config path/to/a11y.js` 10 | Then it should pass with: 11 | """ 12 | ✓ https://a11ytests.com/perfect 13 | """ 14 | -------------------------------------------------------------------------------- /lib/a11y.js: -------------------------------------------------------------------------------- 1 | const jquery = require('jquery') 2 | const standards = require('./standards') 3 | 4 | window.a11y = { 5 | test: function (pageConfiguration, finder, ask) { 6 | return standards 7 | .matching(pageConfiguration) 8 | .test( 9 | finder || jquery, 10 | pageConfiguration || {}, 11 | ask || function () { 12 | return Promise.resolve() 13 | } 14 | ) 15 | } 16 | } 17 | 18 | module.exports = window.a11y 19 | -------------------------------------------------------------------------------- /lib/standards/tests/volumeControlsShouldBeProvidedForInteractiveMedia.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Volume controls should be provided for interactive media', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that provides no volume controls for interactive media', 7 | 8 | manualTest: { 9 | question: 'Are suitable volume controls provided for different audio layers in all interactive media?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /features/support/nullWindowAdapter.js: -------------------------------------------------------------------------------- 1 | module.exports = class NullWindowAdapter { 2 | constructor () { 3 | this.width = 640 4 | this.height = 480 5 | } 6 | 7 | getContentSize () { 8 | return { 9 | width: this.width, 10 | height: this.height 11 | } 12 | } 13 | 14 | setContentSize (width, height) { 15 | this.width = width 16 | this.height = height 17 | } 18 | 19 | measureInnerWidth () { 20 | return this.width 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/standards/tests/controlsLabelsAndOtherFormElementsMustBeProperlyGrouped.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Controls, labels, and other form elements must be properly grouped', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with controls, labels, or other form elements that are improperly grouped', 7 | 8 | manualTest: { 9 | question: 'Are controls, labels, and other form elements properly grouped?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/labelsMustBeCloseAndLaidOutAppropriately.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Labels must be close and laid out appropriately', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with labels that are not close to the relevant form control, or laid out inappropriately', 7 | 8 | manualTest: { 9 | question: 'Are labels placed close to the relevant form control, and laid out appropriately?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/scriptsAndDynamicContentMustBeBuiltInAProgressiveManner.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Scripts and dynamic content must be built in a progressive manner', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with scripts and dynamic content that is not built in a progressive manner', 7 | 8 | manualTest: { 9 | question: 'Are scripts and dynamic content added in a progressive manner?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/allDocumentsMustHaveAW3cRecommendedDoctype.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'All documents must have a W3C recommended doctype', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: 'page that is missing a w3c recommended doctype (e.g. )', 7 | 8 | test: function ({ $, fail }) { 9 | if (!document.doctype || ($('html')[0] && !$('html')[0].ownerDocument.doctype)) { 10 | fail('Missing w3c recommended doctype') 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/standards/tests/preferStandardOperatingSystemNotifications.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Prefer standard operating system notifications', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with non-standard notifications used where standard operating system notifications are appropriate', 7 | 8 | manualTest: { 9 | question: 'Are standard operating system notifications used where available and appropriate?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/focusOrContextMustNotAutomaticallyChangeDuringUserInput.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Focus or context must not automatically change during user input', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with focus or context that changes automatically during user input', 7 | 8 | manualTest: { 9 | question: 'Does focus or context automatically change during user input?', 10 | passText: 'No (or not applicable)', 11 | failText: 'Yes' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /electron/mainWindow.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | body { 5 | height: 100vh; 6 | display: flex; 7 | flex-flow: column; 8 | margin: 0; 9 | padding: 0; 10 | background-color: #eee 11 | } 12 | iframe { 13 | display: flex; 14 | margin: 0; 15 | border: none; 16 | background-color: #fff; 17 | } 18 | #mainFrame { 19 | flex: 1; 20 | } 21 | #answerFrame { 22 | background-color: #84B0E9; 23 | border-top: 3px solid #ccc; 24 | display: none; 25 | height: 180px; 26 | } 27 | -------------------------------------------------------------------------------- /lib/standards/tests/linkAndNavigationTextMustUniquelyDescribeTheTargetOrFunction.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Link and navigation text must uniquely describe the target or function', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with links or navigation text that does not uniquely describe the target or function of the link or item', 7 | 8 | manualTest: { 9 | question: 'Does each link and navigation text uniquely describe the target or function of the link or item?' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/standards/tests/nonCriticalFeedbackOrAssistanceShouldBeProvidedWhenAppropriate.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Non-critical feedback or assistance should be provided when appropriate', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with appropriate non-critical feedback or assistance not provided', 7 | 8 | manualTest: { 9 | question: 'Is non-critical feedback or assistance provided in an accessible way when appropriate?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/audioMustNotPlayAutomaticallyWithoutControls.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Audio must not play automatically without controls', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'audio that plays back without the user being aware or pause/stop/mute button provided', 7 | 8 | manualTest: { 9 | question: 'Does any audio that plays automatically make the user aware this will happen, or provide a pause/stop/mute button?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/narrativeAudioShouldNotConflictWithAssistiveTechnology.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Narrative audio should not conflict with assistive technology', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with narrative audio that conflicts with assistive technology', 7 | 8 | manualTest: { 9 | question: 'Does narrative audio in any interactive media conflict with native assistive technology?', 10 | passText: 'No (or not applicable)', 11 | failText: 'Yes' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | body: 2 | - type: textarea 3 | attributes: 4 | label: Background 5 | description: Why do you think this feature is needed? Are there current alternatives? 6 | validations: 7 | required: true 8 | - type: textarea 9 | attributes: 10 | label: Objectives 11 | description: What should this feature request aim to address? 12 | value: | 13 | 1. 14 | 2. 15 | 3. 16 | validations: 17 | required: true -------------------------------------------------------------------------------- /lib/standards/tests/linksToAlternativeFormatsMustIndicateThatAnAlternativeIsOpening.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Links to alternative formats must indicate that an alternative is opening', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with links to alternative formats that do not indicate that an alternative is opening', 7 | 8 | manualTest: { 9 | question: 'Do all links to alternative formats indicate that an alternative is opening?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/mediaThatUpdatesAndAnimationMustHaveAPauseStopOrHideControl.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Media that updates and animation must have a pause, stop or hide control', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with media that updates or animation that has no pause, stop or hide control', 7 | 8 | manualTest: { 9 | question: 'Does all media that updates or is animated content have a pause, stop or hide control?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/interactiveMediaShouldBeAdjustableForUserAbilityAndPreference.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Interactive media should be adjustable for user ability and preference', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with interactive media that is not adjustable for user ability and preference', 7 | 8 | manualTest: { 9 | question: 'Can all interactive media be suitably adjusted for different user abilities and preferences?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/alternativeDeliveryForEmbeddedMediaMustBeProvided.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Alternative delivery for embedded media must be provided', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page that includes embedded media with no alternative delivery', 7 | 8 | manualTest: { 9 | question: 'Is alternative delivery (such as subtitles, sign language, audio ' + 10 | 'description or transcripts) provided with all embedded media?', 11 | passText: 'Yes (or not applicable)' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/standards/tests/colourCombinationsMustPassColourContrastCheck.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Colour combinations must pass the WCAG colour contrast check', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page whose colour combinations fail the WCAG colour contrast check', 7 | 8 | manualTest: { 9 | question: 'Do all text and background colour combinations pass a reliable ' + 10 | 'colour contrast check?' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/alternativesMustBrieflyDescribeEditorialIntent.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Alternatives must briefly describe editorial intent', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page where alternatives do not briefly describe the editorial intent or purpose of images, objects, or elements', 7 | 8 | manualTest: { 9 | question: 'Do text alternatives briefly describe the editorial intent or purpose of each content image, object, or element? ', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /guides/contributing/raising-issues-with-standards-checks.md: -------------------------------------------------------------------------------- 1 | # Raising issues with standards checks 2 | 3 | If you believe there is a bug or misinterpretation of the BBC Accessibility 4 | Guidelines, please [submit an issue](https://github.com/bbc/bbc-a11y/issues/new) 5 | with as much relevant detail as possible. 6 | 7 | Your issue should include an HTML snippet that demonstrates the problem. Ideally 8 | you can suggest a new scenario to add to one of our cucumber 9 | [features covering the standards checks](../../features/check_standards). 10 | -------------------------------------------------------------------------------- /lib/standards/tests/meaningfulBackgroundImagesMustHaveAccessibleAlternatives.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Meaningful background images must have accessible alternatives', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page without accessible alternatives provided for background images that convey information or meaning', 7 | 8 | manualTest: { 9 | question: 'Are accessible alternatives provided for element background images that convey information or meaning? ', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/titleAttributesMustNotDuplicateContent.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Title attributes must not duplicate content', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: 'element that has the same text content as the page ', 7 | 8 | test: function ({ $, fail }) { 9 | $('[title]:visible').each(function (index, element) { 10 | if ($(element).text().trim() === $(element).attr('title').trim()) { 11 | fail('Title attribute duplicates content:', element) 12 | } 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/standards/tests/groupedInterfaceElementsMustBeRepresentedAsASingleComponent.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Grouped interface elements must be represented as a single component', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page with controls, objects and grouped interface elements not represented as a single accessible component', 7 | 8 | manualTest: { 9 | question: 'Are controls, objects and grouped interface elements represented as a single accessible component?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/standards/tests/titleAttributesOnlyOnInputs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Title attributes only on inputs', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: 'element that is not an input (input, button, textarea, select) ' + 7 | 'or iframe, but does have a title attribute', 8 | 9 | test: function ({ $, fail }) { 10 | $('[title]:not(input, button, textarea, select, iframe):visible').each(function (index, element) { 11 | fail('Non-input element has title attribute:', element) 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/reporter.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | createReporter ({ name, console, remoteConsole }) { 5 | let Reporter 6 | switch (name) { 7 | case 'json': 8 | Reporter = require(`./reporters/${name}`) 9 | break 10 | case 'pretty': 11 | case undefined: 12 | Reporter = require('./reporters/pretty') 13 | break 14 | default: 15 | Reporter = require(path.join(process.cwd(), name)) 16 | } 17 | 18 | return new Reporter(console, remoteConsole) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/standards/tests/exactlyOneMainHeading.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Exactly one main heading', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: 'visible <h1> element except the very first one', 7 | 8 | topFrameOnly: true, 9 | 10 | test: function ({ $, fail }) { 11 | const mainHeadings = $('h1:visible') 12 | const count = mainHeadings.length 13 | if (count === 0) { 14 | fail('Found 0 h1 elements.') 15 | } else if (count > 1) { 16 | fail('Found ' + count + ' h1 elements:', mainHeadings) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Our full security policy and vulnerability reporting procedure is documented on [this external website](https://www.bbc.com/backstage/security-disclosure-policy/#reportingavulnerability). 6 | 7 | Please note that this is a general BBC process. Communication will not be direct with the team responsible for this repo. 8 | 9 | If you would like to, you can also open an issue in this repo regarding your disclosure, but please never share any details of the vulnerability in the GitHub issue. 10 | -------------------------------------------------------------------------------- /lib/standards/tests/editorialLinksMustBeSelfEvident.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Editorial links must be self-evident', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: '<a> that is part of general editorial content, that is not' + 7 | ' identifiable by its visual style, and distinguishable from the surrounding content', 8 | 9 | manualTest: { 10 | question: 'Are all links, buttons and other actionable elements in the content self-evident, ' + 11 | 'identifiable by their visual style, and distinguishable from surrounding content?' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/standards/tests/informationConveyedWithColourMustAlsoBeIdentifiableFromContextOrMarkup.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Information conveyed with colour must also be identifiable from context or markup', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page whose information conveyed with colour is not also identifiable from context or markup', 7 | 8 | manualTest: { 9 | question: 'Is all information conveyed by colour also conveyed by other means, such as additional style, context or markup?', 10 | passText: 'Yes (or not applicable)' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine3.18 2 | 3 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ 4 | sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' && \ 5 | apt-get update -y && \ 6 | apt-get install -y libgconf-2-4 google-chrome-stable xvfb 7 | 8 | RUN yarn add bbc-a11y@2.4.2 9 | 10 | RUN echo '#!/bin/sh\nxvfb-run node_modules/.bin/bbc-a11y "$@"' > /usr/local/bin/bbc-a11y 11 | RUN chmod +x /usr/local/bin/bbc-a11y 12 | 13 | ENTRYPOINT ["bbc-a11y"] 14 | -------------------------------------------------------------------------------- /scripts/generate-coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | IFS=$'\n\t' 4 | 5 | echo '# BBC A11y Test Coverage' > ./guides/coverage.md 6 | echo '' >> ./guides/coverage.md 7 | echo 'bbc-a11y tests URLs against the' >> ./guides/coverage.md 8 | echo '[BBC Mobile Accessibility Guidelines](https://www.bbc.co.uk/guidelines/futuremedia/accessibility/mobile). This is' >> ./guides/coverage.md 9 | echo 'a summary of those guidelines and the level of test coverage currently provided.' >> ./guides/coverage.md 10 | echo '' >> ./guides/coverage.md 11 | 12 | ./bin/bbc-a11y.js --coverage table >> ./guides/coverage.md 13 | -------------------------------------------------------------------------------- /lib/standards/tests/exactlyOneMainLandmark.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Exactly one main landmark', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: 'page with no landmark element (any element with role="main")', 7 | 8 | topFrameOnly: true, 9 | 10 | test: function ({ $, fail }) { 11 | const mainLandmarks = $("[role='main']") 12 | const count = mainLandmarks.length 13 | if (count === 0) { 14 | fail('Found 0 elements with role="main".') 15 | } else if (count > 1) { 16 | fail('Found ' + count + ' elements with role="main":', mainLandmarks) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /features/cli/coloured_output.feature: -------------------------------------------------------------------------------- 1 | Feature: Coloured Output 2 | To make console output easy to interpret 3 | Only terminals that support coloured output should show colours 4 | 5 | @keep-ansi-escape-sequences 6 | Scenario: TTY terminals show coloured output 7 | Given I am using a TTY terminal 8 | When I run a11y against a failing page 9 | Then I see red in the output 10 | 11 | @keep-ansi-escape-sequences 12 | Scenario: Non-TTY terminals show monochrome output 13 | Given I am using a Non-TTY terminal 14 | When I run a11y against a failing page 15 | Then I see monochrome output 16 | -------------------------------------------------------------------------------- /lib/standards/tests/elementsWithZeroTabIndexMustBeFocusableByDefault.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Zero tab index must only be set on elements which are focusable by default', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: 'element that is not focusable by default (input, button, select, textarea, a)' + 7 | ' and has a tabindex attribute set to 0', 8 | 9 | test: function ({ $, fail }) { 10 | const baddies = $("*[tabindex='0']:visible:not(input, button, select, textarea, a)") 11 | baddies.each(function (index, el) { 12 | fail('Non-focusable element with tabindex=0:', el) 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/standards/tests/formsMustHaveSubmitButtons.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Forms must have submit buttons', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: 'visible <form> element that has no submit button (can be any of' + 7 | ' input[type=submit], button[type=submit], input[type=image])', 8 | 9 | test: function ({ $, fail }) { 10 | $('form:visible').each(function (index, form) { 11 | const submits = $(form).find('input[type=submit], button[type=submit], input[type=image]') 12 | if (submits.length === 0) { 13 | fail('Form has no submit button:', form) 14 | } 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/standards/tests/useTablesForData.js: -------------------------------------------------------------------------------- 1 | const detectTableType = require('./support/detectTableType') 2 | 3 | module.exports = { 4 | name: 'Use tables for data', 5 | 6 | type: 'automated', 7 | 8 | failsForEach: '<table> that is used to apply layout, rather than to display data', 9 | 10 | test: function ({ $, fail }) { 11 | $('table').each(function () { 12 | if (detectTableType(this) === 'layout') { 13 | fail('Table used for layout:', this) 14 | } 15 | }) 16 | $(':not(table)').each(function () { 17 | if ($(this).css('display') === 'table') { 18 | fail('Table used for layout:', this) 19 | } 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /xvfb: -------------------------------------------------------------------------------- 1 | XVFB=/usr/bin/Xvfb 2 | XVFBARGS="+extension RANDR :99 -ac -screen 0 1024x768x24" 3 | PIDFILE=/tmp/cucumber_xvfb_99.pid 4 | case "$1" in 5 | start) 6 | echo -n "Starting virtual X frame buffer: Xvfb" 7 | /sbin/start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile --background --exec $XVFB -- $XVFBARGS 8 | echo "." 9 | ;; 10 | stop) 11 | echo -n "Stopping virtual X frame buffer: Xvfb" 12 | /sbin/start-stop-daemon --stop --quiet --pidfile $PIDFILE 13 | rm -f $PIDFILE 14 | echo "." 15 | ;; 16 | restart) 17 | $0 stop 18 | $0 start 19 | ;; 20 | *) 21 | echo "Usage: /etc/init.d/xvfb {start|stop|restart}" 22 | exit 1 23 | esac 24 | exit 0 25 | -------------------------------------------------------------------------------- /lib/standards/tests/elementsMustBeVisibleOnFocus.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Elements must be visible on focus', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: '<a> element that is hidden after receiving focus', 7 | 8 | test: function ({ $, fail }) { 9 | $('a:visible').each(function (index, element) { 10 | const $element = $(element) 11 | $element.focus() 12 | const offset = $element.offset() 13 | const visible = offset.top + $element.height() > 0 && 14 | offset.left + $element.width() > 0 15 | if (!visible) { 16 | fail('Element is invisible on focus:', element) 17 | } 18 | }) 19 | $(document).scrollTop(0) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /features/cli/json_reporter.feature: -------------------------------------------------------------------------------- 1 | Feature: JSON Reporter 2 | Scenario: Reporting results in generic JSON format 3 | Given a website running on a11ytests.com 4 | And a file named "a11y.js" with: 5 | """ 6 | page("https://a11ytests.com/perfect", { 7 | skip: ["Structure: Containers and landmarks: Exactly one main landmark"] 8 | }) 9 | page("https://a11ytests.com/missing_main_heading") 10 | """ 11 | When I run `bbc-a11y --reporter json` 12 | Then it should fail with: 13 | """ 14 | { 15 | "pagesChecked": 2, 16 | "errorsFound": 1, 17 | "errorsHidden": 0, 18 | "standardsSkipped": 1, 19 | "pages": [ 20 | { 21 | "url": "https://a11ytests.com/perfect", 22 | "result": { 23 | """ 24 | -------------------------------------------------------------------------------- /features/cli/command_line_help.feature: -------------------------------------------------------------------------------- 1 | Feature: Command Line Help 2 | 3 | Scenario: Asking the command line tool for help 4 | When I run `bbc-a11y --help` 5 | Then it should pass with: 6 | """ 7 | Usage: bbc-a11y [options] <urls> 8 | 9 | Options: 10 | -V, --version output the version number 11 | -i, --interactive show browser window and leave open for debugging 12 | -m, --manual include manual tests 13 | -w, --width <pixels> override viewport width 14 | -c, --config <config> path to config file 15 | -r, --reporter <reporter> json or [path to custom reporter module] 16 | --coverage <list|table> lists all tests with a description of each test 17 | -h, --help display help for command 18 | """ -------------------------------------------------------------------------------- /electron/answerFrame.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: sans-serif; 3 | box-sizing: border-box; 4 | } 5 | body { 6 | color: #222; 7 | margin: 0; 8 | padding: 10px; 9 | } 10 | h1, textarea, button { 11 | margin: 5px; 12 | } 13 | h1 { 14 | font-size: 1.2em 15 | } 16 | textarea { 17 | width: 100%; 18 | height: 48px; 19 | font-size: 1em; 20 | padding: 6px; 21 | box-sizing: border-box; 22 | margin: 0; 23 | border: 3px solid #ccc; 24 | margin-top: 5px; 25 | } 26 | button { 27 | font-size: 1.4em; 28 | border-width: 3px; 29 | min-width: 200px; 30 | border-radius: 10px; 31 | padding: 5px 12px; 32 | height: 42px; 33 | } 34 | .pass-button { 35 | position: absolute; 36 | bottom: 5px; 37 | left: 5px; 38 | background-color: #77dd77; 39 | } 40 | .fail-button { 41 | position: absolute; 42 | bottom: 5px; 43 | right: 5px; 44 | background-color: #ff6961; 45 | } 46 | -------------------------------------------------------------------------------- /bin/bbc-a11y.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const childProcess = require('child_process') 4 | const path = require('path') 5 | 6 | const electron = require('electron') 7 | 8 | const args = [path.join(__dirname, '..', 'electron', 'bbc-a11y.js')].concat(process.argv.slice(2)) 9 | 10 | const child = childProcess.spawn(electron, args) 11 | child.stdout.pipe(process.stdout) 12 | process.stdin.pipe(child.stdin) 13 | 14 | child.stderr.on('data', function (data) { 15 | const str = data.toString('utf8') 16 | // Silence Chromium/Electron noise 17 | if (str.match(/^\[\d+:\d+/) || str.match(/Electron Helper\[/) || str.match(/\*\*\* WARNING: Textured window/)) return 18 | process.stderr.write(data) 19 | }) 20 | 21 | child.on('close', function (code) { 22 | process.exit(code) 23 | }) 24 | 25 | process.on('SIGINT', function () { 26 | child.kill('SIGINT') 27 | child.kill('SIGTERM') 28 | }) 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'main' 7 | push: 8 | branches: 9 | - 'main' 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: [20.x, 22.x] 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - name: Fix Electron sandbox permissions 30 | run: sudo chown root:root node_modules/electron/dist/chrome-sandbox && sudo chmod 4755 node_modules/electron/dist/chrome-sandbox 31 | - name: Set display 32 | run: export DISPLAY=:99.0 33 | - name: Run headless tests 34 | run: xvfb-run --auto-servernum npm test 35 | -------------------------------------------------------------------------------- /features/standards/mag/audio_and_video/03_metadata.feature: -------------------------------------------------------------------------------- 1 | Feature: Metadata 2 | 3 | Relevant metadata should be provided for all media. 4 | 5 | Metadata, information about an item, can help people to find what they 6 | require. Metadata provided with media content can help users understand the 7 | media and locate alternative versions. 8 | 9 | Relevant metadata might include duration, and the presence of subtitles, 10 | sign language, or audio description. 11 | 12 | Background: 13 | Given I am performing a manual test of the "Audio & Video: Metadata: Relevant metadata should be provided for all media" standard 14 | And I have been asked "Is relevant metadata provided for all media?" 15 | 16 | @html @manual 17 | Scenario: Manual pass 18 | When I answer "Yes (or not applicable)" 19 | Then the manual test passes 20 | 21 | @html @manual 22 | Scenario: Manual fail 23 | When I answer "No" 24 | Then the manual test fails 25 | -------------------------------------------------------------------------------- /lib/electron/cookies.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | set: ({ url, cookies }) => { 3 | const electronRemote = require('electron').remote 4 | const session = electronRemote.session.defaultSession 5 | 6 | if (!cookies || !cookies.length) { 7 | return Promise.resolve() 8 | } 9 | 10 | return Promise.all((cookies || []).map((cookie) => new Promise((resolve, reject) => { 11 | try { 12 | const cookieValue = Object.assign({}, cookie, { url }) 13 | console.log('Setting cookie:', cookieValue) 14 | 15 | session.cookies.set(cookieValue) 16 | .then(() => { 17 | console.log('Cookie set successfully:', cookie.name) 18 | resolve() 19 | }) 20 | .catch(err => { 21 | console.error('Error setting cookie:', err) 22 | resolve() 23 | }) 24 | } catch (e) { 25 | console.error('Exception in cookie handling:', e) 26 | resolve() 27 | } 28 | }))) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/commandLineArgsSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const commandLineArgs = require('../lib/cli/args') 3 | const assert = require('assert') 4 | 5 | describe('commandLineArgs.parse(argv)', function () { 6 | it('parses --width integer as viewport width', function () { 7 | const args = commandLineArgs.parse(['node', 'bbc-a11y', '--width', '777']) 8 | assert.equal(777, args.width) 9 | }) 10 | 11 | it('parses --interactive as a boolean', function () { 12 | const args = commandLineArgs.parse(['node', 'bbc-a11y', '--interactive']) 13 | assert.equal(true, args.interactive) 14 | }) 15 | 16 | it('parses --config as a string', function () { 17 | const args = commandLineArgs.parse(['node', 'bbc-a11y', '--config', 'foo']) 18 | assert.equal('foo', args.configPath) 19 | }) 20 | 21 | it('parses all other arguments as URLs', function () { 22 | const args = commandLineArgs.parse(['node', 'bbc-a11y', 'foo', 'bar']) 23 | assert.deepEqual(['foo', 'bar'], args.urls) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /features/standards/mag/focus/03_content_order.feature: -------------------------------------------------------------------------------- 1 | Feature: Content order 2 | 3 | Content order must be logical. 4 | 5 | All users benefit when content is logically ordered, in particular users of 6 | assistive technology that follows the flow of the page or screen. 7 | 8 | Assistive technology such as screen readers will read through a page or 9 | screen in content order, regardless of the layout. However, expert users may 10 | jump between elements such as headings and move forward or backward from that 11 | point. 12 | 13 | Background: 14 | Given I am performing a manual test of the "Focus: Content order: Content order must be logical" standard 15 | And I have been asked "Is content logically ordered?" 16 | 17 | @html @manual 18 | Scenario: Logical content order (manual pass) 19 | When I answer "Yes" 20 | Then the manual test passes 21 | 22 | @html @manual 23 | Scenario: Illogical content order (manual fail) 24 | When I answer "No" 25 | Then the manual test fails 26 | -------------------------------------------------------------------------------- /features/cli/skipping_standards.feature: -------------------------------------------------------------------------------- 1 | Feature: Skipping Standards 2 | 3 | Scenario: One standard is skipped 4 | Given a website running on a11ytests.com 5 | And a file named "a11y.js" with: 6 | """ 7 | page("https://a11ytests.com/missing_main_heading", { 8 | skip: "Structure: Headings: Exactly one main heading" 9 | }) 10 | """ 11 | When I run `bbc-a11y` 12 | Then it should pass with: 13 | """ 14 | ✓ https://a11ytests.com/missing_main_heading 15 | """ 16 | 17 | Scenario: All standards except one is skipped 18 | Given a website running on a11ytests.com 19 | And a file named "a11y.js" with: 20 | """ 21 | page("https://a11ytests.com/two_headings_failures", { 22 | only: "Structure: Headings: Exactly one main heading" 23 | }) 24 | """ 25 | When I run `bbc-a11y` 26 | Then it should fail with: 27 | """ 28 | 1 page checked, 1 error found, 0 warnings, [count all standards - 1] standards skipped 29 | """ 30 | -------------------------------------------------------------------------------- /features/disabled_caching.feature: -------------------------------------------------------------------------------- 1 | Feature: Disabled Caching 2 | 3 | So that testers can make changes to their code and re-test, all caching 4 | must be disabled 5 | 6 | Scenario: Re-testing an updated page 7 | Given a website running on a11ytests.com 8 | When I run `bbc-a11y https://a11ytests.com/goodThenBad` 9 | Then it should pass with: 10 | """ 11 | No errors found. But please remember: 12 | 13 | "Testing shows the presence, not the absence of bugs" -- Edsger W. Dijkstra 14 | 15 | I am only a robot. Always make time to perform manual testing using assistive 16 | technologies like VoiceOver, JAWS and NVDA to make sure you're providing a good 17 | user experience. 18 | """ 19 | When I run `bbc-a11y https://a11ytests.com/goodThenBad?retest=true` 20 | Then it should fail with: 21 | """ 22 | ✗ https://a11ytests.com/goodThenBad?retest=true 23 | * Structure: Headings: Exactly one main heading 24 | - Found 0 h1 elements. 25 | """ 26 | -------------------------------------------------------------------------------- /lib/cli/args.js: -------------------------------------------------------------------------------- 1 | const commander = require('commander') 2 | const version = require('../../package.json').version 3 | 4 | module.exports = { 5 | parse: function (argv) { 6 | const opts = commander 7 | .version(version) 8 | .arguments('<urls>') 9 | .option('-i, --interactive', 'show browser window and leave open for debugging') 10 | .option('-m, --manual', 'include manual tests') 11 | .option('-w, --width <pixels>', 'override viewport width', parseInt) 12 | .option('-c, --config <config>', 'path to config file') 13 | .option('-r, --reporter <reporter>', 'json or [path to custom reporter module]') 14 | .option('--coverage <list|table>', 'lists all tests with a description of each test') 15 | .parse(argv) 16 | 17 | return { 18 | interactive: !!opts.interactive, 19 | manual: !!opts.manual, 20 | urls: opts.args, 21 | width: opts.width, 22 | configPath: opts.config, 23 | reporter: opts.reporter, 24 | coverage: opts.coverage 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/config/loader.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | loadConfigFromPath: function (pathToConfigModule) { 3 | const pages = [] 4 | const hadPage = 'page' in global 5 | const oldPage = global.page 6 | global.page = (url, config = {}) => { 7 | const page = { url } 8 | if (config.skip) page.skip = [].concat(config.skip) 9 | if (config.only) page.only = [].concat(config.only) 10 | if (config.width) page.width = config.width 11 | if (config.hide) page.hide = [].concat(config.hide) 12 | if (config.cookies) page.cookies = config.cookies 13 | if (config.visit) page.visit = config.visit 14 | if (config.w3cValidator) page.w3cValidator = config.w3cValidator 15 | pages.push(page) 16 | return page 17 | } 18 | let error 19 | try { 20 | require(pathToConfigModule) 21 | } catch (e) { 22 | error = e 23 | } 24 | if (hadPage) { global.page = oldPage } else { delete global.page } 25 | if (error) { return Promise.reject(error) } else { return Promise.resolve({ pages }) } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/standards/tests/textCannotBeTooSmall.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Text cannot be too small', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: 'text node with a computed font size of less than 11px', 7 | 8 | test: function ({ $, fail }) { 9 | const textNodes = $('*:not(head, script, style):visible').contents().filter(function () { 10 | return this.nodeType === 3 && this.textContent.trim().length > 0 11 | }) 12 | 13 | const parents = [] 14 | textNodes.each(function (index, node) { 15 | const parent = $(node).parent()[0] 16 | if (parents.indexOf(parent) === -1) { 17 | parents.push(parent) 18 | } 19 | }) 20 | 21 | for (let i = 0; i < parents.length; ++i) { 22 | const element = $(parents[i]) 23 | const size = fontSizeOf(element) 24 | if (size < 11 && size !== fontSizeOf(element.parent())) { 25 | fail('Text size too small (' + size + 'px):', element) 26 | } 27 | } 28 | } 29 | } 30 | 31 | function fontSizeOf (element) { 32 | return Number(element.css('fontSize').replace('px', '')) 33 | } 34 | -------------------------------------------------------------------------------- /features/standards/mag/audio_and_video/05_audio_conflict.feature: -------------------------------------------------------------------------------- 1 | Feature: Audio conflict 2 | 3 | Narrative audio in games or interactive media should not talk over or conflict 4 | with native assistive technology. 5 | 6 | In order to interact with embedded media, users need to perceive the editorial 7 | narrative and/or instructions. 8 | 9 | If the embedded media is self-voicing content, then this should be hidden from 10 | the screen reader. If the embedded media is providing content to the screen 11 | reader, then this should not be self-voicing. 12 | 13 | Background: 14 | Given I am performing a manual test of the "Audio & Video: Audio conflict: Narrative audio should not conflict with assistive technology" standard 15 | And I have been asked "Does narrative audio in any interactive media conflict with native assistive technology?" 16 | 17 | @html @manual 18 | Scenario: Manual pass 19 | When I answer "No (or not applicable)" 20 | Then the manual test passes 21 | 22 | @html @manual 23 | Scenario: Manual fail 24 | When I answer "Yes" 25 | Then the manual test fails 26 | -------------------------------------------------------------------------------- /electron/renderer.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const Runner = require('../lib/runner') 4 | const Reporter = require('../lib/reporter') 5 | const ElectronWindowAdapter = require('./windowAdapter') 6 | const CommandLineArgs = require('../lib/cli/args') 7 | 8 | const electronRemote = require('electron').remote 9 | const remoteConsole = electronRemote.getGlobal('console') 10 | 11 | const argv = electronRemote.process.argv 12 | const args = CommandLineArgs.parse(argv) 13 | 14 | const reporter = Reporter.createReporter({ name: args.reporter, console, remoteConsole }) 15 | 16 | const { coverage } = args 17 | 18 | const exit = args.interactive ? () => {} : electronRemote.process.exit 19 | 20 | const configPath = path.resolve(args.configPath || path.join(process.cwd(), 'a11y.js')) 21 | 22 | const pages = args.urls.map(url => ({ url, width: args.width })) 23 | 24 | const windowAdapter = new ElectronWindowAdapter() 25 | 26 | const runManualTests = args.manual 27 | 28 | new Runner({ 29 | configPath, 30 | pages, 31 | windowAdapter, 32 | runManualTests, 33 | reporter, 34 | coverage, 35 | exit 36 | }).run() 37 | -------------------------------------------------------------------------------- /features/standards/mag/scripts_and_dynamic_content/03_page_refreshes.feature: -------------------------------------------------------------------------------- 1 | Feature: Page refreshes 2 | 3 | Automatic page refreshes must not be used without warning. 4 | 5 | Assistive technology, such as a screen reader, may lose its place in the 6 | content and announce the wrong information when an entire page reloads 7 | automatically. This can be confusing and disorienting for the user. 8 | 9 | Techniques that would trigger an entire page to be reloaded must not be used 10 | unless the user has been notified. 11 | 12 | Background: 13 | Given I am performing a manual test of the "Scripts and dynamic content: Page refreshes: Automatic page refreshes must not be used without warning" standard 14 | And I have been asked "Does the page refresh automatically without warning?" 15 | 16 | @html @manual 17 | Scenario: Page does not refresh automatically without warning (manual pass) 18 | When I answer "No" 19 | Then the manual test passes 20 | 21 | @html @manual 22 | Scenario: Page refreshes automatically without warning (manual fail) 23 | When I answer "Yes" 24 | Then the manual test fails 25 | -------------------------------------------------------------------------------- /features/standards/mag/design/05_spacing.feature: -------------------------------------------------------------------------------- 1 | Feature: Spacing 2 | 3 | An inactive space should be provided around actionable elements. 4 | 5 | Anyone can find it challenging to interact with small controls that are 6 | closely grouped together, in particular users with motor or vision impairment. 7 | 8 | Actionable elements should not touch or overlap, and there should be an 9 | inactive space between actionable elements in order to reduce the risk of 10 | activating the wrong control. The minimum space possible to set on any device 11 | or screen resolution is 1 pixel, preferably the space would be larger. 12 | 13 | Background: 14 | Given I am performing a manual test of the "Design: Spacing: An inactive space should be provided around actionable elements" standard 15 | And I have been asked "Is an inactive space provided around every actionable element?" 16 | 17 | @html @manual 18 | Scenario: Manual pass 19 | When I answer "Yes (or not applicable)" 20 | Then the manual test passes 21 | 22 | @html @manual 23 | Scenario: Manual fail 24 | When I answer "No" 25 | Then the manual test fails 26 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v6 11 | with: 12 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 13 | stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' 14 | close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' 15 | close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' 16 | days-before-issue-stale: 30 17 | days-before-pr-stale: 45 18 | days-before-issue-close: 5 19 | days-before-pr-close: 10 20 | operations-per-run: 90 21 | exempt-issue-labels: keep 22 | exempt-pr-labels: keep 23 | exempt-all-assignees: true 24 | exempt-all-milestones: true 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Resolves #NUMBER 2 | 3 | **Description:** 4 | _A very high-level summary of easily-reproducible changes that can be understood by non-devs._ 5 | 6 | **Type of change:** 7 | 8 | - [ ] Bug fix (non-breaking change which fixes an issue) 9 | - [ ] New feature (non-breaking change which adds functionality) 10 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 11 | 12 | **Why is this change required?:** 13 | _A simple explanation of what the problem is and how this PR solves it_ 14 | 15 | **Code changes:** 16 | 17 | - _A bullet point list of key code changes that have been made._ 18 | - _When describing code changes, try to communicate **how** and **why** you implemented something a specific way, not just **what** has changed._ 19 | 20 | --- 21 | 22 | **Checklist:** 23 | 24 | - [ ] My code follows the code style of this project. 25 | - [ ] My change requires a change to the documentation. 26 | - [ ] I have updated the documentation accordingly. 27 | - [ ] I have read the **CONTRIBUTING** document. 28 | - [ ] I have added tests to cover my changes. 29 | - [ ] All new and existing tests passed. 30 | -------------------------------------------------------------------------------- /features/cli/report_configuration_errors.feature: -------------------------------------------------------------------------------- 1 | Feature: Report configuration errors 2 | 3 | Scenario: Call an unknown keyword in the configuration file 4 | Given a file named "a11y.js" with: 5 | """ 6 | page("one") 7 | page("two") 8 | paage("http://bad.com") 9 | page("three") 10 | page("four") 11 | """ 12 | When I run `bbc-a11y` 13 | Then it should fail with exactly: 14 | """ 15 | There was an error reading your configuration file at line 3 of 'a11y.js' 16 | 17 | paage is not defined 18 | 19 | For help learning the configuration DSL, please visit https://github.com/bbc/bbc-a11y 20 | 21 | """ 22 | 23 | Scenario: Compilation error in configuration file 24 | Given a file named "a11y.js" with: 25 | """ 26 | throw new Error("oops") 27 | """ 28 | When I run `bbc-a11y` 29 | Then it should fail with exactly: 30 | """ 31 | There was an error reading your configuration file at line 1 of 'a11y.js' 32 | 33 | oops 34 | 35 | For help learning the configuration DSL, please visit https://github.com/bbc/bbc-a11y 36 | 37 | """ 38 | -------------------------------------------------------------------------------- /lib/standards/tests/titleElementMustIdentifyMainContent.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Title element must identify main content', 3 | 4 | type: 'semi-automated', 5 | 6 | failsForEach: '<title> element that does not match a pre-configured pattern ' + 7 | 'or function', 8 | 9 | test: ({ $, fail, warn, pageConfiguration }) => { 10 | function testTitle (expected) { 11 | const actual = $('title').text() 12 | if (expected !== actual) { 13 | fail(`Title element failed to identify main content: expected "${expected}", actual "${actual}"`) 14 | } 15 | } 16 | switch (typeof pageConfiguration.title) { 17 | case 'undefined': 18 | return 19 | case 'function': 20 | return testTitle(pageConfiguration.title($)) 21 | case 'string': { 22 | let expected = pageConfiguration.title 23 | const template = expected.match(/\{([^}].+)\}/) 24 | 25 | if (template) { 26 | expected = expected.replace(template[0], $(template[1]).text()) 27 | } 28 | return testTitle(expected) 29 | } 30 | } 31 | fail(`Invalid title ${pageConfiguration.title.toString()}`) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/lock-threads.yml: -------------------------------------------------------------------------------- 1 | name: "Lock Threads" 2 | 3 | on: 4 | schedule: 5 | - cron: "0 * * * *" # Once a day, at midnight UTC 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | concurrency: 13 | group: lock 14 | 15 | jobs: 16 | action: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@v4 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | issue-inactive-days: "30" # Lock issues after 30 days of being closed 23 | pr-inactive-days: "5" # Lock closed PRs after 5 days. This ensures that issues that stem from a PR are opened as issues, rather than comments on the recently merged PR. 24 | add-issue-labels: "outdated" 25 | exclude-issue-created-before: "2023-01-01" 26 | issue-comment: > 27 | This issue has been closed for more than 30 days. If this issue is still occuring, please open a new issue with more recent context. 28 | pr-comment: > 29 | This pull request has already been merged/closed. If you experience issues related to these changes, please open a new issue referencing this pull request. 30 | -------------------------------------------------------------------------------- /features/standards/mag/text_equivalents/02_decorative_content.feature: -------------------------------------------------------------------------------- 1 | Feature: Decorative content 2 | 3 | Decorative images must be hidden from assistive technology. 4 | 5 | Hidden or inactive content that is, for example, behind other content such as 6 | a pop-over or side-drawer, should not be navigable for users of assistive 7 | technology as they may think they can interact with this content. 8 | 9 | This can be achieved by setting the appropriate properties or states on an 10 | element or object, so assistive technology is informed that it is off-screen, 11 | obscured, or hidden. 12 | 13 | Background: 14 | Given I am performing a manual test of the "Text equivalents: Decorative content: Decorative images must be hidden from assistive technology" standard 15 | And I have been asked "Are decorative images hidden from assistive technology?" 16 | 17 | @html @manual 18 | Scenario: Decorative images hidden from assistive technology (manual pass) 19 | When I answer "Yes (or not applicable)" 20 | Then the manual test passes 21 | 22 | @html @manual 23 | Scenario: Decorative images not hidden from assistive technology (manual fail) 24 | When I answer "No" 25 | Then the manual test fails 26 | -------------------------------------------------------------------------------- /features/setting_up_the_browser_window.feature: -------------------------------------------------------------------------------- 1 | Feature: Setting up the browser window 2 | 3 | Providing a `visit` function that returns a promise, allows complete control 4 | over navigation. Validation begins after the promise is resolved. 5 | 6 | Scenario: Filling in a login box before running tests 7 | Given a website running on a11ytests.com 8 | And a file named "a11y.js" with: 9 | """ 10 | page('Members area (after logging in)', { 11 | visit: function (frame) { 12 | frame.src = 'https://a11ytests.com/perfect_after_login' 13 | return new Promise(function (test) { 14 | frame.onload = function () { 15 | var loginPage = frame.contentDocument 16 | loginPage.getElementById('username').value = 'admin' 17 | loginPage.getElementById('password').value = 'donut' 18 | loginPage.getElementById('loginButton').click() 19 | frame.onload = test 20 | } 21 | }) 22 | } 23 | }) 24 | """ 25 | When I run `bbc-a11y` 26 | Then it should pass with: 27 | """ 28 | ✓ Members area (after logging in) 29 | 30 | 1 page checked, 0 errors found, 0 warnings, 0 standards skipped 31 | """ 32 | -------------------------------------------------------------------------------- /features/standards/mag/focus/06_alternative_input_methods.feature: -------------------------------------------------------------------------------- 1 | Feature: Alternative input methods 2 | 3 | Alternative input methods must be supported. 4 | 5 | Some users do not use the input control provided with a device, such as the 6 | touch screen, or mouse. Instead, they may use a switch device, keyboard or 7 | braille display. 8 | 9 | Alternative methods of input and navigation that work with the platform must 10 | be supported to facilitate the needs of the user. 11 | 12 | Interactive content must not rely on a single input method. For example, a 13 | carousel must not support only touch interaction, it must also support 14 | alternative inputs via visible focusable elements. 15 | 16 | Background: 17 | Given I am performing a manual test of the "Focus: Alternative input methods: Alternative input methods must be supported" standard 18 | And I have been asked "Are alternative input methods, such as keyboard or voice, supported?" 19 | 20 | @html @manual 21 | Scenario: Alternative input methods supported (manual pass) 22 | When I answer "Yes" 23 | Then the manual test passes 24 | 25 | @html @manual 26 | Scenario: Alternative input methods unsupported (manual fail) 27 | When I answer "No" 28 | Then the manual test fails 29 | -------------------------------------------------------------------------------- /guides/using/semi-automated-tests.md: -------------------------------------------------------------------------------- 1 | # Semi-automated tests 2 | 3 | Some standards need additional configuration before they are run. These are 4 | considered optional, so they will not generate warnings if you don't configure 5 | them. The following tests are semi-automated: 6 | 7 | ### Page titles 8 | 9 | The `title` page configuration setting should be set to one of: 10 | 11 | * a string with the exact title 12 | * a string with a template part containing a CSS selector, for example 13 | `Site Name - {h1:first}` 14 | * a function taking a jQuery object as an argument and returning the title 15 | 16 | This setting will be matched against should the inner text of the `<title>` 17 | element, for example each of these tests will pass: 18 | 19 | ```js 20 | // Given the HTML at https://www.bbc.co.uk/news/technology-39419728 includes: 21 | // <title>Uber set to pull out of Denmark - BBC News 22 | 23 | page('https://www.bbc.co.uk/news/technology-39419728', { 24 | title: 'Uber set to pull out of Denmark - BBC News' 25 | }) 26 | 27 | page('https://www.bbc.co.uk/news/technology-39419728', { 28 | title: '{h1:first} - BBC News' 29 | }) 30 | 31 | page('https://www.bbc.co.uk/news/technology-39419728', { 32 | title: $ => $('h1:first').text() + ' - BBC News' 33 | }) 34 | ``` 35 | -------------------------------------------------------------------------------- /features/standards/mag/links/02_links_to_alternative_formats.feature: -------------------------------------------------------------------------------- 1 | Feature: Links to alternative formats 2 | 3 | Links to alternative formats must indicate that an alternative is opening. 4 | 5 | Unexpectedly opening an item in another format and/or application could cause 6 | any user to become disoriented. This is especially relevant for users with 7 | cognitive impairments or using assistive technology. 8 | 9 | It is important to inform the user that they will be going to a different 10 | format and/or application and what that will be, so that they know what to 11 | expect and where they are. 12 | 13 | Background: 14 | Given I am performing a manual test of the "Links: Links to alternative formats: Links to alternative formats must indicate that an alternative is opening" standard 15 | And I have been asked "Do all links to alternative formats indicate that an alternative is opening?" 16 | 17 | @html @manual 18 | Scenario: Links to alternative formats indicate that an alternative is opening (manual pass) 19 | When I answer "Yes (or not applicable)" 20 | Then the manual test passes 21 | 22 | @html @manual 23 | Scenario: Links to alternative formats do not indicate that an alternative is opening (manual fail) 24 | When I answer "No" 25 | Then the manual test fails 26 | -------------------------------------------------------------------------------- /features/standards/mag/links/03_combining_repeated_links.feature: -------------------------------------------------------------------------------- 1 | Feature: Combining repeated links 2 | 3 | Repeated links to the same resource must be combined within a single link. 4 | 5 | Grouping adjacent links to the same page or resource into a single link helps 6 | all users to navigate quickly, especially those using assistive technology, 7 | such as switch devices or screen readers. It reduces the number of elements to 8 | navigate, reduces screen reader verbosity, and also helps to increase touch 9 | target size. 10 | 11 | Repeated links, for example, could be an adjacent image, title and topic that 12 | all link to the same page or resource. 13 | 14 | Background: 15 | Given I am performing a manual test of the "Links: Combining repeated links: Repeated links to the same resource must be combined" standard 16 | And I have been asked "Are neighbouring elements linking to the same resource combined within a single link?" 17 | 18 | @html @manual 19 | Scenario: Repeated links to the same resource are combined within a single link (manual pass) 20 | When I answer "Yes (or not applicable)" 21 | Then the manual test passes 22 | 23 | @html @manual 24 | Scenario: Repeated links to the same resource (manual fail) 25 | When I answer "No" 26 | Then the manual test fails 27 | -------------------------------------------------------------------------------- /lib/standards/tests/headingsMustBeInAscendingOrder.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Headings must be in ascending order', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: 'visible heading (

to ) that does follow the next' + 7 | ' highest-level heading. For example, a

can only come after' + 8 | ' a

', 9 | 10 | test: function ({ $, fail, warn }) { 11 | const headings = $('h1, h2, h3, h4, h5, h6, h7, h8') 12 | const headingLevels = headings.map(function (index, heading) { 13 | return { heading, level: parseInt(heading.tagName[1]) } 14 | }) 15 | eachCons(headingLevels, 2).forEach(function (pair) { 16 | if (pair[1].level > (pair[0].level + 1)) { 17 | fail('Headings are not in order:', pair[0].heading, pair[1].heading) 18 | } 19 | }) 20 | if (headingLevels.length > 0 && headingLevels[0].level > 1) { 21 | warn('First heading was not a main heading:', headingLevels[0].heading) 22 | } 23 | } 24 | } 25 | 26 | function eachCons (a, n) { 27 | const r = [] 28 | for (let i = 0; i < a.length - n + 1; i++) { 29 | r.push(range(a, i, n)) 30 | } 31 | return r 32 | } 33 | 34 | function range (a, i, n) { 35 | const r = [] 36 | for (let j = 0; j < n; j++) { 37 | r.push(a[i + j]) 38 | } 39 | return r 40 | } 41 | -------------------------------------------------------------------------------- /features/cli/display_result_summary.feature: -------------------------------------------------------------------------------- 1 | Feature: Display result summary 2 | 3 | Scenario: Summarises pages checked and standard results 4 | Given a website running on a11ytests.com 5 | And a file named "a11y.js" with: 6 | """ 7 | page("https://a11ytests.com/perfect") 8 | page("https://a11ytests.com/missing_main_heading", { 9 | skip: "Structure: Headings: Exactly one main heading" 10 | }) 11 | page("https://a11ytests.com/missing_main_heading?again!") 12 | page("https://a11ytests.com/subheading_first") 13 | """ 14 | When I run `bbc-a11y` 15 | Then it should fail with: 16 | """ 17 | 4 pages checked, 1 error found, 1 warning, 1 standard skipped 18 | """ 19 | 20 | Scenario: Reminds users to consider usability beyond lint results 21 | Given a website running on a11ytests.com 22 | When I run `bbc-a11y https://a11ytests.com/perfect` 23 | Then it should pass with: 24 | """ 25 | No errors found. But please remember: 26 | 27 | "Testing shows the presence, not the absence of bugs" -- Edsger W. Dijkstra 28 | 29 | I am only a robot. Always make time to perform manual testing using assistive 30 | technologies like VoiceOver, JAWS and NVDA to make sure you're providing a good 31 | user experience. 32 | """ 33 | -------------------------------------------------------------------------------- /features/setting_cookies.feature: -------------------------------------------------------------------------------- 1 | Feature: Setting Cookies 2 | 3 | Scenario: Setting a cookie before visiting a page 4 | Given a website running on a11ytests.com 5 | And a file named "a11y.js" with: 6 | """ 7 | page("https://a11ytests.com/good_with_cookie", { 8 | cookies: [ 9 | { name: 'open', value: 'sesame' } 10 | ] 11 | }) 12 | page("https://a11ytests.com/good_with_cookie", { 13 | cookies: [ 14 | { name: 'open', value: 'wrong' } 15 | ] 16 | }) 17 | page("https://a11ytests.com/good_with_cookie") 18 | page("https://a11ytests.com/good_with_cookie", { 19 | cookies: [ 20 | { name: 'open', value: 'sesame' } 21 | ] 22 | }) 23 | """ 24 | When I run `bbc-a11y` 25 | Then it should fail with: 26 | """ 27 | ✓ https://a11ytests.com/good_with_cookie 28 | 29 | ✗ https://a11ytests.com/good_with_cookie 30 | * Structure: Containers and landmarks: Exactly one main landmark 31 | - Found 0 elements with role="main". 32 | 33 | ✗ https://a11ytests.com/good_with_cookie 34 | * Structure: Containers and landmarks: Exactly one main landmark 35 | - Found 0 elements with role="main". 36 | 37 | ✓ https://a11ytests.com/good_with_cookie 38 | """ 39 | -------------------------------------------------------------------------------- /features/standards/mag/design/03_styling_and_readability.feature: -------------------------------------------------------------------------------- 1 | Feature: Styling and readability 2 | 3 | Core content must still be accessible when styling is unsupported or removed. 4 | 5 | Older mobile devices may have poor support for fonts, colours and styles. 6 | Additionally, assistive technology cannot draw meaning from styling. And some 7 | users will change settings (fonts, styles, colours, etc.) to suit their needs. 8 | 9 | A user must still be able to complete the core purpose of a page or screen, 10 | whether reading or taking action, when background colours, images, layout or 11 | features are missing. For example, read a news article, play a radio station 12 | or navigate elsewhere. 13 | 14 | If embedded media is not supported then a suitable message should be displayed 15 | instead. 16 | 17 | Background: 18 | Given I am performing a manual test of the "Design: Styling and readability: Core content must be accessible when styling is removed" standard 19 | And I have been asked "Is the core content of the page readable and usable without styling/CSS?" 20 | 21 | @html @manual 22 | Scenario: Manual pass 23 | When I answer "Yes" 24 | Then the manual test passes 25 | 26 | @html @manual 27 | Scenario: Manual fail 28 | When I answer "No" 29 | Then the manual test fails 30 | -------------------------------------------------------------------------------- /features/standards/mag/links/01_descriptive_links.feature: -------------------------------------------------------------------------------- 1 | Feature: Descriptive links 2 | 3 | Link and navigation text must uniquely describe the target or function of the 4 | link or item. 5 | 6 | Unique links and navigation items are essential for screen reader and 7 | magnification users who may not perceive the context of a link or item. This 8 | is especially an issue for users who have not followed the content order. 9 | 10 | If link text is duplicated in a page or screen (e.g. Learn more..., More 11 | info..., Continue reading...) ways of making each link unique must also be 12 | used, for example, by using labels or hidden text. 13 | 14 | Background: 15 | Given I am performing a manual test of the "Links: Descriptive links: Link and navigation text must uniquely describe the target or function" standard 16 | And I have been asked "Does each link and navigation text uniquely describe the target or function of the link or item?" 17 | 18 | @html @manual 19 | Scenario: Link and navigation text uniquely describes the target or function (manual pass) 20 | When I answer "Yes" 21 | Then the manual test passes 22 | 23 | @html @manual 24 | Scenario: Link and navigation text does not uniquely describe the target or function (manual fail) 25 | When I answer "No" 26 | Then the manual test fails 27 | -------------------------------------------------------------------------------- /features/hiding_warnings.feature: -------------------------------------------------------------------------------- 1 | Feature: Hiding warnings 2 | 3 | Websites often contain external parts that we don't control, which will 4 | clutter up our pages with violations. 5 | 6 | We need a way to ignore these kinds of problems, so we can focus on the 7 | problems that we can actually fix. 8 | 9 | Scenario: Hide warnings matching a selector 10 | Given a website running on a11ytests.com 11 | And a file named "a11y.js" with: 12 | """ 13 | page("https://a11ytests.com/one_warning", { 14 | hide: "@id='main'" 15 | }) 16 | """ 17 | When I run `bbc-a11y` 18 | Then it should pass with: 19 | """ 20 | ✓ https://a11ytests.com/one_warning 21 | 22 | 1 page checked, 0 errors found, 0 warnings, 1 warning hidden, 0 standards skipped 23 | """ 24 | 25 | Scenario: Hide warnings matching a rule 26 | Given a website running on a11ytests.com 27 | And a file named "a11y.js" with: 28 | """ 29 | page("https://a11ytests.com/one_warning", { 30 | hide: "First heading was not a main heading" 31 | }) 32 | """ 33 | When I run `bbc-a11y` 34 | Then it should pass with: 35 | """ 36 | ✓ https://a11ytests.com/one_warning 37 | 38 | 1 page checked, 0 errors found, 0 warnings, 1 warning hidden, 0 standards skipped 39 | """ 40 | -------------------------------------------------------------------------------- /features/standards/mag/notifications/04_feedback_and_assistance.feature: -------------------------------------------------------------------------------- 1 | Feature: Feedback and assistance 2 | 3 | Non-critical feedback or assistance should be provided when appropriate. 4 | 5 | Occasional feedback and assistance can help people learn how to use something 6 | unfamiliar. It can be especially helpful for young children and people with 7 | cognitive impairments. 8 | 9 | When someone is not completing an objective correctly and/or does not progress 10 | multiple times, support and encouragement can motivate them to continue or 11 | keep trying. For example, in a game or other interactive content this could 12 | include hints, tips or the option to pass and move on to other content. 13 | 14 | Background: 15 | Given I am performing a manual test of the "Notifications: Feedback and assistance: Non-critical feedback or assistance should be provided when appropriate" standard 16 | And I have been asked "Is non-critical feedback or assistance provided in an accessible way when appropriate?" 17 | 18 | @html @manual 19 | Scenario: Non-critical feedback or assistance provided (manual pass) 20 | When I answer "Yes (or not applicable)" 21 | Then the manual test passes 22 | 23 | @html @manual 24 | Scenario: Non-critical feedback or assistance not provided (manual fail) 25 | When I answer "No" 26 | Then the manual test fails 27 | -------------------------------------------------------------------------------- /features/standards/mag/scripts_and_dynamic_content/04_timeouts.feature: -------------------------------------------------------------------------------- 1 | Feature: Timeouts 2 | 3 | A timed response must be adjustable. 4 | 5 | Some people may not be able to respond or interact before a time limit is 6 | reached. If a timeout is essential, allow users to extend, change or disable 7 | the time limit to ensure they can still access content, complete forms, and 8 | make choices at their own speed. 9 | 10 | For example, as appropriate for the content: 11 | 12 | * provide a means to adjust or disable a timing feature before starting an 13 | interaction, 14 | * warn the user of a timeout and provide a means to extend the time. 15 | * An exception may be made, with sought advice, for real-time content and 16 | content that would be invalidated by allowing more time, such as a quiz or 17 | vote. 18 | 19 | Background: 20 | Given I am performing a manual test of the "Scripts and dynamic content: Timeouts: Timed responses must be adjustable" standard 21 | And I have been asked "Are there any timed responses that cannot be adjusted?" 22 | 23 | @html @manual 24 | Scenario: No nonadjustable timed responses (manual pass) 25 | When I answer "No" 26 | Then the manual test passes 27 | 28 | @html @manual 29 | Scenario: Nonadjustable timed responses (manual fail) 30 | When I answer "Yes" 31 | Then the manual test fails 32 | -------------------------------------------------------------------------------- /test/minimumTextSizeStandardSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const standard = require('../lib/standards/tests/textCannotBeTooSmall') 3 | const $ = require('jquery') 4 | const expect = require('chai').expect 5 | 6 | describe('Minimum text size standard', function () { 7 | it('ignores text nodes with parents with display: none', function () { 8 | $('
Text!
').appendTo('body') 9 | const failures = [] 10 | const fail = function (failure) { 11 | failures.push(failure) 12 | } 13 | standard.test({ $, fail }) 14 | expect(failures).to.eql([]) 15 | }) 16 | 17 | it('ignores comment nodes', function () { 18 | $('
').appendTo('body') 19 | const failures = [] 20 | const fail = function (failure) { 21 | failures.push(failure) 22 | } 23 | standard.test({ $, fail }) 24 | expect(failures).to.eql([]) 25 | }) 26 | 27 | it('only reports the parent element when a child element also has small fonts', function () { 28 | $('
Text!
').appendTo('body') 29 | const failures = [] 30 | const fail = function (failure) { failures.push(failure) } 31 | standard.test({ $, fail }) 32 | expect(failures).to.eql(['Text size too small (2px):']) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/sectionsSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert') 3 | const sections = require('../lib/standards/sections') 4 | 5 | describe('Standards.sections', function () { 6 | it('has a unique documentationUrl for each section', function () { 7 | const urls = {} 8 | for (const name in sections) { 9 | const section = sections[name] 10 | assert.equal(typeof section.documentationUrl, 'string') 11 | if (urls[section.documentationUrl]) assert.fail('duplicate: ' + section.documentationUrl) 12 | urls[section.documentationUrl] = true 13 | } 14 | }) 15 | 16 | it('has a valid documentationUrl for each section (set TEST_DOCUMENTATION_URLS=true to run this test)', function () { 17 | if (process.env.TEST_DOCUMENTATION_URLS !== 'true') { 18 | return 19 | } 20 | this.timeout(60000) 21 | return Promise.all(Object.values(sections).map(section => { 22 | return fetch(section.documentationUrl) 23 | .then(response => { 24 | if (!response.ok) { 25 | throw new Error(`Invalid documentationUrl: ${section.documentationUrl}`) 26 | } 27 | return response.text() 28 | }) 29 | .then(body => { 30 | if (body.indexOf('Not found') > -1) { throw new Error(`Invalid documentationUrl: ${section.documentationUrl}`) } 31 | }) 32 | })) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /lib/standards/tests/contentMustBeVisibleAndUsableWithPageZoomed.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Content must be visible and usable with page zoomed to 200% of normal', 3 | 4 | type: 'manual', 5 | 6 | failsForEach: 'page whose content is not visible or is not usable when zoomed to 200% of normal', 7 | 8 | topFrameOnly: true, 9 | 10 | test: function ({ $, fail, warn, pageConfiguration, ask }) { 11 | if (typeof require === 'function') { 12 | window.top.launchZoomedWindow = function () { 13 | const electronRemote = require('electron').remote 14 | const currentWindow = electronRemote.getCurrentWindow() 15 | const BrowserWindow = electronRemote.BrowserWindow 16 | const simpleWindow = new BrowserWindow({ 17 | parent: currentWindow, 18 | offscreen: true, 19 | webPreferences: { 20 | webSecurity: false, 21 | enableRemoteModule: true, 22 | contextIsolation: false, 23 | nodeIntegration: true 24 | } 25 | }) 26 | simpleWindow.loadURL(pageConfiguration.url) 27 | simpleWindow.webContents.setZoomLevel(2) 28 | } 29 | return ask('Does the content scale when zoom or pinch-zoom is used?') 30 | .catch(function (error) { 31 | fail(error) 32 | }) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/standardsSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const Standards = require('../lib/standards') 3 | const expect = require('chai').expect 4 | 5 | describe('Standards', function () { 6 | describe('.matching(undefined)', function () { 7 | it('finds all standards', function () { 8 | const matches = Standards.matching() 9 | expect(matches.standards.length).to.equal(Standards.all.length) 10 | }) 11 | }) 12 | 13 | describe('.matching("Principles: Anchors must have hrefs")', function () { 14 | it('finds standards matching the string', function () { 15 | const matches = Standards.matching('Principles: Anchors must have hrefs') 16 | expect(matches.standards.length).to.equal(1) 17 | }) 18 | }) 19 | 20 | describe('.matching({ skip: ["Principles: Anchors must have hrefs"] })', function () { 21 | it('finds all standards except one', function () { 22 | const matches = Standards.matching({ skip: ['Principles: Anchors must have hrefs'] }) 23 | expect(matches.standards.length).to.equal(Standards.all.length - 1) 24 | }) 25 | }) 26 | 27 | describe('.matching({ only: ["Principles: Anchors must have hrefs"] })', function () { 28 | it('finds one standard', function () { 29 | const matches = Standards.matching({ only: ['Principles: Anchors must have hrefs'] }) 30 | expect(matches.standards.length).to.equal(1) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /features/standards/mag/focus/02_keyboard_trap.feature: -------------------------------------------------------------------------------- 1 | Feature: Keyboard trap 2 | 3 | There must not be a keyboard trap. 4 | 5 | If using a keyboard or other non-pointer input, user focus must be allowed to 6 | progress and not become trapped. All focusable elements must be accessible. 7 | 8 | Any modal components that open from a user action should keep focus within the 9 | component and must provide a means to close or dismiss the component, which 10 | would return focus to the trigger element. For example, on-screen keyboards, 11 | information panels, or full-screen media. 12 | 13 | Any menu or drawer component that opens from a user action may follow the 14 | modal pattern, or may automatically close or dismiss the component and return 15 | focus to the trigger element after the user moves focus onward from the last 16 | element. For example, a drop-down menu, side-drawer menu, or accordion panel. 17 | 18 | Background: 19 | Given I am performing a manual test of the "Focus: Keyboard trap: There must not be a keyboard trap" standard 20 | And I have been asked "Is anything a keyboard trap?" 21 | 22 | @html @manual 23 | Scenario: No keyboard trap exists (manual pass) 24 | When I answer "No" 25 | Then the manual test passes 26 | 27 | @html @manual 28 | Scenario: Keyboard trap exists (manual fail) 29 | When I answer "Yes" 30 | Then the manual test fails 31 | -------------------------------------------------------------------------------- /lib/reporters/json.js: -------------------------------------------------------------------------------- 1 | function JsonReporter (devToolsConsole, commandLineConsole) { 2 | this.devToolsConsole = devToolsConsole 3 | this.commandLineConsole = commandLineConsole 4 | } 5 | 6 | JsonReporter.prototype.runStarted = function () { 7 | this.summary = { 8 | pagesChecked: 0, 9 | errorsFound: 0, 10 | errorsHidden: 0, 11 | standardsSkipped: 0, 12 | pages: [] 13 | } 14 | } 15 | 16 | JsonReporter.prototype.pageChecked = function (page, pageResult) { 17 | pageResult = pageResult.flatten() 18 | this.summary.errorsFound += pageResult.errorsFound 19 | this.summary.pagesChecked++ 20 | this.summary.standardsSkipped += pageResult.skipped.length 21 | this.summary.pages.push({ url: page.url, result: pageResult }) 22 | } 23 | 24 | JsonReporter.prototype.runEnded = function () { 25 | this.log(this.summary) 26 | } 27 | 28 | JsonReporter.prototype.unexpectedError = function (error) { 29 | this.log({ outcome: 'error', error: error.stack }) 30 | } 31 | 32 | JsonReporter.prototype.configMissing = function () { 33 | this.log({ outcome: 'error', error: 'Missing configuration file' }) 34 | } 35 | 36 | JsonReporter.prototype.configError = function (error) { 37 | this.log({ outcome: 'error', error: error.stack }) 38 | } 39 | 40 | JsonReporter.prototype.log = function (object) { 41 | this.commandLineConsole.log(JSON.stringify(object, null, 2)) 42 | } 43 | 44 | module.exports = JsonReporter 45 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | ## bbc-a11y-docker 2 | 3 | A Docker image containing [bbc-a11y](https://github.com/bbc/bbc-a11y) and all the necessary packages. 4 | 5 | You can use this when you want to run bbc-a11y from a machine on which you can't install node.js or electron, for whatever reason (such as insufficient priveleges or incompatibility). 6 | 7 | For example, electron is not easy to install on CentOS, so if you are using CentOS build agents in your continuous integration environment, this may be useful to you. 8 | 9 | The docker image is not always necessary to use bbc-a11y for continuous integration. For example, in TravisCI one option available is to prepend the run script with xvfb-run - [relevant TravisCI documentation](https://docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI). Furthermore, an alternative to using the docker image in Jenkins might be to use the [xvfb plugin](https://wiki.jenkins.io/display/JENKINS/Xvfb+Plugin) - though this is untested. 10 | 11 | ### Usage 12 | 13 | Test a single page: 14 | 15 | ``` 16 | docker run --rm --tty bbca11y/bbc-a11y-docker https://www.bbc.co.uk/news/0 17 | ``` 18 | 19 | Or run tests from a config file: 20 | 21 | ``` 22 | docker run --rm --tty --volume $PWD:/ws bbca11y/bbc-a11y-docker --config /ws/a11y.js 23 | ``` 24 | 25 | ### Credits 26 | 27 | Thanks to [Joseph Wynn](https://github.com/wildlyinaccurate) for the initial version. 28 | -------------------------------------------------------------------------------- /features/standards/mag/audio_and_video/04_volume_control.feature: -------------------------------------------------------------------------------- 1 | Feature: Volume control 2 | 3 | Separate volume controls should be provided for background music, ambient 4 | sounds, narrative and editorially significant sound effects. 5 | 6 | Separate volume controls, in addition to the mute control, should be provided 7 | in settings for interactive media, such as games, to minimise the risk of 8 | sensory overload for users with audio sensitivity: 9 | 10 | * Users with cognitive impairments that include audio sensitivity need to be 11 | able to minimise the risk of sensory shock. 12 | 13 | * Users with mild to moderate hearing impairment may need to adjust different 14 | audio elements to hear the narrative speech clearly. 15 | 16 | * Screen reader users need to be able to hear the screen reader over the 17 | sounds within interactive media. 18 | 19 | Background: 20 | Given I am performing a manual test of the "Audio & Video: Volume control: Volume controls should be provided for interactive media" standard 21 | And I have been asked "Are suitable volume controls provided for different audio layers in all interactive media?" 22 | 23 | @html @manual 24 | Scenario: Manual pass 25 | When I answer "Yes (or not applicable)" 26 | Then the manual test passes 27 | 28 | @html @manual 29 | Scenario: Manual fail 30 | When I answer "No" 31 | Then the manual test fails 32 | -------------------------------------------------------------------------------- /features/standards/mag/design/12_flicker.feature: -------------------------------------------------------------------------------- 1 | Feature: Flicker 2 | Content must not visibly or intentionally flicker or flash more than three 3 | times in any one-second period. 4 | 5 | Visual flicker, flashing and strobe lighting can affect anyone, but some users 6 | will be more susceptible than others. Symptoms may include eyestrain, 7 | dizziness, fatigue, headaches, migraine, and nausea. Users with medical 8 | conditions such as Ménière's or photosensitive epilepsy can be severely 9 | affected, experiencing vertigo, hearing loss and seizures. 10 | 11 | A well-documented example of the effects of flicker is Pokémon Shock. 12 | 13 | If flicker is unavoidable, the user must be warned before they reach the content. 14 | 15 | Where editorially appropriate, provide an alternative version of content that 16 | does not flicker but is as close to the original as possible. 17 | 18 | Background: 19 | Given I am performing a manual test of the "Design: Flicker: Content must not flicker or flash" standard 20 | And I have been asked "Does any content visibly or intentionally flicker or flash more than three times in any one-second period?" 21 | 22 | @html @manual 23 | Scenario: Manual pass 24 | When I answer "No (or not applicable)" 25 | Then the manual test passes 26 | 27 | @html @manual 28 | Scenario: Manual fail 29 | When I answer "Yes" 30 | Then the manual test fails 31 | -------------------------------------------------------------------------------- /features/standards/mag/forms/02_form_inputs.feature: -------------------------------------------------------------------------------- 1 | Feature: Form inputs 2 | 3 | A default input format must be indicated and supported. 4 | 5 | All users benefit from clearly indicated, well supported, form input formats, 6 | whether text, numbers, date, or a specific combination. It makes it easier to 7 | get it right first time and reduces errors when completing forms. 8 | 9 | The format required can be indicated as part of the label, set by correctly 10 | coding the input field, and assisted by providing the correct keyboard mode on 11 | devices that support it. 12 | 13 | Gesture based input, such as a slider or swipe-able select list, should also 14 | be clearly indicated. Any gestures must be implemented along with support for 15 | accessible alternatives, for example mobile keyboards. 16 | 17 | Background: 18 | Given I am performing a manual test of the "Forms: Form inputs: A default input format must be indicated and supported" standard 19 | And I have been asked "Is a default input format indicated/implied and supported?" 20 | 21 | @html @manual 22 | Scenario: A default input format is indicated and supported (manual pass) 23 | When I answer "Yes (or not applicable)" 24 | Then the manual test passes 25 | 26 | @html @manual 27 | Scenario: A default input format is not indicated or supported (manual fail) 28 | When I answer "No" 29 | Then the manual test fails 30 | -------------------------------------------------------------------------------- /features/standards/mag/forms/03_form_layout.feature: -------------------------------------------------------------------------------- 1 | Feature: Form layout 2 | 3 | Labels must be placed close to the relevant form control, and laid out 4 | appropriately. 5 | 6 | Labelling form controls helps users to understand what is required. Keep 7 | labels close to the associated form control to prevent users becoming 8 | disoriented, particularly users who magnify or zoom in on content. 9 | 10 | Labels should precede associated controls, visually above or to the left of 11 | the input field. Labels for radio buttons and checkboxes visually work better 12 | to the right of the field, however, assistive technology such as screen 13 | readers must always speak the associated label before the input control. 14 | Labels for select lists may be included as the first item of the list itself. 15 | 16 | Background: 17 | Given I am performing a manual test of the "Forms: Form layout: Labels must be close and laid out appropriately" standard 18 | And I have been asked "Are labels placed close to the relevant form control, and laid out appropriately?" 19 | 20 | @html @manual 21 | Scenario: Labels are close and laid out appropriately (manual pass) 22 | When I answer "Yes (or not applicable)" 23 | Then the manual test passes 24 | 25 | @html @manual 26 | Scenario: Labels are not close or laid out inappropriately (manual fail) 27 | When I answer "No" 28 | Then the manual test fails 29 | -------------------------------------------------------------------------------- /features/standards/mag/images/01_images_of_text.feature: -------------------------------------------------------------------------------- 1 | Feature: Images of text 2 | 3 | Images of text should be avoided. 4 | 5 | Images are an inflexible way to present text information. The text can blur 6 | when magnified or enlarged, is difficult to adapt for users wishing to change 7 | the colour, language or spacing, and is not available to assistive technology 8 | such as screen readers. Additionally, images can be slow to download and 9 | require more data. 10 | 11 | Sometimes it may be difficult to avoid using images of text, such as for brand 12 | logos or interactive content. If text can be read, it should also be available 13 | to assistive technology. Consider using methods such as SVG images, text 14 | alternatives, hidden text, and ARIA labels. 15 | 16 | Where available, use BBC Global Experience Language iconography. Icons should 17 | always have a consistent label, which is visible text when possible. 18 | 19 | Background: 20 | Given I am performing a manual test of the "Images: Images of text: Images of text should be avoided" standard 21 | And I have been asked "Are there any unnecessary images of text?" 22 | 23 | @html @manual 24 | Scenario: No images of text (manual pass) 25 | When I answer "No" 26 | Then the manual test passes 27 | 28 | @html @manual 29 | Scenario: Images of text (manual fail) 30 | When I answer "Yes" 31 | Then the manual test fails 32 | -------------------------------------------------------------------------------- /features/standards/mag/audio_and_video/02_autoplay.feature: -------------------------------------------------------------------------------- 1 | Feature: Autoplay 2 | 3 | Audio must not play automatically unless the user is made aware this will 4 | happen or a pause/stop/mute button is provided. 5 | 6 | Audio in AV and interactive content can be disruptive for screen reader users 7 | because it can conflict with and speak over the screen reader. Unexpected 8 | audio may also distress users with cognitive or sensory sensitivity. 9 | 10 | Users should be given a choice to opt in for auto-playing content audio. Where 11 | a pause/stop/mute button is provided instead, it must be fully and immediately 12 | accessible. 13 | 14 | Where play automatically continues to the next content item, this must be 15 | indicated in an accessible way, with a choice to opt out and sufficient time 16 | to do so. 17 | 18 | User preferences should persist. 19 | 20 | Background: 21 | Given I am performing a manual test of the "Audio & Video: Autoplay: Audio must not play automatically without controls" standard 22 | And I have been asked "Does any audio that plays automatically make the user aware this will happen, or provide a pause/stop/mute button?" 23 | 24 | @html @manual 25 | Scenario: Manual pass 26 | When I answer "Yes (or not applicable)" 27 | Then the manual test passes 28 | 29 | @html @manual 30 | Scenario: Manual fail 31 | When I answer "No" 32 | Then the manual test fails 33 | -------------------------------------------------------------------------------- /features/standards/mag/images/02_background_images.feature: -------------------------------------------------------------------------------- 1 | Feature: Background images 2 | 3 | Background images that convey information or meaning must have an additional 4 | accessible alternative. 5 | 6 | Background images are not available to assistive technology such as screen 7 | readers and are not supported on devices with minimal support for CSS. 8 | Additionally, a background image may not load. 9 | 10 | It is not possible to directly assign alternative text to a CSS background 11 | image. Another method must also be used to provide the same information 12 | visibly, and in a way that is programmatically determinable by assistive 13 | technology, such as screen readers. 14 | 15 | Background: 16 | Given I am performing a manual test of the "Images: Background images: Meaningful background images must have accessible alternatives" standard 17 | And I have been asked "Are accessible alternatives provided for element background images that convey information or meaning?" 18 | 19 | @html @manual 20 | Scenario: Accessible alternatives are provided for background images that convey information or meaning (manual pass) 21 | When I answer "Yes (or not applicable)" 22 | Then the manual test passes 23 | 24 | @html @manual 25 | Scenario: No accessible alternatives are provided for background images that convey information or meaning (manual fail) 26 | When I answer "No" 27 | Then the manual test fails 28 | -------------------------------------------------------------------------------- /features/hiding_errors.feature: -------------------------------------------------------------------------------- 1 | Feature: Hiding errors 2 | 3 | Websites often contain external parts that we don't control, which will 4 | clutter up our pages with violations. 5 | 6 | We need a way to ignore these kinds of problems, so we can focus on the 7 | problems that we can actually fix. 8 | 9 | Scenario: Hide errors matching a pattern 10 | Given a website running on a11ytests.com 11 | And a file named "a11y.js" with: 12 | """ 13 | page("https://a11ytests.com/errors_in_orb_modules", { 14 | hide: "@id='orb-modules'" 15 | }) 16 | """ 17 | When I run `bbc-a11y` 18 | Then it should pass with: 19 | """ 20 | ✓ https://a11ytests.com/errors_in_orb_modules 21 | 22 | 1 page checked, 0 errors found, 2 errors hidden, 0 warnings, 0 standards skipped 23 | """ 24 | 25 | Scenario: Hide errors matching multiple patterns 26 | Given a website running on a11ytests.com 27 | And a file named "a11y.js" with: 28 | """ 29 | page("https://a11ytests.com/errors_in_orb_modules", { 30 | hide: [ 31 | "No content follows:", 32 | "Image has no alt attribute:" 33 | ] 34 | }) 35 | """ 36 | When I run `bbc-a11y` 37 | Then it should pass with: 38 | """ 39 | ✓ https://a11ytests.com/errors_in_orb_modules 40 | 41 | 1 page checked, 0 errors found, 2 errors hidden, 0 warnings, 0 standards skipped 42 | """ 43 | -------------------------------------------------------------------------------- /lib/standards/tests/fieldsMustHaveLabelsOrTitles.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Fields must have labels or titles', 3 | 4 | type: 'automated', 5 | 6 | failsForEach: [ 7 | 'visible input element (,