diff --git a/src/DetailsView/components/assessment-visualization-enabled-toggle.tsx b/src/DetailsView/components/assessment-visualization-enabled-toggle.tsx index d3ef06bcf53..461e78ad1c3 100644 --- a/src/DetailsView/components/assessment-visualization-enabled-toggle.tsx +++ b/src/DetailsView/components/assessment-visualization-enabled-toggle.tsx @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { GeneratedAssessmentInstance } from 'common/types/store-data/assessment-result-data'; -import { isEmpty } from 'lodash'; import { BaseVisualHelperToggle } from './base-visual-helper-toggle'; export class AssessmentVisualizationEnabledToggle extends BaseVisualHelperToggle { - protected isDisabled(filteredInstances: GeneratedAssessmentInstance<{}, {}>[]): boolean { - return isEmpty(filteredInstances); + protected isDisabled(instances: GeneratedAssessmentInstance<{}, {}>[]): boolean { + return !this.isAnyInstanceVisualizable(instances); } protected isChecked(instances: GeneratedAssessmentInstance<{}, {}>[]): boolean { @@ -28,10 +27,16 @@ export class AssessmentVisualizationEnabledToggle extends BaseVisualHelperToggle }; private isAnyInstanceVisible(instances: GeneratedAssessmentInstance<{}, {}>[]): boolean { + const testStep = this.props.assessmentNavState.selectedTestSubview; return instances.some( - instance => - instance.testStepResults[this.props.assessmentNavState.selectedTestSubview] - .isVisualizationEnabled, + instance => instance.testStepResults[testStep].isVisualizationEnabled, + ); + } + + private isAnyInstanceVisualizable(instances: GeneratedAssessmentInstance<{}, {}>[]): boolean { + const testStep = this.props.assessmentNavState.selectedTestSubview; + return instances.some( + instance => instance.testStepResults[testStep].isVisualizationSupported, ); } } diff --git a/src/assessments/assessment-builder.tsx b/src/assessments/assessment-builder.tsx index b150d41e3b5..a01c3f3bed5 100644 --- a/src/assessments/assessment-builder.tsx +++ b/src/assessments/assessment-builder.tsx @@ -10,6 +10,7 @@ import { RequirementComparer } from 'common/assessment/requirement-comparer'; import { AssessmentVisualizationConfiguration } from 'common/configs/assessment-visualization-configuration'; import { Messages } from 'common/messages'; import { ManualTestStatus } from 'common/types/manual-test-status'; +import { InstanceIdToInstanceDataMap } from 'common/types/store-data/assessment-result-data'; import { FeatureFlagStoreData } from 'common/types/store-data/feature-flag-store-data'; import { AssessmentScanData, ScanData } from 'common/types/store-data/visualization-store-data'; import { @@ -52,6 +53,15 @@ export class AssessmentBuilder { requirement.getInstanceStatus = AssessmentBuilder.getInstanceStatus; } + if (!requirement.getInitialManualTestStatus) { + requirement.getInitialManualTestStatus = AssessmentBuilder.getInitialManualTestStatus; + } + + if (!requirement.isVisualizationSupportedForResult) { + requirement.isVisualizationSupportedForResult = + AssessmentBuilder.isVisualizationSupportedForResult; + } + if (!requirement.getInstanceStatusColumns) { requirement.getInstanceStatusColumns = AssessmentBuilder.getInstanceStatusColumns; } @@ -75,6 +85,16 @@ export class AssessmentBuilder { return ManualTestStatus.UNKNOWN; } + private static getInitialManualTestStatus( + instances: InstanceIdToInstanceDataMap, + ): ManualTestStatus { + return ManualTestStatus.UNKNOWN; + } + + private static isVisualizationSupportedForResult(result: DecoratedAxeNodeResult): boolean { + return true; + } + private static getInstanceStatusColumns(): Readonly[] { return [ { diff --git a/src/assessments/landmarks/auto-pass-if-no-landmarks.ts b/src/assessments/landmarks/auto-pass-if-no-landmarks.ts new file mode 100644 index 00000000000..4a2d28673a8 --- /dev/null +++ b/src/assessments/landmarks/auto-pass-if-no-landmarks.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { ManualTestStatus } from 'common/types/manual-test-status'; +import { InstanceIdToInstanceDataMap } from 'common/types/store-data/assessment-result-data'; +import { some } from 'lodash'; + +export function autoPassIfNoLandmarks(instanceData: InstanceIdToInstanceDataMap): ManualTestStatus { + const someInstanceHasLandmarkRole = some( + Object.values(instanceData), + instance => instance.propertyBag != null && instance.propertyBag['role'] != null, + ); + + return someInstanceHasLandmarkRole ? ManualTestStatus.UNKNOWN : ManualTestStatus.PASS; +} diff --git a/src/assessments/landmarks/does-result-have-main-role.ts b/src/assessments/landmarks/does-result-have-main-role.ts new file mode 100644 index 00000000000..5a99b888217 --- /dev/null +++ b/src/assessments/landmarks/does-result-have-main-role.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { DecoratedAxeNodeResult } from 'injected/scanner-utils'; +import { some } from 'lodash'; + +export function doesResultHaveMainRole(result: DecoratedAxeNodeResult): boolean { + // This 'role' data is populated by the unique-landmark rule, which considers + // both explicit role attributes and implicit roles based on tag name + return ( + some(result.any, checkResult => checkResult.data['role'] === 'main') || + some(result.all, checkResult => checkResult.data['role'] === 'main') + ); +} diff --git a/src/assessments/landmarks/test-steps/no-repeating-content.tsx b/src/assessments/landmarks/test-steps/no-repeating-content.tsx index 9a0a84860b5..f10f2c03485 100644 --- a/src/assessments/landmarks/test-steps/no-repeating-content.tsx +++ b/src/assessments/landmarks/test-steps/no-repeating-content.tsx @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as React from 'react'; +import { doesResultHaveMainRole } from 'assessments/landmarks/does-result-have-main-role'; import { VisualizationType } from 'common/types/visualization-type'; import { link } from 'content/link'; import * as content from 'content/test/landmarks/no-repeating-content'; @@ -10,10 +11,14 @@ import { AnalyzerConfigurationFactory } from '../../common/analyzer-configuratio import { ManualTestRecordYourResults } from '../../common/manual-test-record-your-results'; import * as Markup from '../../markup'; import { Requirement } from '../../types/requirement'; +import { autoPassIfNoLandmarks } from '../auto-pass-if-no-landmarks'; import { LandmarkTestStep } from './test-steps'; const description: JSX.Element = ( - The main landmark must not contain any blocks of content that repeat across pages. + + The main landmark must not contain any blocks of content + that repeat across pages. + ); const howToTest: JSX.Element = ( @@ -22,10 +27,33 @@ const howToTest: JSX.Element = ( The visual helper for this requirement highlights the page's{' '} main landmark.

+

+ + Note: If no landmarks are found, this requirement will automatically be marked as + pass. + +

  1. - In the target page, examine the main landmark to verify that it does not contain any - blocks of content that repeat across pages (e.g., site-wide navigation links). +

    Examine the target page to verify that all of the following are true:

    +
      +
    1. + The page has exactly one main landmark, + and +
    2. +
    3. + The main landmark does not contain any + blocks of content that repeat across pages (such as site-wide navigation + links). +
    4. +
    +

    + Exception: If a page has nested document or{' '} + application roles (typically applied to{' '} + or elements), + each nested document or application may also{' '} + have one main landmark. +

@@ -38,11 +66,13 @@ export const NoRepeatingContent: Requirement = { description, howToTest, isManual: true, + getInitialManualTestStatus: autoPassIfNoLandmarks, + isVisualizationSupportedForResult: doesResultHaveMainRole, guidanceLinks: [link.WCAG_1_3_1, link.WCAG_2_4_1], getAnalyzer: provider => provider.createRuleAnalyzer( AnalyzerConfigurationFactory.forScanner({ - rules: ['main-landmark'], + rules: ['unique-landmark'], key: LandmarkTestStep.noRepeatingContent, testType: VisualizationType.LandmarksAssessment, }), diff --git a/src/assessments/landmarks/test-steps/primary-content.tsx b/src/assessments/landmarks/test-steps/primary-content.tsx index 6f13b711e4f..f76a938feac 100644 --- a/src/assessments/landmarks/test-steps/primary-content.tsx +++ b/src/assessments/landmarks/test-steps/primary-content.tsx @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as React from 'react'; +import { doesResultHaveMainRole } from 'assessments/landmarks/does-result-have-main-role'; import { VisualizationType } from 'common/types/visualization-type'; import { link } from 'content/link'; import * as content from 'content/test/landmarks/primary-content'; @@ -10,19 +11,48 @@ import { AnalyzerConfigurationFactory } from '../../common/analyzer-configuratio import { ManualTestRecordYourResults } from '../../common/manual-test-record-your-results'; import * as Markup from '../../markup'; import { Requirement } from '../../types/requirement'; +import { autoPassIfNoLandmarks } from '../auto-pass-if-no-landmarks'; import { LandmarkTestStep } from './test-steps'; const description: JSX.Element = ( - The main landmark must contain all of the page's primary content. + + The main landmark must contain all of the page's primary + content. + ); const howToTest: JSX.Element = (
-

The visual helper for this requirement highlights the page's main landmark.

+

+ The visual helper for this requirement highlights the page's{' '} + main landmark. +

+

+ + Note: If no landmarks are found, this requirement will automatically be marked as + pass. + +

  1. - In the target page, examine the main landmark to - verify that it contains all of the page's primary content. +

    Examine the target page to verify that all of the following are true:

    +
      +
    1. + The page has exactly one main landmark, + and +
    2. +
    3. + The main landmark contains all of the + page's primary content. +
    4. +
    +

    + Exception: If a page has nested document or{' '} + application roles (typically applied to{' '} + or elements), + each nested document or application may also{' '} + have one main landmark. +

@@ -35,11 +65,13 @@ export const PrimaryContent: Requirement = { description, howToTest, isManual: true, + getInitialManualTestStatus: autoPassIfNoLandmarks, + isVisualizationSupportedForResult: doesResultHaveMainRole, guidanceLinks: [link.WCAG_1_3_1, link.WCAG_2_4_1], getAnalyzer: provider => provider.createRuleAnalyzer( AnalyzerConfigurationFactory.forScanner({ - rules: ['main-landmark'], + rules: ['unique-landmark'], key: LandmarkTestStep.primaryContent, testType: VisualizationType.LandmarksAssessment, }), diff --git a/src/assessments/types/requirement.ts b/src/assessments/types/requirement.ts index 2656094ff08..8ccb7fc4560 100644 --- a/src/assessments/types/requirement.ts +++ b/src/assessments/types/requirement.ts @@ -5,6 +5,7 @@ import { ManualTestStatus } from 'common/types/manual-test-status'; import { AssessmentNavState, GeneratedAssessmentInstance, + InstanceIdToInstanceDataMap, } from 'common/types/store-data/assessment-result-data'; import { FeatureFlagStoreData } from 'common/types/store-data/feature-flag-store-data'; import { DetailsViewActionMessageCreator } from 'DetailsView/actions/details-view-action-message-creator'; @@ -39,10 +40,17 @@ export interface Requirement { addFailureInstruction?: string; infoAndExamples?: ContentPageComponent; isManual: boolean; + // This is for semi-manual cases where we can't present a list of instances like an assisted + // test would, but can infer a PASS or FAIL state. If not specified, acts like () => UNKNOWN. + getInitialManualTestStatus?: (instances: InstanceIdToInstanceDataMap) => ManualTestStatus; guidanceLinks: HyperlinkDefinition[]; columnsConfig?: InstanceTableColumn[]; getAnalyzer?: (provider: AnalyzerProvider) => Analyzer; getVisualHelperToggle?: (props: VisualHelperToggleConfig) => JSX.Element; + // Any results this returns false for will be omitted from visual helper displays, but still + // present for the purposes of instance lists or getInitialManualTestStatus. By default, all + // results support visualization. + isVisualizationSupportedForResult?: (result: DecoratedAxeNodeResult) => boolean; visualizationInstanceProcessor?: VisualizationInstanceProcessorCallback< PropertyBags, PropertyBags diff --git a/src/background/assessment-data-converter.ts b/src/background/assessment-data-converter.ts index 0c5c7d24522..edc68817be1 100644 --- a/src/background/assessment-data-converter.ts +++ b/src/background/assessment-data-converter.ts @@ -29,6 +29,7 @@ export class AssessmentDataConverter { stepName: string, generateInstanceIdentifier: (instance: UniquelyIdentifiableInstances) => string, getInstanceStatus: (result: DecoratedAxeNodeResult) => ManualTestStatus, + isVisualizationSupported: (result: DecoratedAxeNodeResult) => boolean, ): AssessmentInstancesMap { let instancesMap: AssessmentInstancesMap = {}; @@ -51,6 +52,7 @@ export class AssessmentDataConverter { stepName, ruleResult, getInstanceStatus, + isVisualizationSupported, ); } }); @@ -98,6 +100,7 @@ export class AssessmentDataConverter { testStep: string, ruleResult: DecoratedAxeNodeResult, getInstanceStatus: (result: DecoratedAxeNodeResult) => ManualTestStatus, + isVisualizationSupported: (result: DecoratedAxeNodeResult) => boolean, ): GeneratedAssessmentInstance { const target: string[] = elementAxeResult.target; let testStepResults = {}; @@ -114,6 +117,7 @@ export class AssessmentDataConverter { ruleResult, elementAxeResult, getInstanceStatus, + isVisualizationSupported, ); let actualPropertyBag = { @@ -163,6 +167,7 @@ export class AssessmentDataConverter { status: ManualTestStatus.UNKNOWN, isCapturedByUser: false, failureSummary: null, + isVisualizationSupported: true, isVisualizationEnabled: true, isVisible: true, }; @@ -172,12 +177,14 @@ export class AssessmentDataConverter { ruleResult: DecoratedAxeNodeResult, elementAxeResult: HtmlElementAxeResults, getInstanceStatus: (result: DecoratedAxeNodeResult) => ManualTestStatus, + isVisualizationSupported: (result: DecoratedAxeNodeResult) => boolean, ): TestStepResult { return { id: ruleResult.id, status: getInstanceStatus(ruleResult), isCapturedByUser: false, failureSummary: ruleResult.failureSummary, + isVisualizationSupported: isVisualizationSupported(ruleResult), isVisualizationEnabled: false, isVisible: true, }; diff --git a/src/background/stores/assessment-store.ts b/src/background/stores/assessment-store.ts index e8f9e7d7ce9..a2a5e4108d8 100644 --- a/src/background/stores/assessment-store.ts +++ b/src/background/stores/assessment-store.ts @@ -11,6 +11,7 @@ import { AssessmentData, AssessmentStoreData, GeneratedAssessmentInstance, + InstanceIdToInstanceDataMap, TestStepResult, UserCapturedInstance, } from 'common/types/store-data/assessment-result-data'; @@ -272,16 +273,17 @@ export class AssessmentStore extends BaseStoreImpl { private onChangeAssessmentVisualizationStateForAll = ( payload: ChangeInstanceSelectionPayload, ): void => { - const config = this.assessmentsProvider - .forType(payload.test) - .getVisualizationConfiguration(); + const { test, requirement } = payload; + const config = this.assessmentsProvider.forType(test).getVisualizationConfiguration(); const assessmentDataMap = config.getAssessmentData(this.state) .generatedAssessmentInstancesMap; + forEach(assessmentDataMap, val => { - const stepResult = val.testStepResults[payload.requirement]; + const stepResult = val.testStepResults[requirement]; if (stepResult != null) { - stepResult.isVisualizationEnabled = payload.isVisualizationEnabled; + stepResult.isVisualizationEnabled = + stepResult.isVisualizationSupported && payload.isVisualizationEnabled; } }); @@ -318,15 +320,14 @@ export class AssessmentStore extends BaseStoreImpl { private onChangeAssessmentVisualizationState = ( payload: ChangeInstanceSelectionPayload, ): void => { - const config = this.assessmentsProvider - .forType(payload.test) - .getVisualizationConfiguration(); + const { test, requirement } = payload; + const config = this.assessmentsProvider.forType(test).getVisualizationConfiguration(); const assessmentData = config.getAssessmentData(this.state); - const stepResult: TestStepResult = - assessmentData.generatedAssessmentInstancesMap[payload.selector].testStepResults[ - payload.requirement - ]; - stepResult.isVisualizationEnabled = payload.isVisualizationEnabled; + const instance = assessmentData.generatedAssessmentInstancesMap[payload.selector]; + const stepResult: TestStepResult = instance.testStepResults[requirement]; + + stepResult.isVisualizationEnabled = + stepResult.isVisualizationSupported && payload.isVisualizationEnabled; this.emitChanged(); }; @@ -382,6 +383,7 @@ export class AssessmentStore extends BaseStoreImpl { step, config.getInstanceIdentiferGenerator(step), stepConfig.getInstanceStatus, + stepConfig.isVisualizationSupportedForResult, ); assessmentData.generatedAssessmentInstancesMap = generatedAssessmentInstancesMap; assessmentData.testStepStatus[step].isStepScanned = true; @@ -432,12 +434,38 @@ export class AssessmentStore extends BaseStoreImpl { testStepName: string, testType: VisualizationType, ): void { - const isManual = this.assessmentsProvider.getStep(testType, testStepName).isManual; - if (isManual !== true) { + const step = this.assessmentsProvider.getStep(testType, testStepName); + const { isManual, getInitialManualTestStatus } = step; + + if (isManual) { + this.applyInitialManualTestStatus( + assessmentData, + testStepName, + testType, + getInitialManualTestStatus, + ); + } else { this.updateTestStepStatusForGeneratedInstances(assessmentData, testStepName); } } + private applyInitialManualTestStatus( + assessmentData: AssessmentData, + testStepName: string, + testType: VisualizationType, + getInitialManualTestStatus: (InstanceIdToInstanceDataMap) => ManualTestStatus, + ): void { + const originalStatus = assessmentData.manualTestStepResultMap[testStepName].status; + if (originalStatus !== ManualTestStatus.UNKNOWN) { + return; // Never override an explicitly set status + } + + const instanceMap = assessmentData.generatedAssessmentInstancesMap; + const status = getInitialManualTestStatus(instanceMap); + assessmentData.manualTestStepResultMap[testStepName].status = status; + this.updateManualTestStepStatus(assessmentData, testStepName, testType); + } + private getGroupResult( instanceMap: DictionaryStringTo, testStepName: string, diff --git a/src/common/types/store-data/assessment-result-data.ts b/src/common/types/store-data/assessment-result-data.ts index a44c441affa..f90213f4da2 100644 --- a/src/common/types/store-data/assessment-result-data.ts +++ b/src/common/types/store-data/assessment-result-data.ts @@ -61,6 +61,7 @@ export interface TestStepResult { status: ManualTestStatus; isCapturedByUser: boolean; failureSummary: string; + isVisualizationSupported: boolean; isVisualizationEnabled: boolean; isVisible: boolean; originalStatus?: ManualTestStatus; diff --git a/src/scanner/custom-rule-configurations.ts b/src/scanner/custom-rule-configurations.ts index 4791b40d7c2..481f380563f 100644 --- a/src/scanner/custom-rule-configurations.ts +++ b/src/scanner/custom-rule-configurations.ts @@ -10,7 +10,6 @@ import { frameTitleConfiguration } from './custom-rules/frame-title'; import { headerRuleConfiguration } from './custom-rules/header-rule'; import { headingConfiguration } from './custom-rules/heading-rule'; import { imageConfiguration } from './custom-rules/image-rule'; -import { landmarkConfiguration } from './custom-rules/landmark-rule'; import { linkFunctionConfiguration } from './custom-rules/link-function'; import { linkPurposeConfiguration } from './custom-rules/link-purpose'; import { nativeWidgetsDefaultConfiguration } from './custom-rules/native-widgets-default'; @@ -24,7 +23,6 @@ import { RuleConfiguration } from './iruleresults'; export const configuration: RuleConfiguration[] = [ headingConfiguration, colorConfiguration, - landmarkConfiguration, uniqueLandmarkConfiguration, imageConfiguration, textAlternativeConfiguration, diff --git a/src/scanner/custom-rules/landmark-rule.ts b/src/scanner/custom-rules/landmark-rule.ts deleted file mode 100644 index 778f201e4a1..00000000000 --- a/src/scanner/custom-rules/landmark-rule.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { RuleConfiguration } from '../iruleresults'; - -const landmarkCheckId: string = 'unique-landmark'; - -export const landmarkConfiguration: RuleConfiguration = { - checks: [], - rule: { - id: 'main-landmark', - selector: '[role=main], main', - any: [landmarkCheckId], - enabled: false, - }, -}; diff --git a/src/tests/unit/common/visual-helper-toggle-config-builder.ts b/src/tests/unit/common/visual-helper-toggle-config-builder.ts index 23a86eb2aec..065653bf9b5 100644 --- a/src/tests/unit/common/visual-helper-toggle-config-builder.ts +++ b/src/tests/unit/common/visual-helper-toggle-config-builder.ts @@ -52,6 +52,7 @@ export class VisualHelperToggleConfigBuilder extends BaseDataBuilder, } as GeneratedAssessmentInstance, @@ -68,7 +70,8 @@ export class VisualHelperToggleConfigBuilder extends BaseDataBuilder, } as GeneratedAssessmentInstance, @@ -102,4 +105,29 @@ export class VisualHelperToggleConfigBuilder extends BaseDataBuilder, + } as GeneratedAssessmentInstance, + 'selector-2': { + testStepResults: { + [this.stepKey]: { + id: 'id1', + status: ManualTestStatus.PASS, + isVisualizationEnabled: false, + isVisualizationSupported: false, + } as TestStepResult, + } as AssessmentResultType, + } as GeneratedAssessmentInstance, + }; + return this; + } } diff --git a/src/tests/unit/tests/DetailsView/components/assessment-visualization-enabled-toggle.test.tsx b/src/tests/unit/tests/DetailsView/components/assessment-visualization-enabled-toggle.test.tsx index e9b8e86d8a1..99a457b0984 100644 --- a/src/tests/unit/tests/DetailsView/components/assessment-visualization-enabled-toggle.test.tsx +++ b/src/tests/unit/tests/DetailsView/components/assessment-visualization-enabled-toggle.test.tsx @@ -1,200 +1,234 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as Enzyme from 'enzyme'; -import * as React from 'react'; -import { IMock, Mock, Times } from 'typemoq'; import { VisualizationToggle, VisualizationToggleProps, -} from '../../../../../common/components/visualization-toggle'; -import { DetailsViewActionMessageCreator } from '../../../../../DetailsView/actions/details-view-action-message-creator'; -import { AssessmentVisualizationEnabledToggle } from '../../../../../DetailsView/components/assessment-visualization-enabled-toggle'; -import { visualHelperText } from '../../../../../DetailsView/components/base-visual-helper-toggle'; -import { VisualHelperToggleConfigBuilder } from '../../../common/visual-helper-toggle-config-builder'; -import { VisualizationTogglePropsBuilder } from '../../../common/visualization-toggle-props-builder'; +} from 'common/components/visualization-toggle'; +import { DetailsViewActionMessageCreator } from 'DetailsView/actions/details-view-action-message-creator'; +import { AssessmentVisualizationEnabledToggle } from 'DetailsView/components/assessment-visualization-enabled-toggle'; +import { visualHelperText } from 'DetailsView/components/base-visual-helper-toggle'; +import * as Enzyme from 'enzyme'; +import * as React from 'react'; +import { VisualHelperToggleConfigBuilder } from 'tests/unit/common/visual-helper-toggle-config-builder'; +import { VisualizationTogglePropsBuilder } from 'tests/unit/common/visualization-toggle-props-builder'; +import { IMock, Mock, Times } from 'typemoq'; describe('AssessmentVisualizationEnabledToggle', () => { const actionMessageCreatorMock: IMock = Mock.ofType( DetailsViewActionMessageCreator, ); - it('render with disabled message', () => { - const props = new VisualHelperToggleConfigBuilder() - .withToggleStepEnabled(true) - .withToggleStepScanned(false) - .withActionMessageCreator(actionMessageCreatorMock.object) - .withEmptyFilteredMap() - .build(); + describe('render', () => { + it('is disabled when no instances exist', () => { + const props = new VisualHelperToggleConfigBuilder() + .withToggleStepEnabled(true) + .withToggleStepScanned(false) + .withActionMessageCreator(actionMessageCreatorMock.object) + .withEmptyFilteredMap() + .build(); - const wrapper = Enzyme.shallow(); + const testSubject = Enzyme.shallow(); - const visualHelperClass = 'visual-helper'; - const toggleDiv = wrapper.find(`.${visualHelperClass}`); + expectDisabledTextLayout(testSubject); - expect(toggleDiv.exists()).toBeTruthy(); + const expectedToggleProps = getDefaultVisualizationTogglePropsBuilder() + .with('checked', false) + .with('disabled', true) + .build(); - const textDiv = toggleDiv.find(`.${visualHelperClass}-text`); + expectChildVisualizationToggleWith(expectedToggleProps, testSubject); + }); - expect(textDiv.exists()).toBeTruthy(); - expect(textDiv.childAt(0).text()).toEqual(visualHelperText); + it("is disabled when only instances that don't support visualization exist", () => { + const props = new VisualHelperToggleConfigBuilder() + .withToggleStepEnabled(true) + .withToggleStepScanned(false) + .withActionMessageCreator(actionMessageCreatorMock.object) + .withNonEmptyFilteredMap(false, false) + .build(); - const noMatchesWarningClass = 'no-matching-elements'; - expect(wrapper.find(`.${noMatchesWarningClass}`).exists()).toBeTruthy(); + const testSubject = Enzyme.shallow(); - const toggle = wrapper.find(VisualizationToggle); + expectDisabledTextLayout(testSubject); - const expectedToggleProps = getDefaultVisualizationTogglePropsBuilder() - .with('checked', false) - .with('disabled', true) - .build(); + const expectedToggleProps = getDefaultVisualizationTogglePropsBuilder() + .with('checked', false) + .with('disabled', true) + .build(); - assertVisualizationToggle(expectedToggleProps, toggle); - }); + expectChildVisualizationToggleWith(expectedToggleProps, testSubject); + }); - it('render: toggle not disabled', () => { - const props = new VisualHelperToggleConfigBuilder() - .withToggleStepEnabled(true) - .withToggleStepScanned(false) - .withActionMessageCreator(actionMessageCreatorMock.object) - .withNonEmptyFilteredMap() - .build(); + it('is enabled but unchecked if step is enabled but all instance visualizations are disabled', () => { + const props = new VisualHelperToggleConfigBuilder() + .withToggleStepEnabled(true) + .withToggleStepScanned(false) + .withActionMessageCreator(actionMessageCreatorMock.object) + .withNonEmptyFilteredMap(false) + .build(); - const wrapper = Enzyme.shallow(); + const testSubject = Enzyme.shallow(); - const visualHelperClass = 'visual-helper'; - const toggleDiv = wrapper.find(`.${visualHelperClass}`); + expectEnabledTextLayout(testSubject); - expect(toggleDiv.exists()).toBeTruthy(); + const expectedToggleProps = getDefaultVisualizationTogglePropsBuilder() + .with('checked', false) + .with('disabled', false) + .build(); - const textDiv = toggleDiv.find(`.${visualHelperClass}-text`); + expectChildVisualizationToggleWith(expectedToggleProps, testSubject); + }); - expect(textDiv.exists()).toBeTruthy(); - expect(textDiv.childAt(0).text()).toEqual(visualHelperText); - expect(wrapper.find('strong').exists()).toBeFalsy(); - const toggle = wrapper.find(VisualizationToggle); + it('is enabled and checked if step and instance visualizations are both enabled', () => { + const props = new VisualHelperToggleConfigBuilder() + .withToggleStepEnabled(false) + .withToggleStepScanned(false) + .withActionMessageCreator(actionMessageCreatorMock.object) + .withNonEmptyFilteredMap(true) + .build(); - const expectedToggleProps = getDefaultVisualizationTogglePropsBuilder() - .with('checked', false) - .with('disabled', false) - .build(); + const testSubject = Enzyme.shallow(); - assertVisualizationToggle(expectedToggleProps, toggle); - }); + expectEnabledTextLayout(testSubject); - it('render: have non empty instance map with a visible instance', () => { - const props = new VisualHelperToggleConfigBuilder() - .withToggleStepEnabled(false) - .withToggleStepScanned(false) - .withActionMessageCreator(actionMessageCreatorMock.object) - .withNonEmptyFilteredMap(true) - .build(); + const expectedToggleProps = getDefaultVisualizationTogglePropsBuilder() + .with('checked', true) + .with('disabled', false) + .build(); - const wrapper = Enzyme.shallow(); + expectChildVisualizationToggleWith(expectedToggleProps, testSubject); + }); - const visualHelperClass = 'visual-helper'; - const toggleDiv = wrapper.find(`.${visualHelperClass}`); + it("is enabled and checked if some instances support visualization and some don't", () => { + const props = new VisualHelperToggleConfigBuilder() + .withToggleStepEnabled(false) + .withToggleStepScanned(false) + .withActionMessageCreator(actionMessageCreatorMock.object) + .withMixedVisualizationSupportFilteredMap() + .build(); - expect(toggleDiv.exists()).toBeTruthy(); + const testSubject = Enzyme.shallow(); - const textDiv = toggleDiv.find(`.${visualHelperClass}-text`); + expectEnabledTextLayout(testSubject); - expect(textDiv.exists()).toBeTruthy(); - expect(textDiv.childAt(0).text()).toEqual(visualHelperText); - expect(wrapper.find('strong').exists()).toBeFalsy(); + const expectedToggleProps = getDefaultVisualizationTogglePropsBuilder() + .with('checked', true) + .with('disabled', false) + .build(); - const toggle = wrapper.find(VisualizationToggle); + expectChildVisualizationToggleWith(expectedToggleProps, testSubject); + }); - const expectedToggleProps = getDefaultVisualizationTogglePropsBuilder() - .with('checked', true) - .with('disabled', false) - .build(); + it('is enabled and unchecked if step and instance visualizations are both disabled', () => { + const props = new VisualHelperToggleConfigBuilder() + .withToggleStepEnabled(false) + .withToggleStepScanned(false) + .withActionMessageCreator(actionMessageCreatorMock.object) + .withNonEmptyFilteredMap(false) + .build(); - assertVisualizationToggle(expectedToggleProps, toggle); - }); + const testSubject = Enzyme.shallow(); - it('render: have non empty instance map without a visible instance', () => { - const props = new VisualHelperToggleConfigBuilder() - .withToggleStepEnabled(false) - .withToggleStepScanned(false) - .withActionMessageCreator(actionMessageCreatorMock.object) - .withNonEmptyFilteredMap() - .build(); + expectEnabledTextLayout(testSubject); - const wrapper = Enzyme.shallow(); + const expectedToggleProps = getDefaultVisualizationTogglePropsBuilder() + .with('checked', false) + .with('disabled', false) + .build(); - const visualHelperClass = 'visual-helper'; - const toggleDiv = wrapper.find(`.${visualHelperClass}`); + expectChildVisualizationToggleWith(expectedToggleProps, testSubject); + }); - expect(toggleDiv.exists()).toBeTruthy(); + function expectEnabledTextLayout( + testSubject: Enzyme.ShallowWrapper, + ): void { + const visualHelperClass = 'visual-helper'; + const toggleDiv = testSubject.find(`.${visualHelperClass}`); - const textDiv = toggleDiv.find(`.${visualHelperClass}-text`); + expect(toggleDiv.exists()).toBeTruthy(); - expect(textDiv.exists()).toBeTruthy(); - expect(textDiv.childAt(0).text()).toEqual(visualHelperText); - expect(wrapper.find('strong').exists()).toBeFalsy(); - const toggle = wrapper.find(VisualizationToggle); + const textDiv = toggleDiv.find(`.${visualHelperClass}-text`); - const expectedToggleProps = getDefaultVisualizationTogglePropsBuilder() - .with('checked', false) - .with('disabled', false) - .build(); + expect(textDiv.exists()).toBeTruthy(); + expect(textDiv.childAt(0).text()).toEqual(visualHelperText); + expect(testSubject.find('strong').exists()).toBeFalsy(); + } - assertVisualizationToggle(expectedToggleProps, toggle); - }); + function expectDisabledTextLayout( + testSubject: Enzyme.ShallowWrapper, + ): void { + const visualHelperClass = 'visual-helper'; + const toggleDiv = testSubject.find(`.${visualHelperClass}`); + + expect(toggleDiv.exists()).toBeTruthy(); - it('enables all visualizations when none are shown', () => { - const props = new VisualHelperToggleConfigBuilder() - .withToggleStepEnabled(true) - .withToggleStepScanned(false) - .withActionMessageCreator(actionMessageCreatorMock.object) - .build(); - - const wrapper = Enzyme.shallow(); - actionMessageCreatorMock.reset(); - actionMessageCreatorMock - .setup(acm => - acm.changeAssessmentVisualizationStateForAll( - true, - props.assessmentNavState.selectedTestType, - props.assessmentNavState.selectedTestSubview, - ), - ) - .verifiable(Times.once()); - - wrapper.find(VisualizationToggle).simulate('click'); - - actionMessageCreatorMock.verifyAll(); + const textDiv = toggleDiv.find(`.${visualHelperClass}-text`); + + expect(textDiv.exists()).toBeTruthy(); + expect(textDiv.childAt(0).text()).toEqual(visualHelperText); + + const noMatchesWarningClass = 'no-matching-elements'; + expect(testSubject.find(`.${noMatchesWarningClass}`).exists()).toBeTruthy(); + } }); - it('disables all visualizations when some are shown', () => { - const props = new VisualHelperToggleConfigBuilder() - .withToggleStepEnabled(true) - .withToggleStepScanned(false) - .withActionMessageCreator(actionMessageCreatorMock.object) - .withNonEmptyFilteredMap(true) - .build(); - - const wrapper = Enzyme.shallow(); - actionMessageCreatorMock.reset(); - actionMessageCreatorMock - .setup(acm => - acm.changeAssessmentVisualizationStateForAll( - false, - props.assessmentNavState.selectedTestType, - props.assessmentNavState.selectedTestSubview, - ), - ) - .verifiable(Times.once()); - - wrapper.find(VisualizationToggle).simulate('click'); - - actionMessageCreatorMock.verifyAll(); + describe('toggle behavior', () => { + it('enables all visualizations when none are shown', () => { + const props = new VisualHelperToggleConfigBuilder() + .withToggleStepEnabled(true) + .withToggleStepScanned(false) + .withActionMessageCreator(actionMessageCreatorMock.object) + .build(); + + const wrapper = Enzyme.shallow(); + actionMessageCreatorMock.reset(); + actionMessageCreatorMock + .setup(acm => + acm.changeAssessmentVisualizationStateForAll( + true, + props.assessmentNavState.selectedTestType, + props.assessmentNavState.selectedTestSubview, + ), + ) + .verifiable(Times.once()); + + wrapper.find(VisualizationToggle).simulate('click'); + + actionMessageCreatorMock.verifyAll(); + }); + + it('disables all visualizations when some are shown', () => { + const props = new VisualHelperToggleConfigBuilder() + .withToggleStepEnabled(true) + .withToggleStepScanned(false) + .withActionMessageCreator(actionMessageCreatorMock.object) + .withNonEmptyFilteredMap(true) + .build(); + + const wrapper = Enzyme.shallow(); + actionMessageCreatorMock.reset(); + actionMessageCreatorMock + .setup(acm => + acm.changeAssessmentVisualizationStateForAll( + false, + props.assessmentNavState.selectedTestType, + props.assessmentNavState.selectedTestSubview, + ), + ) + .verifiable(Times.once()); + + wrapper.find(VisualizationToggle).simulate('click'); + + actionMessageCreatorMock.verifyAll(); + }); }); - function assertVisualizationToggle( + function expectChildVisualizationToggleWith( expectedProps: VisualizationToggleProps, - visualizationToggle: Enzyme.ShallowWrapper, + testSubject: Enzyme.ShallowWrapper, ): void { + const visualizationToggle = testSubject.find(VisualizationToggle); + expect(visualizationToggle.exists()).toBeTruthy(); const actualProps = visualizationToggle.props(); diff --git a/src/tests/unit/tests/assessments/landmarks/auto-pass-if-no-landmarks.test.ts b/src/tests/unit/tests/assessments/landmarks/auto-pass-if-no-landmarks.test.ts new file mode 100644 index 00000000000..ece051ee4ac --- /dev/null +++ b/src/tests/unit/tests/assessments/landmarks/auto-pass-if-no-landmarks.test.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { autoPassIfNoLandmarks } from 'assessments/landmarks/auto-pass-if-no-landmarks'; +import { ManualTestStatus } from 'common/types/manual-test-status'; +import { InstanceIdToInstanceDataMap } from 'common/types/store-data/assessment-result-data'; + +describe('autoPassIfNoLandmarks', () => { + it('returns PASS for instance data with no landmarks', () => { + const input = makeInputDataWithLandmarkRoles([]); + expect(autoPassIfNoLandmarks(input)).toBe(ManualTestStatus.PASS); + }); + + it.each(['main', 'complementary'])( + 'returns UNKNOWN for instance data with one %s landmark', + (landmarkRole: string) => { + const input = makeInputDataWithLandmarkRoles([landmarkRole]); + expect(autoPassIfNoLandmarks(input)).toBe(ManualTestStatus.UNKNOWN); + }, + ); + + it('returns UNKNOWN for instance data with multiple landmarks', () => { + const input = makeInputDataWithLandmarkRoles(['main', 'complementary', 'header']); + expect(autoPassIfNoLandmarks(input)).toBe(ManualTestStatus.UNKNOWN); + }); + + function makeInputDataWithLandmarkRoles(landmarkRoles: string[]): InstanceIdToInstanceDataMap { + const data: InstanceIdToInstanceDataMap = {}; + for (const landmarkRole of landmarkRoles) { + data[`#element-with-${landmarkRole}`] = { + target: [`#element-with-${landmarkRole}`], + html: `
`, + propertyBag: { + role: landmarkRole, + }, + testStepResults: {}, + }; + } + return data; + } +}); diff --git a/src/tests/unit/tests/assessments/landmarks/does-result-have-main-role.test.ts b/src/tests/unit/tests/assessments/landmarks/does-result-have-main-role.test.ts new file mode 100644 index 00000000000..83295474935 --- /dev/null +++ b/src/tests/unit/tests/assessments/landmarks/does-result-have-main-role.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { doesResultHaveMainRole } from 'assessments/landmarks/does-result-have-main-role'; +import { DecoratedAxeNodeResult } from 'injected/scanner-utils'; + +describe('doesResultHaveMainRole', () => { + it('returns false for results with no check results', () => { + const input = { any: [], all: [] } as DecoratedAxeNodeResult; + + expect(doesResultHaveMainRole(input)).toBe(false); + }); + + it('returns false for results with check results without role data', () => { + const input = { + any: [makeCheckResultWithRole(undefined)], + all: [makeCheckResultWithRole(undefined)], + } as DecoratedAxeNodeResult; + + expect(doesResultHaveMainRole(input)).toBe(false); + }); + + it('returns false for results with check results with only non-main role data', () => { + const input = { + any: [makeCheckResultWithRole('complementary')], + all: [makeCheckResultWithRole('banner')], + } as DecoratedAxeNodeResult; + + expect(doesResultHaveMainRole(input)).toBe(false); + }); + + it('returns true for results with an "any" check result with "main" role data', () => { + const input = { + any: [makeCheckResultWithRole('main')], + all: [], + } as DecoratedAxeNodeResult; + + expect(doesResultHaveMainRole(input)).toBe(true); + }); + + it('returns true for results with an "all" check result with "main" role data', () => { + const input = { + any: [], + all: [makeCheckResultWithRole('main')], + } as DecoratedAxeNodeResult; + + expect(doesResultHaveMainRole(input)).toBe(true); + }); + + it('returns true for results with mixed check results', () => { + const input = { + any: [makeCheckResultWithRole('banner'), makeCheckResultWithRole(undefined)], + all: [ + makeCheckResultWithRole('main'), + makeCheckResultWithRole('banner'), + makeCheckResultWithRole(undefined), + ], + } as DecoratedAxeNodeResult; + + expect(doesResultHaveMainRole(input)).toBe(true); + }); + + function makeCheckResultWithRole(role: string): FormattedCheckResult { + return { + id: 'test-check-id', + message: 'test check message', + data: { + role, + }, + }; + } +}); diff --git a/src/tests/unit/tests/background/assessment-data-converter.test.ts b/src/tests/unit/tests/background/assessment-data-converter.test.ts index c87b86145f7..911c9c97f5f 100644 --- a/src/tests/unit/tests/background/assessment-data-converter.test.ts +++ b/src/tests/unit/tests/background/assessment-data-converter.test.ts @@ -13,7 +13,7 @@ import { DecoratedAxeNodeResult, HtmlElementAxeResults } from '../../../../injec import { TabStopEvent } from '../../../../injected/tab-stops-listener'; import { DictionaryStringTo } from '../../../../types/common-types'; -describe('AssessmentDataConverterTest', () => { +describe('AssessmentDataConverter', () => { let testSubject: AssessmentDataConverter; const uid: string = 'uid123'; let testStep: string; @@ -22,6 +22,7 @@ describe('AssessmentDataConverterTest', () => { status: ManualTestStatus.UNKNOWN, isCapturedByUser: false, failureSummary: null, + isVisualizationSupported: true, isVisualizationEnabled: true, isVisible: true, }; @@ -39,305 +40,285 @@ describe('AssessmentDataConverterTest', () => { htmlStub = 'some html'; }); - test('generateAssessmentInstancesMap: property bag from any checks', () => { - const expectedPropertyBag = { - someProperty: 1, - }; - - const selectorMap: DictionaryStringTo = { - [selectorStub]: { - ruleResults: { - rule1: { - any: [ - { - id: 'rule1', - data: expectedPropertyBag, - }, - ], - html: htmlStub, - id: 'id1', - status: true, - } as DecoratedAxeNodeResult, + describe('generateAssessmentInstancesMap', () => { + it('should ignore selectors with no rule results.', () => { + const selectorMap: DictionaryStringTo = { + [selectorStub]: { + ruleResults: {}, + target: [selectorStub], }, - target: [selectorStub], - }, - }; - - setupGenerateInstanceIdentifierMock( - { target: [selectorStub], html: htmlStub }, - identifierStub, - ); - const instanceMap = testSubject.generateAssessmentInstancesMap( - null, - selectorMap, - testStep, - generateInstanceIdentifierMock.object, - () => ManualTestStatus.UNKNOWN, - ); - - expect(instanceMap[identifierStub].propertyBag).toEqual(expectedPropertyBag); - }); - - test(`generateAssessmentInstancesMap: previouslyGeneratedInstances is null, - new rule result is not false and any data is there.`, () => { - const selectorMap: DictionaryStringTo = { - [selectorStub]: { - ruleResults: { - rule1: { - any: [ - { - id: 'rule1', - data: { someProperty: 1 }, - }, - ], - html: htmlStub, - id: 'id1', - status: true, - } as DecoratedAxeNodeResult, + }; + const previouslyGeneratedInstances = {}; + const instanceMap = testSubject.generateAssessmentInstancesMap( + previouslyGeneratedInstances, + selectorMap, + testStep, + undefined, + null, + null, + ); + + expect(instanceMap).toEqual(previouslyGeneratedInstances); + }); + + it('should produce an empty map (not null) if previouslyGeneratedInstances is null and there are no rule results to add', () => { + const selectorMap: DictionaryStringTo = { + [selectorStub]: { + ruleResults: {}, + target: [selectorStub], }, - target: [selectorStub], - }, - }; - const expectedResult = { - [identifierStub]: { - html: htmlStub, - propertyBag: { someProperty: 1 }, - target: [selectorStub], - testStepResults: { - [testStep]: { - id: 'id1', - status: ManualTestStatus.UNKNOWN, - isCapturedByUser: false, - failureSummary: undefined, - isVisible: true, - isVisualizationEnabled: false, + }; + const expectedResult = {}; + const instanceMap = testSubject.generateAssessmentInstancesMap( + null, + selectorMap, + testStep, + undefined, + null, + null, + ); + + expect(instanceMap).toEqual(expectedResult); + }); + + it('should create a new instance wrapping the generated result if there is no existing instance matching the selector', () => { + const selectorMap: DictionaryStringTo = { + [selectorStub]: { + ruleResults: { + rule1: { + html: htmlStub, + id: 'id1', + status: false, + } as DecoratedAxeNodeResult, }, + target: [selectorStub], }, - }, - }; - - setupGenerateInstanceIdentifierMock( - { target: [selectorStub], html: htmlStub }, - identifierStub, - ); - const instanceMap = testSubject.generateAssessmentInstancesMap( - null, - selectorMap, - testStep, - generateInstanceIdentifierMock.object, - () => ManualTestStatus.UNKNOWN, - ); - - expect(instanceMap).toEqual(expectedResult); - }); - - test(`generateAssessmentInstancesMap: previouslyGeneratedInstances is null, - new rule result is null (shouldn't happen but covered).`, () => { - const selectorMap: DictionaryStringTo = { - [selectorStub]: { - ruleResults: {}, - target: [selectorStub], - }, - }; - const expectedResult = {}; - const instanceMap = testSubject.generateAssessmentInstancesMap( - null, - selectorMap, - testStep, - undefined, - null, - ); - - expect(instanceMap).toEqual(expectedResult); - }); - - test(`generateAssessmentInstancesMap: previouslyGeneratedInstances is not null, - new rule result is null (shouldn't happen but covered).`, () => { - const selectorMap: DictionaryStringTo = { - [selectorStub]: { - ruleResults: {}, - target: [selectorStub], - }, - }; - const previouslyGeneratedInstances = {}; - const instanceMap = testSubject.generateAssessmentInstancesMap( - previouslyGeneratedInstances, - selectorMap, - testStep, - undefined, - null, - ); - - expect(instanceMap).toEqual(previouslyGeneratedInstances); - }); - - test(`generateAssessmentInstancesMap: previouslyGeneratedInstances is - empty/does not match any new instances and any data is not there`, () => { - const selectorMap: DictionaryStringTo = { - [selectorStub]: { - ruleResults: { - rule1: { - html: htmlStub, - id: 'id1', - status: false, - } as DecoratedAxeNodeResult, + }; + const expectedResult = { + [identifierStub]: { + html: htmlStub, + propertyBag: null, + target: [selectorStub], + testStepResults: { + [testStep]: { + id: 'id1', + status: ManualTestStatus.UNKNOWN, + isCapturedByUser: false, + failureSummary: undefined, + isVisible: true, + isVisualizationEnabled: false, + isVisualizationSupported: true, + }, + }, }, - target: [selectorStub], - }, - }; - const expectedResult = { - [identifierStub]: { - html: htmlStub, - propertyBag: null, - target: [selectorStub], - testStepResults: { - [testStep]: { - id: 'id1', - status: ManualTestStatus.UNKNOWN, - isCapturedByUser: false, - failureSummary: undefined, - isVisible: true, - isVisualizationEnabled: false, + }; + setupGenerateInstanceIdentifierMock( + { target: [selectorStub], html: htmlStub }, + identifierStub, + ); + const instanceMap = testSubject.generateAssessmentInstancesMap( + {}, + selectorMap, + testStep, + generateInstanceIdentifierMock.object, + () => ManualTestStatus.UNKNOWN, + () => true, + ); + + expect(instanceMap).toEqual(expectedResult); + }); + + it("should merge check results' data into the instance's propertyBag", () => { + const expectedPropertyBag = { + someProperty: 1, + }; + + const selectorMap: DictionaryStringTo = { + [selectorStub]: { + ruleResults: { + rule1: { + any: [ + { + id: 'rule1', + data: expectedPropertyBag, + }, + ], + html: htmlStub, + id: 'id1', + status: true, + } as DecoratedAxeNodeResult, }, + target: [selectorStub], }, + }; + + setupGenerateInstanceIdentifierMock( + { target: [selectorStub], html: htmlStub }, + identifierStub, + ); + const instanceMap = testSubject.generateAssessmentInstancesMap( + null, + selectorMap, + testStep, + generateInstanceIdentifierMock.object, + () => ManualTestStatus.UNKNOWN, + () => true, + ); + + expect(instanceMap[identifierStub].propertyBag).toEqual(expectedPropertyBag); + }); + + it.each([ManualTestStatus.FAIL, ManualTestStatus.UNKNOWN, ManualTestStatus.PASS])( + 'should use getInstanceStatus to determine the status of the generated step result', + (getInstanceStatusResult: ManualTestStatus) => { + const selectorMap: DictionaryStringTo = { + [selectorStub]: { + ruleResults: { + rule1: { + html: htmlStub, + id: 'id1', + status: false, + } as DecoratedAxeNodeResult, + }, + target: [selectorStub], + }, + }; + + setupGenerateInstanceIdentifierMock( + { target: [selectorStub], html: htmlStub }, + identifierStub, + ); + const instanceMap = testSubject.generateAssessmentInstancesMap( + {}, + selectorMap, + testStep, + generateInstanceIdentifierMock.object, + () => getInstanceStatusResult, + () => true, + ); + + expect(instanceMap[identifierStub].testStepResults[testStep].status).toEqual( + getInstanceStatusResult, + ); }, - }; - setupGenerateInstanceIdentifierMock( - { target: [selectorStub], html: htmlStub }, - identifierStub, - ); - const instanceMap = testSubject.generateAssessmentInstancesMap( - {}, - selectorMap, - testStep, - generateInstanceIdentifierMock.object, - () => ManualTestStatus.UNKNOWN, ); - expect(instanceMap).toEqual(expectedResult); - }); - - test('generateAssessmentInstancesMap: automated check status should be FAIL', () => { - const selectorMap: DictionaryStringTo = { - [selectorStub]: { - ruleResults: { - rule1: { - html: htmlStub, - id: 'id1', - status: false, - } as DecoratedAxeNodeResult, - }, - target: [selectorStub], - }, - }; - const expectedResult = { - [identifierStub]: { - html: htmlStub, - propertyBag: null, - target: [selectorStub], - testStepResults: { - [testStep]: { - id: 'id1', - status: ManualTestStatus.FAIL, - isCapturedByUser: false, - failureSummary: undefined, - isVisible: true, - isVisualizationEnabled: false, + it.each([true, false])( + 'should use isVisualizationSupported to determine the corresponding property of the generated step result', + (isVisualizationSupportedResult: boolean) => { + const selectorMap: DictionaryStringTo = { + [selectorStub]: { + ruleResults: { + rule1: { + html: htmlStub, + id: 'id1', + status: false, + } as DecoratedAxeNodeResult, + }, + target: [selectorStub], }, - }, + }; + + setupGenerateInstanceIdentifierMock( + { target: [selectorStub], html: htmlStub }, + identifierStub, + ); + const instanceMap = testSubject.generateAssessmentInstancesMap( + {}, + selectorMap, + testStep, + generateInstanceIdentifierMock.object, + () => ManualTestStatus.UNKNOWN, + () => isVisualizationSupportedResult, + ); + + expect( + instanceMap[identifierStub].testStepResults[testStep].isVisualizationSupported, + ).toEqual(isVisualizationSupportedResult); }, - }; - setupGenerateInstanceIdentifierMock( - { target: [selectorStub], html: htmlStub }, - identifierStub, ); - const instanceMap = testSubject.generateAssessmentInstancesMap( - {}, - selectorMap, - testStep, - generateInstanceIdentifierMock.object, - () => ManualTestStatus.FAIL, - ); - - expect(instanceMap).toEqual(expectedResult); - }); - test('generateAssessmentInstancesMap: previouslyGeneratedInstances contains matching instance', () => { - const anotherTestStep = 'another test step'; - const selectorMap: DictionaryStringTo = { - [selectorStub]: { - ruleResults: { - rule1: { - any: [ - { - id: 'rule1', - data: { someProperty: 1 }, - }, - ], - html: htmlStub, - id: 'id1', - status: false, - } as DecoratedAxeNodeResult, - }, - target: [selectorStub], - }, - }; - const previouslyGeneratedInstances: AssessmentInstancesMap = { - [identifierStub]: { - html: htmlStub, - propertyBag: { someProperty: 3 }, - target: [selectorStub], - testStepResults: { - [anotherTestStep]: { - id: 'id2', - status: ManualTestStatus.UNKNOWN, - isCapturedByUser: false, - failureSummary: undefined, - isVisible: true, - isVisualizationEnabled: true, + it('should merge results for a previously seen selector into the existing instance data', () => { + const anotherTestStep = 'another test step'; + const selectorMap: DictionaryStringTo = { + [selectorStub]: { + ruleResults: { + rule1: { + any: [ + { + id: 'rule1', + data: { someProperty: 1 }, + }, + ], + html: htmlStub, + id: 'id1', + status: false, + } as DecoratedAxeNodeResult, }, + target: [selectorStub], }, - }, - }; - const expectedResult = { - [identifierStub]: { - html: htmlStub, - propertyBag: { someProperty: 3 }, - target: [selectorStub], - testStepResults: { - [testStep]: { - id: 'id1', - status: ManualTestStatus.UNKNOWN, - isCapturedByUser: false, - failureSummary: undefined, - isVisible: true, - isVisualizationEnabled: false, + }; + const previouslyGeneratedInstances: AssessmentInstancesMap = { + [identifierStub]: { + html: htmlStub, + propertyBag: { someProperty: 3 }, + target: [selectorStub], + testStepResults: { + [anotherTestStep]: { + id: 'id2', + status: ManualTestStatus.UNKNOWN, + isCapturedByUser: false, + failureSummary: undefined, + isVisible: true, + isVisualizationEnabled: true, + isVisualizationSupported: true, + }, }, - [anotherTestStep]: { - id: 'id2', - status: ManualTestStatus.UNKNOWN, - isCapturedByUser: false, - failureSummary: undefined, - isVisible: true, - isVisualizationEnabled: true, + }, + }; + const expectedResult = { + [identifierStub]: { + html: htmlStub, + propertyBag: { someProperty: 3 }, + target: [selectorStub], + testStepResults: { + [testStep]: { + id: 'id1', + status: ManualTestStatus.UNKNOWN, + isCapturedByUser: false, + failureSummary: undefined, + isVisible: true, + isVisualizationEnabled: false, + isVisualizationSupported: true, + }, + [anotherTestStep]: { + id: 'id2', + status: ManualTestStatus.UNKNOWN, + isCapturedByUser: false, + failureSummary: undefined, + isVisible: true, + isVisualizationEnabled: true, + isVisualizationSupported: true, + }, }, }, - }, - }; - - setupGenerateInstanceIdentifierMock( - { target: [selectorStub], html: htmlStub }, - identifierStub, - ); - const instanceMap = testSubject.generateAssessmentInstancesMap( - previouslyGeneratedInstances, - selectorMap, - testStep, - generateInstanceIdentifierMock.object, - () => ManualTestStatus.UNKNOWN, - ); - - expect(instanceMap).toEqual(expectedResult); + }; + + setupGenerateInstanceIdentifierMock( + { target: [selectorStub], html: htmlStub }, + identifierStub, + ); + const instanceMap = testSubject.generateAssessmentInstancesMap( + previouslyGeneratedInstances, + selectorMap, + testStep, + generateInstanceIdentifierMock.object, + () => ManualTestStatus.UNKNOWN, + () => true, + ); + + expect(instanceMap).toEqual(expectedResult); + }); }); test('generateAssessmentInstancesMapForEvents: previouslyGeneratedInstances is null', () => { diff --git a/src/tests/unit/tests/background/stores/__snapshots__/assessment-store.test.ts.snap b/src/tests/unit/tests/background/stores/__snapshots__/assessment-store.test.ts.snap index 7c92b440b56..217f5139508 100644 --- a/src/tests/unit/tests/background/stores/__snapshots__/assessment-store.test.ts.snap +++ b/src/tests/unit/tests/background/stores/__snapshots__/assessment-store.test.ts.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AssessmentStoreTest onResetAllAssessmentsData 1`] = `"tab with Id 1000 not found"`; +exports[`AssessmentStore onResetAllAssessmentsData 1`] = `"tab with Id 1000 not found"`; -exports[`AssessmentStoreTest onUpdateTargetTabId 1`] = `"tab with Id 1000 not found"`; +exports[`AssessmentStore onUpdateTargetTabId 1`] = `"tab with Id 1000 not found"`; diff --git a/src/tests/unit/tests/background/stores/assessment-store.test.ts b/src/tests/unit/tests/background/stores/assessment-store.test.ts index f87c17fad8a..39776f31617 100644 --- a/src/tests/unit/tests/background/stores/assessment-store.test.ts +++ b/src/tests/unit/tests/background/stores/assessment-store.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { AssessmentsProvider } from 'assessments/types/assessments-provider'; import { Assessment } from 'assessments/types/iassessment'; +import { Requirement } from 'assessments/types/requirement'; import { AddFailureInstancePayload, AddResultDescriptionPayload, @@ -59,7 +60,7 @@ const assessmentKey: string = 'assessment-1'; const requirementKey: string = 'assessment-1-step-1'; const assessmentType = -1 as VisualizationType; -describe('AssessmentStoreTest', () => { +describe('AssessmentStore', () => { let browserMock: IMock; let assessmentDataConverterMock: IMock; let assessmentDataRemoverMock: IMock; @@ -470,7 +471,7 @@ describe('AssessmentStoreTest', () => { .testListenerToBeCalledOnce(initialState, finalState); }); - test('onScanCompleted', () => { + test('onScanCompleted with an assisted requirement', () => { const initialAssessmentData = new AssessmentDataBuilder() .with('testStepStatus', { ['assessment-1-step-1']: getDefaultTestStepData(), @@ -490,16 +491,121 @@ describe('AssessmentStoreTest', () => { const expectedInstanceMap = {}; const stepMapStub = assessmentsProvider.getStepMap(assessmentType); - const stepConfig = assessmentsProvider.getStep(assessmentType, 'assessment-1-step-1'); + const stepConfig: Readonly = { + ...assessmentsProvider.getStep(assessmentType, 'assessment-1-step-1'), + isManual: false, + }; const assessmentData = new AssessmentDataBuilder() .with('generatedAssessmentInstancesMap', expectedInstanceMap) .with('testStepStatus', { + // should PASS because it is a non-manual test with no associated instances ['assessment-1-step-1']: generateTestStepData(ManualTestStatus.PASS, true), + // should stay unchanged because the event/payload is requirement-specific + ['assessment-1-step-2']: getDefaultTestStepData(), + ['assessment-1-step-3']: getDefaultTestStepData(), + }) + .with('scanIncompleteWarnings', []) + .build(); + + const finalState = getStateWithAssessment(assessmentData); + + assessmentsProviderMock + .setup(provider => provider.all()) + .returns(() => assessmentsProvider.all()); + + assessmentsProviderMock + .setup(provider => provider.getStepMap(assessmentType)) + .returns(() => stepMapStub); + + assessmentsProviderMock + .setup(provider => provider.getStep(assessmentType, 'assessment-1-step-1')) + .returns(() => stepConfig); + + assessmentsProviderMock + .setup(provider => provider.forType(payload.testType)) + .returns(() => assessmentMock.object); + + assessmentMock.setup(am => am.getVisualizationConfiguration()).returns(() => configStub); + + getInstanceIdentiferGeneratorMock + .setup(idGetter => idGetter(requirementKey)) + .returns(() => instanceIdentifierGeneratorStub); + + assessmentDataConverterMock + .setup(a => + a.generateAssessmentInstancesMap( + initialAssessmentData.generatedAssessmentInstancesMap, + payload.selectorMap, + requirementKey, + instanceIdentifierGeneratorStub, + stepConfig.getInstanceStatus, + stepConfig.isVisualizationSupportedForResult, + ), + ) + .returns(() => expectedInstanceMap); + + createStoreTesterForAssessmentActions('scanCompleted') + .withActionParam(payload) + .testListenerToBeCalledOnce(initialState, finalState); + }); + + test('onScanCompleted with a manual requirement uses getInitialManualTestStatus to set status', () => { + const initialManualTestStepResult = { + status: ManualTestStatus.UNKNOWN, + id: requirementKey, + instances: [ + { + id: '1', + description: 'aaa', + }, + ], + }; + const initialAssessmentData = new AssessmentDataBuilder() + .with('testStepStatus', { + ['assessment-1-step-1']: getDefaultTestStepData(), + ['assessment-1-step-2']: getDefaultTestStepData(), + ['assessment-1-step-3']: getDefaultTestStepData(), + }) + .with('manualTestStepResultMap', { + [requirementKey]: initialManualTestStepResult, + }) + .build(); + const initialState = getStateWithAssessment(initialAssessmentData); + + const payload: ScanCompletedPayload = { + selectorMap: {}, + scanResult: {} as ScanResults, + testType: assessmentType, + key: requirementKey, + scanIncompleteWarnings: [], + }; + + const expectedInstanceMap = {}; + const stepMapStub = assessmentsProvider.getStepMap(assessmentType); + const stepConfig: Readonly = { + ...assessmentsProvider.getStep(assessmentType, 'assessment-1-step-1'), + isManual: true, + getInitialManualTestStatus: () => ManualTestStatus.FAIL, + }; + + const assessmentData = new AssessmentDataBuilder() + .with('generatedAssessmentInstancesMap', expectedInstanceMap) + .with('testStepStatus', { + // should FAIL based on getInitialManualTestStatus + ['assessment-1-step-1']: generateTestStepData(ManualTestStatus.FAIL, true), + // should stay unchanged because the event/payload is requirement-specific ['assessment-1-step-2']: getDefaultTestStepData(), ['assessment-1-step-3']: getDefaultTestStepData(), }) .with('scanIncompleteWarnings', []) + .with('manualTestStepResultMap', { + [requirementKey]: { + ...initialManualTestStepResult, + // should FAIL based on getInitialManualTestStatus + status: ManualTestStatus.FAIL, + }, + }) .build(); const finalState = getStateWithAssessment(assessmentData); @@ -533,7 +639,108 @@ describe('AssessmentStoreTest', () => { payload.selectorMap, requirementKey, instanceIdentifierGeneratorStub, - It.isAny(), + stepConfig.getInstanceStatus, + stepConfig.isVisualizationSupportedForResult, + ), + ) + .returns(() => expectedInstanceMap); + + createStoreTesterForAssessmentActions('scanCompleted') + .withActionParam(payload) + .testListenerToBeCalledOnce(initialState, finalState); + }); + + test('onScanCompleted with a manual requirement skips getInitialManualTestStatus for requirements that already have a status', () => { + const initialManualTestStepResult = { + status: ManualTestStatus.PASS, + id: requirementKey, + instances: [ + { + id: '1', + description: 'aaa', + }, + ], + }; + const initialAssessmentData = new AssessmentDataBuilder() + .with('testStepStatus', { + ['assessment-1-step-1']: generateTestStepData(ManualTestStatus.PASS, true), + ['assessment-1-step-2']: getDefaultTestStepData(), + ['assessment-1-step-3']: getDefaultTestStepData(), + }) + .with('manualTestStepResultMap', { + [requirementKey]: initialManualTestStepResult, + }) + .build(); + const initialState = getStateWithAssessment(initialAssessmentData); + + const payload: ScanCompletedPayload = { + selectorMap: {}, + scanResult: {} as ScanResults, + testType: assessmentType, + key: requirementKey, + scanIncompleteWarnings: [], + }; + + const expectedInstanceMap = {}; + const stepMapStub = assessmentsProvider.getStepMap(assessmentType); + const stepConfig: Readonly = { + ...assessmentsProvider.getStep(assessmentType, 'assessment-1-step-1'), + isManual: true, + getInitialManualTestStatus: () => ManualTestStatus.FAIL, + }; + + const assessmentData = new AssessmentDataBuilder() + .with('generatedAssessmentInstancesMap', expectedInstanceMap) + .with('testStepStatus', { + // should ignore getInitialManualTestStatus because the original state was not UNKNOWN + ['assessment-1-step-1']: generateTestStepData(ManualTestStatus.PASS, true), + // should stay unchanged because the event/payload is requirement-specific + ['assessment-1-step-2']: getDefaultTestStepData(), + ['assessment-1-step-3']: getDefaultTestStepData(), + }) + .with('scanIncompleteWarnings', []) + .with('manualTestStepResultMap', { + [requirementKey]: { + ...initialManualTestStepResult, + // should ignore getInitialManualTestStatus because the original state was not UNKNOWN + status: ManualTestStatus.PASS, + }, + }) + .build(); + + const finalState = getStateWithAssessment(assessmentData); + + assessmentsProviderMock + .setup(provider => provider.all()) + .returns(() => assessmentsProvider.all()); + + assessmentsProviderMock + .setup(provider => provider.getStepMap(assessmentType)) + .returns(() => stepMapStub); + + assessmentsProviderMock + .setup(provider => provider.getStep(assessmentType, 'assessment-1-step-1')) + .returns(() => stepConfig); + + assessmentsProviderMock + .setup(provider => provider.forType(payload.testType)) + .returns(() => assessmentMock.object); + + assessmentMock.setup(am => am.getVisualizationConfiguration()).returns(() => configStub); + + getInstanceIdentiferGeneratorMock + .setup(idGetter => idGetter(requirementKey)) + .returns(() => instanceIdentifierGeneratorStub); + + assessmentDataConverterMock + .setup(a => + a.generateAssessmentInstancesMap( + initialAssessmentData.generatedAssessmentInstancesMap, + payload.selectorMap, + requirementKey, + instanceIdentifierGeneratorStub, + stepConfig.getInstanceStatus, + stepConfig.isVisualizationSupportedForResult, ), ) .returns(() => expectedInstanceMap); @@ -893,56 +1100,73 @@ describe('AssessmentStoreTest', () => { .testListenerToBeCalledOnce(initialState, finalState); }); - test('on changeAssessmentVisualizationState', () => { - const generatedAssessmentInstancesMap: DictionaryStringTo = { - selector: { - testStepResults: { - [requirementKey]: { - isVisualizationEnabled: true, + test.each` + supportsVisualization | startsEnabled | payloadEnabled | expectedFinalEnabled + ${false} | ${false} | ${true} | ${false} + ${true} | ${false} | ${true} | ${true} + ${true} | ${true} | ${false} | ${false} + ${true} | ${true} | ${true} | ${true} + ${true} | ${false} | ${false} | ${false} + `( + 'on changeAssessmentVisualizationState: supportsVisualization:$supportsVisualization, ' + + 'startsEnabled:$startsEnabled, payloadEnabled:$payloadEnabled -> finalEnabled:$expectedFinalEnabled', + ({ supportsVisualization, startsEnabled, payloadEnabled, expectedFinalEnabled }) => { + const generatedAssessmentInstancesMap: DictionaryStringTo = { + selector: { + testStepResults: { + [requirementKey]: { + isVisualizationEnabled: startsEnabled, + isVisualizationSupported: supportsVisualization, + }, }, - }, - } as any, - }; + } as any, + }; - const assessmentData = new AssessmentDataBuilder() - .with('generatedAssessmentInstancesMap', generatedAssessmentInstancesMap) - .build(); + const assessmentData = new AssessmentDataBuilder() + .with('generatedAssessmentInstancesMap', generatedAssessmentInstancesMap) + .build(); - const initialState = getStateWithAssessment(assessmentData); + const initialState = getStateWithAssessment(assessmentData); - const payload: ChangeInstanceSelectionPayload = { - test: assessmentType, - requirement: requirementKey, - isVisualizationEnabled: true, - selector: 'selector', - }; + const payload: ChangeInstanceSelectionPayload = { + test: assessmentType, + requirement: requirementKey, + isVisualizationEnabled: payloadEnabled, + selector: 'selector', + }; - assessmentsProviderMock - .setup(apm => apm.forType(payload.test)) - .returns(() => assessmentMock.object); + assessmentsProviderMock + .setup(apm => apm.forType(payload.test)) + .returns(() => assessmentMock.object); - assessmentMock.setup(am => am.getVisualizationConfiguration()).returns(() => configStub); + assessmentMock + .setup(am => am.getVisualizationConfiguration()) + .returns(() => configStub); - const expectedInstancesMap = cloneDeep(generatedAssessmentInstancesMap); - expectedInstancesMap.selector.testStepResults[requirementKey].isVisualizationEnabled = true; + const expectedInstancesMap = cloneDeep(generatedAssessmentInstancesMap); + expectedInstancesMap.selector.testStepResults[ + requirementKey + ].isVisualizationEnabled = expectedFinalEnabled; - const expectedAssessment = new AssessmentDataBuilder() - .with('generatedAssessmentInstancesMap', expectedInstancesMap) - .build(); + const expectedAssessment = new AssessmentDataBuilder() + .with('generatedAssessmentInstancesMap', expectedInstancesMap) + .build(); - const finalState = getStateWithAssessment(expectedAssessment); + const finalState = getStateWithAssessment(expectedAssessment); - createStoreTesterForAssessmentActions('changeAssessmentVisualizationState') - .withActionParam(payload) - .testListenerToBeCalledOnce(initialState, finalState); - }); + createStoreTesterForAssessmentActions('changeAssessmentVisualizationState') + .withActionParam(payload) + .testListenerToBeCalledOnce(initialState, finalState); + }, + ); - test('on changeAssessmentVisualizationStateForAll', () => { + test('changeAssessmentVisualizationStateForAll enables all visualizations that support it', () => { const generatedAssessmentInstancesMap: DictionaryStringTo = { selector1: { testStepResults: { [requirementKey]: { isVisualizationEnabled: true, + isVisualizationSupported: true, }, }, } as any, @@ -950,12 +1174,21 @@ describe('AssessmentStoreTest', () => { testStepResults: { [requirementKey]: { isVisualizationEnabled: false, + isVisualizationSupported: false, }, }, } as any, selector3: { testStepResults: {}, } as any, + selector4: { + testStepResults: { + [requirementKey]: { + isVisualizationEnabled: false, + isVisualizationSupported: true, + }, + }, + } as any, }; const assessmentData = new AssessmentDataBuilder() @@ -978,7 +1211,12 @@ describe('AssessmentStoreTest', () => { assessmentMock.setup(am => am.getVisualizationConfiguration()).returns(() => configStub); const expectedInstancesMap = cloneDeep(generatedAssessmentInstancesMap); - expectedInstancesMap.selector2.testStepResults[ + + // Selector 1 shouldn't change because it's already enabled + // Selector 2 shouldn't change because it doesn't support visualizations + // Selector 3 shouldn't change because it has no test step results + // Selector 4 should toggle from disabled to enabled: + expectedInstancesMap.selector4.testStepResults[ requirementKey ].isVisualizationEnabled = true; diff --git a/src/tests/unit/tests/scanner/custom-rules/landmark-role.test.ts b/src/tests/unit/tests/scanner/custom-rules/landmark-role.test.ts deleted file mode 100644 index 12e0a14e3a7..00000000000 --- a/src/tests/unit/tests/scanner/custom-rules/landmark-role.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { landmarkConfiguration } from '../../../../../scanner/custom-rules/landmark-rule'; - -describe('landmarkRule', () => { - describe('verify landmark configs', () => { - it('should have correct props', () => { - expect(landmarkConfiguration.rule.id).toBe('main-landmark'); - expect(landmarkConfiguration.rule.selector).toBe('[role=main], main'); - expect(landmarkConfiguration.rule.any[0]).toBe('unique-landmark'); - expect(landmarkConfiguration.checks.length).toBe(0); - }); - }); -});