diff --git a/src/assessments/pointer-motion/target-size-column-renderer-factory.tsx b/src/assessments/pointer-motion/target-size-column-renderer-factory.tsx new file mode 100644 index 00000000000..12056a5d614 --- /dev/null +++ b/src/assessments/pointer-motion/target-size-column-renderer-factory.tsx @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { targetSizeColumnRenderer } from 'assessments/pointer-motion/target-size-column-renderer'; +import { InstanceTableRow } from 'assessments/types/instance-table-data'; +import { ColumnValueBag } from 'common/types/property-bag/column-value-bag'; +import { PropertyBagColumnRendererConfig } from 'common/types/property-bag/property-bag-column-renderer-config'; + +export class TargetSizeColumnRendererFactory { + public static getColumnComponent( + configs: PropertyBagColumnRendererConfig[], + ): (item: InstanceTableRow) => JSX.Element { + return item => { + return targetSizeColumnRenderer(item, configs); + }; + } +} diff --git a/src/assessments/pointer-motion/target-size-column-renderer.tsx b/src/assessments/pointer-motion/target-size-column-renderer.tsx new file mode 100644 index 00000000000..40683a7ebf2 --- /dev/null +++ b/src/assessments/pointer-motion/target-size-column-renderer.tsx @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as Markup from 'assessments/markup'; +import { InstanceTableRow } from 'assessments/types/instance-table-data'; +import { ColumnValueBag } from 'common/types/property-bag/column-value-bag'; +import { PropertyBagColumnRendererConfig } from 'common/types/property-bag/property-bag-column-renderer-config'; +import { TargetSizePropertyBag } from 'common/types/property-bag/target-size-property-bag'; +import * as React from 'react'; +import { DictionaryStringTo } from 'types/common-types'; +import { PropertyBagColumnRendererFactory } from '../common/property-bag-column-renderer-factory'; + +export function targetSizeColumnRenderer( + item: InstanceTableRow, + configs: PropertyBagColumnRendererConfig[], +): JSX.Element { + const propertyBag = item.instance.propertyBag; + const renderColumnComponent = ( + propertyBag: TargetSizePropertyBag, + columnComponent: (props: TargetSizePropertyBag) => JSX.Element, + ) => { + return columnComponent(propertyBag); + }; + + if ( + propertyBag.sizeStatus && + hasNeededProperties(['height', 'width', 'minSize'], propertyBag) + ) { + propertyBag.sizeMessageKey = propertyBag.sizeMessageKey || 'default'; + + propertyBag.sizeComponent = ( + + {renderColumnComponent( + propertyBag, + getTargetSizeMessageComponentFromPropertyBag(propertyBag), + )} + + ); + } + if ( + propertyBag.offsetStatus && + hasNeededProperties(['closestOffset', 'minOffset'], propertyBag) + ) { + propertyBag.offsetMessageKey = propertyBag.offsetMessageKey || 'default'; + propertyBag.offsetComponent = ( + + {renderColumnComponent( + propertyBag, + getTargetOffsetMessageComponentFromPropertyBag(propertyBag), + )} + + ); + } + + const propertyBagRenderer = PropertyBagColumnRendererFactory.getRenderer(configs); + + return propertyBagRenderer(item); +} + +function hasNeededProperties( + neededProperties: string[], + propertyBag: TargetSizePropertyBag, +): boolean { + return !neededProperties.some(key => propertyBag.hasOwnProperty(key) === false); +} + +export function getTargetSizeMessageComponentFromPropertyBag( + propertyBag: TargetSizePropertyBag, +): (props: TargetSizePropertyBag) => JSX.Element { + return statusWithMessageKeyToMessageComponentMapping.size[propertyBag.sizeStatus][ + propertyBag.sizeMessageKey + ]; +} + +export function getTargetOffsetMessageComponentFromPropertyBag( + propertyBag: TargetSizePropertyBag, +): (props: TargetSizePropertyBag) => JSX.Element { + return statusWithMessageKeyToMessageComponentMapping.offset[propertyBag.offsetStatus][ + propertyBag.offsetMessageKey + ]; +} + +const statusWithMessageKeyToMessageComponentMapping: DictionaryStringTo< + DictionaryStringTo JSX.Element>> +> = { + size: { + pass: { + default: props => ( + <> + Element has sufficient touch target size ({props.height}px by {props.width} + px). + + ), + obscured: props => ( + <>Element was ignored because it is fully obscured and not clickable. + ), + large: props => <>Element has sufficient touch target size., + }, + fail: { + default: props => ( + <> + Element has insufficient touch target size ( + {props.height}px by {props.width} + px, should be at least {props.minSize}px by {props.minSize}px){' '} + + ), + partiallyObscured: props => ( + <> + Element has insufficient touch target size ( + {props.height}px by {props.width} + px, should be at least {props.minSize}px by {props.minSize}px) because it is + partially obscured. + + ), + }, + incomplete: { + default: props => ( + <> + Element has negative tabindex with insufficient touch + target size ({props.height}px by {props.width}px, should be at least{' '} + {props.minSize}px by + {props.minSize}px). This may be OK if the element is + not a touch target. + + ), + contentOverflow: props => ( + <> + Element touch target size{' '} + could not be accurately determined due to overflow + content. + + ), + partiallyObscured: props => ( + <> + Element with negative tabindex has + insufficient touch target size because it is + partially obscured. This may be OK if the element is + not a touch target. + + ), + partiallyObscuredNonTabbable: props => ( + <> + Element has insufficient + touch target size because it is partially obscured by a neighbor with negative + tabindex. This may be OK if the neighbor is not a + touch target. + + ), + tooManyRects: props => ( + <> + Could not determine element target size + because there are too many overlapping elements. + + ), + }, + }, + offset: { + pass: { + default: props => ( + <> + Element has sufficient offset from its closest neighbor ({props.closestOffset} + px) + + ), + large: props => <>Element has sufficient offset from its closest neighbor., + }, + fail: { + default: props => ( + <> + Element has insufficient offset to its closest + neighbor ({props.closestOffset} + px in diameter, should be at least {props.minOffset}px in diameter) + + ), + }, + incomplete: { + default: props => ( + <> + Element with negative tabindex has + insufficient offset to its closest neighbor. This{' '} + may be OK if the element is not a touch target. + + ), + nonTabbableNeighbor: props => ( + <> + Element has sufficient offset from its closest + neighbor and the closest neighbor has a negative tabindex. This{' '} + may be OK if the neighbor is not a touch target. + + ), + }, + }, +}; diff --git a/src/assessments/pointer-motion/test-steps/target-size.tsx b/src/assessments/pointer-motion/test-steps/target-size.tsx index c69cb51cb7b..883b4716d71 100644 --- a/src/assessments/pointer-motion/test-steps/target-size.tsx +++ b/src/assessments/pointer-motion/test-steps/target-size.tsx @@ -1,8 +1,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { AnalyzerConfigurationFactory } from 'assessments/common/analyzer-configuration-factory'; +import { + getTargetOffsetMessageComponentFromPropertyBag, + getTargetSizeMessageComponentFromPropertyBag, +} from 'assessments/pointer-motion/target-size-column-renderer'; +import { TargetSizeColumnRendererFactory } from 'assessments/pointer-motion/target-size-column-renderer-factory'; +import { ReportInstanceField } from 'assessments/types/report-instance-field'; +import { ChecksType } from 'background/assessment-data-converter'; +import { TargetSizePropertyBag } from 'common/types/property-bag/target-size-property-bag'; +import { DecoratedAxeNodeResult } from 'common/types/store-data/visualization-scan-result-data'; import { link } from 'content/link'; import * as content from 'content/test/pointer-motion/target-size'; +import { AssessmentVisualizationEnabledToggle } from 'DetailsView/components/assessment-visualization-enabled-toggle'; +import { AnalyzerConfiguration } from 'injected/analyzers/analyzer'; +import { AnalyzerProvider } from 'injected/analyzers/analyzer-provider'; +import { ScannerUtils } from 'injected/scanner-utils'; +import { isEmpty } from 'lodash'; import * as React from 'react'; +import { PropertyBagColumnRendererConfig } from '../../../common/types/property-bag/property-bag-column-renderer-config'; import { ManualTestRecordYourResults } from '../../common/manual-test-record-your-results'; import * as Markup from '../../markup'; import { Requirement } from '../../types/requirement'; @@ -17,27 +33,30 @@ const description: JSX.Element = ( const howToTest: JSX.Element = (
+

+ For this requirement, Accessibility Insights for Web highlights non-inline focusable + elements on the target page and checks the touch target size. +

+

+ + Note: If no matching/failing instances are found, this requirement will + automatically be marked as pass. + +

  1. - Examine the target page to identify interactive elements which have been created - by authors (non-native browser controls). + In the Instances list below, examine each element, + and verify the element is a sufficient size and{' '} + sufficient offset from its neighbor.

  2. - Verify these elements are a minimum size of 24x24 css pixels. The following - exceptions apply: + If an element does not have sufficient size and/or sufficient offset from its + neighbor, verify the following exceptions do not apply:

      -
    • -

      - Spacing: These elements may be - smaller than 24x24 css pixels so long as it is within a 24x24 css pixel - target spacing circle that doesn’t overlap with other targets or their - 24x24 target spacing circle. -

      -
    • Equivalent: If an alternative control @@ -45,13 +64,6 @@ const howToTest: JSX.Element = ( criteria.

    • -
    • -

      - Inline: The target is in a sentence, - or its size is otherwise constrained by the line-height of non-target - text. -

      -
    • User agent control: The size of the @@ -73,12 +85,88 @@ const howToTest: JSX.Element = (

); +const displayPropertyBagConfig: PropertyBagColumnRendererConfig[] = [ + { + propertyName: 'sizeComponent', + displayName: 'Size', + defaultValue: null, + }, + { + propertyName: 'offsetComponent', + displayName: 'Offset', + defaultValue: null, + }, +]; + +const generateTargetSizePropertyBagFrom = ( + ruleResult: DecoratedAxeNodeResult, + checkName: ChecksType, +): TargetSizePropertyBag => { + if ( + ruleResult[checkName] && + !isEmpty(ruleResult[checkName]) && + ruleResult[checkName].some(r => r.data) + ) { + const status = + ruleResult.status === true + ? 'pass' + : ruleResult.status === false + ? 'fail' + : 'incomplete'; + const data = Object.assign( + {}, + ...ruleResult[checkName].map(r => { + return { + ...r.data, + [`${r.id.split('-')[1]}Status`]: status, + [`${r.id.split('-')[1]}MessageKey`]: r.data.messageKey, + }; + }), + ); + return data; + } + return null; +}; export const TargetSize: Requirement = { key: PointerMotionTestStep.targetSize, name: 'Target size', description, howToTest, ...content, - isManual: true, + isManual: false, + columnsConfig: [ + { + key: 'touch-target-info', + name: 'Touch target info', + onRender: + TargetSizeColumnRendererFactory.getColumnComponent( + displayPropertyBagConfig, + ), + }, + ], + reportInstanceFields: [ + ReportInstanceField.fromPropertyBagFunction( + 'Size', + 'sizeComponent', + pb => getTargetSizeMessageComponentFromPropertyBag(pb).toString(), + ), + ReportInstanceField.fromPropertyBagFunction( + 'Offset', + 'offsetComponent', + pb => getTargetOffsetMessageComponentFromPropertyBag(pb).toString(), + ), + ], + getAnalyzer: (provider: AnalyzerProvider, analyzerConfig: AnalyzerConfiguration) => + provider.createRuleAnalyzer( + AnalyzerConfigurationFactory.forScanner({ + rules: ['target-size'], + resultProcessor: (scanner: ScannerUtils) => scanner.getAllApplicableInstances, + ...analyzerConfig, + }), + ), guidanceLinks: [link.WCAG_2_5_8], + getDrawer: provider => provider.createHighlightBoxDrawer(), + getVisualHelperToggle: props => , + generatePropertyBagFrom: generateTargetSizePropertyBagFrom, + // getCompletedRequirementDetailsForTelemetry: labelInNameGetCompletedRequirementDetails, }; diff --git a/src/assessments/types/report-instance-field.ts b/src/assessments/types/report-instance-field.ts index 3340d52aa69..1ae93a841c0 100644 --- a/src/assessments/types/report-instance-field.ts +++ b/src/assessments/types/report-instance-field.ts @@ -64,6 +64,7 @@ const common: { [key in CommonReportInstanceFieldKey]: ReportInstanceField } = { }; function isValid(value: ColumnValue): ColumnValue { + console.log('valid??', isValid); if (!value) { return false; } diff --git a/src/assessments/types/requirement.ts b/src/assessments/types/requirement.ts index c8fa1b5b421..304e71cc882 100644 --- a/src/assessments/types/requirement.ts +++ b/src/assessments/types/requirement.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { IColumn } from '@fluentui/react'; +import { ChecksType } from 'background/assessment-data-converter'; import { UniquelyIdentifiableInstances } from 'background/instance-identifier-generator'; import { HyperlinkDefinition } from 'common/types/hyperlink-definition'; import { @@ -60,6 +61,7 @@ export interface Requirement { getDefaultMessage?: IGetMessageGenerator; instanceTableHeaderType?: InstanceTableHeaderType; getCompletedRequirementDetailsForTelemetry?: (assessmentData: AssessmentData) => any; + generatePropertyBagFrom?: (ruleResult: DecoratedAxeNodeResult, checkName: ChecksType) => any; } export type VisualHelperToggleConfigDeps = { diff --git a/src/background/assessment-data-converter.ts b/src/background/assessment-data-converter.ts index 2de034f0387..e998a490ef7 100644 --- a/src/background/assessment-data-converter.ts +++ b/src/background/assessment-data-converter.ts @@ -36,6 +36,10 @@ export class AssessmentDataConverter { getInstanceStatus: (result: DecoratedAxeNodeResult) => ManualTestStatus, isVisualizationSupported: (result: DecoratedAxeNodeResult) => boolean, getIncludedAlwaysRules: () => string[], + generatePropertyBagFrom?: ( + ruleResult: DecoratedAxeNodeResult, + checkName: ChecksType, + ) => any, ): AssessmentInstancesMap { let instancesMap: AssessmentInstancesMap = {}; @@ -62,6 +66,7 @@ export class AssessmentDataConverter { ruleResult, getInstanceStatus, isVisualizationSupported, + generatePropertyBagFrom, ); } }); @@ -110,6 +115,10 @@ export class AssessmentDataConverter { ruleResult: DecoratedAxeNodeResult, getInstanceStatus: (result: DecoratedAxeNodeResult) => ManualTestStatus, isVisualizationSupported: (result: DecoratedAxeNodeResult) => boolean, + generatePropertyBagFrom?: ( + ruleResult: DecoratedAxeNodeResult, + checkName: ChecksType, + ) => any, ): GeneratedAssessmentInstance { const target: Target = elementAxeResult.target; let testStepResults = {}; @@ -130,7 +139,7 @@ export class AssessmentDataConverter { ); let actualPropertyBag = { - ...this.getPropertyBagFromAnyChecks(ruleResult), + ...this.getPropertyBagFromAnyChecks(ruleResult, generatePropertyBagFrom), ...propertyBag, }; actualPropertyBag = isEmpty(actualPropertyBag) ? null : actualPropertyBag; @@ -203,8 +212,16 @@ export class AssessmentDataConverter { }; } - private getPropertyBagFromAnyChecks(ruleResult: DecoratedAxeNodeResult): any { - return this.getPropertyBagFrom(ruleResult, 'any'); + private getPropertyBagFromAnyChecks( + ruleResult: DecoratedAxeNodeResult, + generatePropertyBagFrom?: ( + ruleResult: DecoratedAxeNodeResult, + checkName: ChecksType, + ) => any, + ): any { + return generatePropertyBagFrom + ? generatePropertyBagFrom(ruleResult, 'any') + : this.getPropertyBagFrom(ruleResult, 'any'); } private getPropertyBagFrom(ruleResult: DecoratedAxeNodeResult, checkName: ChecksType): any { @@ -215,7 +232,6 @@ export class AssessmentDataConverter { ) { return ruleResult[checkName][0].data; } - return null; } @@ -234,4 +250,4 @@ export class AssessmentDataConverter { } } -type ChecksType = 'any' | 'all'; +export type ChecksType = 'any' | 'all'; diff --git a/src/background/stores/assessment-store.ts b/src/background/stores/assessment-store.ts index 8388a2d0f8f..7cb0a13e103 100644 --- a/src/background/stores/assessment-store.ts +++ b/src/background/stores/assessment-store.ts @@ -409,6 +409,7 @@ export class AssessmentStore extends PersistentStore { stepConfig.getInstanceStatus, stepConfig.isVisualizationSupportedForResult, getIncludedAlwaysRules, + stepConfig.generatePropertyBagFrom, ); assessmentData.generatedAssessmentInstancesMap = generatedAssessmentInstancesMap; assessmentData.testStepStatus[step].isStepScanned = true; diff --git a/src/common/types/property-bag/target-size-property-bag.ts b/src/common/types/property-bag/target-size-property-bag.ts new file mode 100644 index 00000000000..36be2fc4f08 --- /dev/null +++ b/src/common/types/property-bag/target-size-property-bag.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { ColumnValueBag } from './column-value-bag'; + +export interface TargetSizePropertyBag extends ColumnValueBag { + width?: number; + height?: number; + closestOffset?: number; + sizeMessageKey?: string; + offsetMessageKey?: string; + sizeStatus?: string; + offsetStatus?: string; + minSize?: number; + minOffset?: number; +} diff --git a/src/injected/scanner-utils.ts b/src/injected/scanner-utils.ts index b0974551566..7abdc339cb8 100644 --- a/src/injected/scanner-utils.ts +++ b/src/injected/scanner-utils.ts @@ -52,6 +52,17 @@ export class ScannerUtils { return resultsMap; }; + public getAllApplicableInstances = ( + results: ScanResults, + ): DictionaryStringTo => { + const resultsMap: DictionaryStringTo = {}; + this.addFailuresToDictionary(resultsMap, results.violations); + this.addIncompletesToDictionary(resultsMap, results.incomplete); + this.addPassesToDictionary(resultsMap, results.passes); + + return resultsMap; + }; + public getAllCompletedInstances = ( results: ScanResults, ): DictionaryStringTo => { diff --git a/src/tests/end-to-end/test-resources/target-size/fail-size-default.html b/src/tests/end-to-end/test-resources/target-size/fail-size-default.html new file mode 100644 index 00000000000..4332a3d52bf --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/fail-size-default.html @@ -0,0 +1,24 @@ + + + + + + Target size + + + +

Target Size Rule Example Controls

+ +

Failing - Size - Default

+ + hmmm + + a + + mmmmm + + + diff --git a/src/tests/end-to-end/test-resources/target-size/fail-size-po-offset-default.html b/src/tests/end-to-end/test-resources/target-size/fail-size-po-offset-default.html new file mode 100644 index 00000000000..be9509678ef --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/fail-size-po-offset-default.html @@ -0,0 +1,29 @@ + + + + + + Target size + + + +

Target Size Rule Example Controls

+ +

Failing - Size - Partially Obscured, Offset - Default

+ + + a + + b + + + + diff --git a/src/tests/end-to-end/test-resources/target-size/incomplete-offset-default.html b/src/tests/end-to-end/test-resources/target-size/incomplete-offset-default.html new file mode 100644 index 00000000000..1e6a9e7bed8 --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/incomplete-offset-default.html @@ -0,0 +1,4 @@ + diff --git a/src/tests/end-to-end/test-resources/target-size/incomplete-offset-non-tabbable-neighbor.html b/src/tests/end-to-end/test-resources/target-size/incomplete-offset-non-tabbable-neighbor.html new file mode 100644 index 00000000000..e2fb3de80ae --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/incomplete-offset-non-tabbable-neighbor.html @@ -0,0 +1,23 @@ + + + + + + Target size + + + +

Target Size Rule Example Controls

+ +

Incomplete - Offset - Non-Tabbable Neighbor

+ + b + a + c + + + + diff --git a/src/tests/end-to-end/test-resources/target-size/incomplete-offset-too-many-rects.html b/src/tests/end-to-end/test-resources/target-size/incomplete-offset-too-many-rects.html new file mode 100644 index 00000000000..1e6a9e7bed8 --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/incomplete-offset-too-many-rects.html @@ -0,0 +1,4 @@ + diff --git a/src/tests/end-to-end/test-resources/target-size/incomplete-size-content-overflow.html b/src/tests/end-to-end/test-resources/target-size/incomplete-size-content-overflow.html new file mode 100644 index 00000000000..59461e1db32 --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/incomplete-size-content-overflow.html @@ -0,0 +1,23 @@ + + + + + + Target size + + + +

Target Size Rule Example Controls

+ +

Incomplete - Size - Content Overflow

+ + + + + + + + diff --git a/src/tests/end-to-end/test-resources/target-size/incomplete-size-default.html b/src/tests/end-to-end/test-resources/target-size/incomplete-size-default.html new file mode 100644 index 00000000000..c03f695ab0c --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/incomplete-size-default.html @@ -0,0 +1,28 @@ + + + + + + Target size + + + +

Target Size Rule Example Controls

+ +

Incomplete - Size - Default

+ + + a + + + + + diff --git a/src/tests/end-to-end/test-resources/target-size/incomplete-size-partially-obscured.html b/src/tests/end-to-end/test-resources/target-size/incomplete-size-partially-obscured.html new file mode 100644 index 00000000000..6fba7b0b20a --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/incomplete-size-partially-obscured.html @@ -0,0 +1,34 @@ + + + + + + Target size + + + +

Target Size Rule Example Controls

+ +

Incomplete - Size - Partially Obscured

+ + + a + + b + + + + diff --git a/src/tests/end-to-end/test-resources/target-size/incomplete-size-partially-obscurred-no-tab.html b/src/tests/end-to-end/test-resources/target-size/incomplete-size-partially-obscurred-no-tab.html new file mode 100644 index 00000000000..e60ea657611 --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/incomplete-size-partially-obscurred-no-tab.html @@ -0,0 +1,30 @@ + + + + + + Target size + + + +

Target Size Rule Example Controls

+ +

Incomplete - Size - Partially Obscured Non-Tabbable

+ + + a + + x + + + + diff --git a/src/tests/end-to-end/test-resources/target-size/pass-offset-default.html b/src/tests/end-to-end/test-resources/target-size/pass-offset-default.html new file mode 100644 index 00000000000..94a54f3aa10 --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/pass-offset-default.html @@ -0,0 +1,28 @@ + + + + + + Target offset + + + +

Target Size Rule Example Controls

+ +

Passing - Offset - Default

+ + b + + a + + + + + diff --git a/src/tests/end-to-end/test-resources/target-size/pass-offset-large.html b/src/tests/end-to-end/test-resources/target-size/pass-offset-large.html new file mode 100644 index 00000000000..0b44aea274e --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/pass-offset-large.html @@ -0,0 +1,24 @@ + + + + + + Target offset + + + +

Target Size Rule Example Controls

+ +

Passing - Offset - Large

+ + b + + a + + + + + diff --git a/src/tests/end-to-end/test-resources/target-size/pass-size-default.html b/src/tests/end-to-end/test-resources/target-size/pass-size-default.html new file mode 100644 index 00000000000..528979267b5 --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/pass-size-default.html @@ -0,0 +1,23 @@ + + + + + + Target size + + + +

Target Size Rule Example Controls

+ +

Passing - Size - Default

+ + + a + + + + + diff --git a/src/tests/end-to-end/test-resources/target-size/pass-size-large.html b/src/tests/end-to-end/test-resources/target-size/pass-size-large.html new file mode 100644 index 00000000000..55c5aae59e9 --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/pass-size-large.html @@ -0,0 +1,23 @@ + + + + + + Target size + + + +

Target Size Rule Example Controls

+ +

Passing - Size - Large

+ + + + + + diff --git a/src/tests/end-to-end/test-resources/target-size/pass-size-obscured.html b/src/tests/end-to-end/test-resources/target-size/pass-size-obscured.html new file mode 100644 index 00000000000..05bc03158a9 --- /dev/null +++ b/src/tests/end-to-end/test-resources/target-size/pass-size-obscured.html @@ -0,0 +1,27 @@ + + + + + + Target size + + + +

Target Size Rule Example Controls

+ +

Passing - Size - Obscured

+ + a + b + + + + diff --git a/src/tests/unit/tests/assessments/pointer-motion/__snapshots__/target-size-column-renderer.test.tsx.snap b/src/tests/unit/tests/assessments/pointer-motion/__snapshots__/target-size-column-renderer.test.tsx.snap new file mode 100644 index 00000000000..c7473161858 --- /dev/null +++ b/src/tests/unit/tests/assessments/pointer-motion/__snapshots__/target-size-column-renderer.test.tsx.snap @@ -0,0 +1,145 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TargetSizeColumnRenderer when offsetStatus is set should render offsetComponent when the necessary properties are available 1`] = ` + +
+
+ + display a: + + value for a +
+
+ + offset: + + + Element has sufficient offset from its closest neighbor (25px) + +
+
+
+`; + +exports[`TargetSizeColumnRenderer when offsetStatus is set should return null when the necessary properties are not available 1`] = ` + +
+
+ + display a: + + value for a +
+
+
+`; + +exports[`TargetSizeColumnRenderer when sizeStatus is set should render expected message with bolded words when sizeStatus is fail 1`] = ` + +
+
+ + display a: + + value for a +
+
+ + size: + + + Element has + + insufficient + + touch target size (5px by 5px, should be at least 28px by 28px) + +
+
+
+`; + +exports[`TargetSizeColumnRenderer when sizeStatus is set should render sizeComponent when the necessary properties are available 1`] = ` + +
+
+ + display a: + + value for a +
+
+ + size: + + + Element has sufficient touch target size (40px by 40px). + +
+
+
+`; + +exports[`TargetSizeColumnRenderer when sizeStatus is set should return null when the necessary properties are not available 1`] = ` + +
+
+ + display a: + + value for a +
+
+
+`; diff --git a/src/tests/unit/tests/assessments/pointer-motion/target-size-column-renderer-factory.test.tsx b/src/tests/unit/tests/assessments/pointer-motion/target-size-column-renderer-factory.test.tsx new file mode 100644 index 00000000000..aec2f07c7c3 --- /dev/null +++ b/src/tests/unit/tests/assessments/pointer-motion/target-size-column-renderer-factory.test.tsx @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { render } from '@testing-library/react'; +import { TargetSizeColumnRendererFactory } from 'assessments/pointer-motion/target-size-column-renderer-factory'; +import { InstanceTableRow } from 'assessments/types/instance-table-data'; +import { PropertyBagColumnRendererConfig } from 'common/types/property-bag/property-bag-column-renderer-config'; +import * as React from 'react'; + +import { ColumnValueBag } from '../../../../../common/types/property-bag/column-value-bag'; +import { RendererWrapper } from '../common/renderer-wrapper'; + +describe('TargetSizeColumnRendererFactory', () => { + let configs: PropertyBagColumnRendererConfig[]; + let item: InstanceTableRow; + + beforeEach(() => { + configs = [ + { + propertyName: 'a', + displayName: 'display a', + }, + { + propertyName: 'sizeComponent', + displayName: 'size', + }, + { + propertyName: 'offsetComponent', + displayName: 'offset', + }, + ]; + + item = { + key: 'stub-key', + instance: { + html: null, + target: null, + testStepResults: null, + propertyBag: { + a: 'value for a', + sizeStatus: 'pass', + height: 40, + width: 40, + minSize: 30, + }, + }, + statusChoiceGroup: null, + visualizationButton: null, + }; + }); + + it('getColumnComponent should render component', () => { + const result = TargetSizeColumnRendererFactory.getColumnComponent(configs); + + checkPropertyBagAndTag(result, 'SPAN'); + }); + + function checkPropertyBagAndTag(result: Function, tag: string): void { + const renderer = () => result(item); + + const renderResult = render(); + renderResult.debug(); + const div = renderResult.container.querySelector('.property-bag-container'); + expect(div).not.toBeNull(); + expect(div.children).toHaveLength(2); + + const targetSizeSpan = div.children[1]; + expect(targetSizeSpan).not.toBeUndefined(); + expect(targetSizeSpan.classList.contains('property-bag-div')).toBeTruthy(); + expect(targetSizeSpan.children).toHaveLength(2); + const contentElement = targetSizeSpan.children[1]; + expect(contentElement).not.toBeNull(); + expect(contentElement.tagName).toBe(tag); + } +}); + +interface TestPropertyBag extends ColumnValueBag { + a: string; +} diff --git a/src/tests/unit/tests/assessments/pointer-motion/target-size-column-renderer.test.tsx b/src/tests/unit/tests/assessments/pointer-motion/target-size-column-renderer.test.tsx new file mode 100644 index 00000000000..2b6455454b3 --- /dev/null +++ b/src/tests/unit/tests/assessments/pointer-motion/target-size-column-renderer.test.tsx @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { render } from '@testing-library/react'; +import { targetSizeColumnRenderer } from 'assessments/pointer-motion/target-size-column-renderer'; +import { InstanceTableRow } from 'assessments/types/instance-table-data'; +import { PropertyBagColumnRendererConfig } from 'common/types/property-bag/property-bag-column-renderer-config'; +import * as React from 'react'; + +import { ColumnValueBag } from '../../../../../common/types/property-bag/column-value-bag'; +import { RendererWrapper } from '../common/renderer-wrapper'; + +describe('TargetSizeColumnRenderer', () => { + let configs: PropertyBagColumnRendererConfig[]; + let item: InstanceTableRow; + + beforeEach(() => { + configs = [ + { + propertyName: 'a', + displayName: 'display a', + }, + { + propertyName: 'sizeComponent', + displayName: 'size', + }, + { + propertyName: 'offsetComponent', + displayName: 'offset', + }, + ]; + + item = { + key: 'stub-key', + instance: { + html: null, + target: null, + testStepResults: null, + propertyBag: { + a: 'value for a', + }, + }, + statusChoiceGroup: null, + visualizationButton: null, + }; + }); + + it('should render unrelated properties', () => { + const renderer = () => targetSizeColumnRenderer(item, configs); + + const renderResult = render(); + + const div = renderResult.container.querySelector('.property-bag-container'); + expect(div).not.toBeNull(); + expect(div.children).toHaveLength(1); + + const designPatternSpan = div.children[0]; + expect(designPatternSpan).not.toBeUndefined(); + expect(designPatternSpan.classList.contains('property-bag-div')).toBeTruthy(); + expect(designPatternSpan.children).toHaveLength(1); + expect(designPatternSpan.textContent).toEqual( + `${configs[0].displayName}: ${item.instance.propertyBag.a}`, + ); + }); + + describe('when sizeStatus is set', () => { + beforeEach(() => { + item.instance.propertyBag.sizeStatus = 'pass'; + }); + it('should render sizeComponent when the necessary properties are available', () => { + item.instance.propertyBag.height = 40; + item.instance.propertyBag.width = 40; + item.instance.propertyBag.minSize = 28; + const renderer = () => targetSizeColumnRenderer(item, configs); + + const renderResult = render(); + const div = renderResult.container.querySelector('.property-bag-container'); + expect(div).not.toBeNull(); + expect(div.children).toHaveLength(2); + + const propertyBagDiv = div.children[1]; + expect(propertyBagDiv).not.toBeUndefined(); + expect(propertyBagDiv.classList.contains('property-bag-div')).toBeTruthy(); + expect(propertyBagDiv.children).toHaveLength(2); + + const targetSizeSpan = propertyBagDiv.children[1]; + expect(targetSizeSpan.id).toEqual('target-size'); + expect(targetSizeSpan.children.length).toEqual(0); + expect(targetSizeSpan.textContent).toContain( + `${item.instance.propertyBag.height}px by ${item.instance.propertyBag.width}px`, + ); + expect(renderResult.asFragment()).toMatchSnapshot(); + }); + + it('should return null when the necessary properties are not available', () => { + item.instance.propertyBag.a = 'value for a'; + const renderer = () => targetSizeColumnRenderer(item, configs); + + const renderResult = render(); + + const div = renderResult.container.querySelector('.property-bag-container'); + expect(div).not.toBeNull(); + expect(div.children).toHaveLength(1); + expect(renderResult.asFragment()).toMatchSnapshot(); + }); + + it('should render expected message with bolded words when sizeStatus is fail', () => { + item.instance.propertyBag.sizeStatus = 'fail'; + item.instance.propertyBag.height = 5; + item.instance.propertyBag.width = 5; + item.instance.propertyBag.minSize = 28; + const renderer = () => targetSizeColumnRenderer(item, configs); + + const renderResult = render(); + const div = renderResult.container.querySelector('.property-bag-container'); + expect(div).not.toBeNull(); + expect(div.children).toHaveLength(2); + + const propertyBagDiv = div.children[1]; + expect(propertyBagDiv).not.toBeUndefined(); + expect(propertyBagDiv.classList.contains('property-bag-div')).toBeTruthy(); + expect(propertyBagDiv.children).toHaveLength(2); + + const targetSizeSpan = propertyBagDiv.children[1]; + expect(targetSizeSpan.id).toEqual('target-size'); + expect(targetSizeSpan.children.length).toEqual(1); + expect(targetSizeSpan.textContent).toContain( + `${item.instance.propertyBag.height}px by ${item.instance.propertyBag.width}px`, + ); + expect(renderResult.asFragment()).toMatchSnapshot(); + }); + }); + + describe('when offsetStatus is set', () => { + beforeEach(() => { + item.instance.propertyBag.offsetStatus = 'pass'; + }); + it('should render offsetComponent when the necessary properties are available', () => { + item.instance.propertyBag.closestOffset = 25; + item.instance.propertyBag.minOffset = 25; + const renderer = () => targetSizeColumnRenderer(item, configs); + + const renderResult = render(); + const div = renderResult.container.querySelector('.property-bag-container'); + expect(div).not.toBeNull(); + expect(div.children).toHaveLength(2); + + const propertyBagDiv = div.children[1]; + expect(propertyBagDiv).not.toBeUndefined(); + expect(propertyBagDiv.classList.contains('property-bag-div')).toBeTruthy(); + expect(propertyBagDiv.children).toHaveLength(2); + + const targetOffsetSpan = propertyBagDiv.children[1]; + expect(targetOffsetSpan.id).toEqual('target-offset'); + expect(targetOffsetSpan.children.length).toEqual(0); + expect(targetOffsetSpan.textContent).toContain( + `${item.instance.propertyBag.closestOffset}px`, + ); + expect(renderResult.asFragment()).toMatchSnapshot(); + }); + + it('should return null when the necessary properties are not available', () => { + item.instance.propertyBag.a = 'value for a'; + const renderer = () => targetSizeColumnRenderer(item, configs); + + const renderResult = render(); + + const div = renderResult.container.querySelector('.property-bag-container'); + expect(div).not.toBeNull(); + expect(div.children).toHaveLength(1); + expect(renderResult.asFragment()).toMatchSnapshot(); + }); + }); +}); + +interface TestPropertyBag extends ColumnValueBag { + a: string; +}