diff --git a/cli/test/fixtures/dobetterweb/dbw_tester.html b/cli/test/fixtures/dobetterweb/dbw_tester.html index dcdbfef37470..93301e328f5a 100644 --- a/cli/test/fixtures/dobetterweb/dbw_tester.html +++ b/cli/test/fixtures/dobetterweb/dbw_tester.html @@ -242,16 +242,6 @@

Do better web tester page

- - - - - - - - - SEO tap target audit tester - - - - - - - - - - sticky - -
- -
- - This link is intentionally placed at the top of the page, so that the sticky header is hard to tap on. - It should not fail though because overlap with a sticky element depends on the scroll position. - -
- -

SEO Tap targets

- - -
- -
- -
- - - 0 - display none - display none parent - - width 0 and overflow x hidden - - - visible target -
-

- - -
- - -
invisible
- visible -
-
- -

- - - -
- Link -
-
- - Link that the top one would overlap with, if it weren't for the inline-block child. - - -

- -
- Tap target with children that are also tap targets should not fail. - (Children should not be counted as independent tap targets that appear - in the list.) - Two children to make sure the two children also don't conflict with each other: - Child 1Child2 -
- -

- - -
- -
- left -
-
- right -
- -

- - -
- -
- left -
-
- right -
- -

- -
- inner - outer -
- -

- - -
-
- - - too small target - - - big enough target - -
-
- -

- - -
- - - zero width target - - - passing target - -
- -

- - -
- - -
- - -

- This is a link in a text block. - This is a link in a text block. - This is a link in a text block. - This is a link in a text block. - This is a link in a text block. -

- - - diff --git a/cli/test/smokehouse/core-tests.js b/cli/test/smokehouse/core-tests.js index 0dbffbeb8a1c..9d4ae94ebc26 100644 --- a/cli/test/smokehouse/core-tests.js +++ b/cli/test/smokehouse/core-tests.js @@ -61,7 +61,6 @@ import screenshot from './test-definitions/screenshot.js'; import seoFailing from './test-definitions/seo-failing.js'; import seoPassing from './test-definitions/seo-passing.js'; import seoStatus403 from './test-definitions/seo-status-403.js'; -import seoTapTargets from './test-definitions/seo-tap-targets.js'; import serviceWorkerReloaded from './test-definitions/service-worker-reloaded.js'; import shiftAttribution from './test-definitions/shift-attribution.js'; import sourceMaps from './test-definitions/source-maps.js'; @@ -126,7 +125,6 @@ const smokeTests = [ seoFailing, seoPassing, seoStatus403, - seoTapTargets, serviceWorkerReloaded, shiftAttribution, sourceMaps, diff --git a/cli/test/smokehouse/test-definitions/dobetterweb.js b/cli/test/smokehouse/test-definitions/dobetterweb.js index 5547b0768805..10cc01da3425 100644 --- a/cli/test/smokehouse/test-definitions/dobetterweb.js +++ b/cli/test/smokehouse/test-definitions/dobetterweb.js @@ -442,7 +442,7 @@ const expectations = { }, 'dom-size': { score: 1, - numericValue: 154, + numericValue: 151, details: { items: [ { @@ -450,7 +450,7 @@ const expectations = { value: { type: 'numeric', granularity: 1, - value: 154, + value: 151, }, }, { diff --git a/cli/test/smokehouse/test-definitions/seo-tap-targets.js b/cli/test/smokehouse/test-definitions/seo-tap-targets.js deleted file mode 100644 index f37c193c72c4..000000000000 --- a/cli/test/smokehouse/test-definitions/seo-tap-targets.js +++ /dev/null @@ -1,210 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -const BASE_URL = 'http://localhost:10200/seo/'; - -/** @type {LH.Config} */ -const config = { - extends: 'lighthouse:default', - settings: { - onlyCategories: ['seo'], - }, -}; - -const expectedGatheredTapTargets = [ - { - node: { - snippet: /large-link-at-bottom-of-page/, - }, - }, - { - node: { - snippet: /visible-target/, - }, - }, - { - node: { - snippet: /target-with-client-rect-outside-scroll-container/, - }, - }, - { - node: { - snippet: /link-containing-large-inline-block-element/, - }, - }, - { - node: { - snippet: /link-next-to-link-containing-large-inline-block-element/, - }, - }, - { - node: { - snippet: /tap-target-containing-other-tap-targets/, - }, - }, - { - node: { - snippet: /child-client-rect-hidden-by-overflow-hidden/, - }, - }, - { - node: { - snippet: /tap-target-next-to-child-client-rect-hidden-by-overflow-hidden/, - }, - }, - { - node: { - snippet: /child-client-rect-overlapping-other-target/, - }, - shouldFail: true, - }, - { - node: { - snippet: /tap-target-overlapped-by-other-targets-position-absolute-child-rect/, - }, - shouldFail: true, - }, - { - node: { - snippet: /position-absolute-tap-target-fully-contained-in-other-target/, - }, - }, - { - node: { - snippet: /tap-target-fully-containing-position-absolute-target/, - }, - }, - { - node: { - snippet: /too-small-failing-tap-target/, - }, - shouldFail: true, - }, - { - node: { - snippet: /large-enough-tap-target-next-to-too-small-tap-target/, - }, - }, - { - node: { - snippet: /zero-width-tap-target-with-overflowing-child-content/, - }, - shouldFail: true, - }, - { - node: { - snippet: /passing-tap-target-next-to-zero-width-target/, - }, - }, - { - node: { - snippet: /links-with-same-link-target-1/, - }, - }, - { - node: { - snippet: /links-with-same-link-target-2/, - }, - }, -]; - -/** - * @type {Smokehouse.ExpectedRunnerResult} - * Expected Lighthouse audit values for a site exercising tap targets. - */ -const expectations = { - lhr: { - finalDisplayedUrl: BASE_URL + 'seo-tap-targets.html', - requestedUrl: BASE_URL + 'seo-tap-targets.html', - audits: { - 'tap-targets': { - score: (() => { - const totalTapTargets = expectedGatheredTapTargets.length; - const passingTapTargets = expectedGatheredTapTargets.filter(t => !t.shouldFail).length; - const SCORE_FACTOR = 0.89; - return Math.round(passingTapTargets / totalTapTargets * SCORE_FACTOR * 100) / 100; - })(), - details: { - items: [ - { - 'tapTarget': { - 'type': 'node', - /* eslint-disable max-len */ - 'snippet': '', - 'path': '2,HTML,1,BODY,14,DIV,0,A', - 'selector': 'body > div > a', - 'nodeLabel': 'zero width target', - }, - 'overlappingTarget': { - 'type': 'node', - /* eslint-disable max-len */ - 'snippet': '', - 'path': '2,HTML,1,BODY,14,DIV,1,A', - 'selector': 'body > div > a', - 'nodeLabel': 'passing target', - }, - 'tapTargetScore': 864, - 'overlappingTargetScore': 720, - 'overlapScoreRatio': 0.8333333333333334, - 'size': '110x18', - 'width': 110, - 'height': 18, - }, - { - 'tapTarget': { - 'type': 'node', - 'path': '2,HTML,1,BODY,10,DIV,0,DIV,1,A', - 'selector': 'body > div > div > a', - 'nodeLabel': 'too small target', - }, - 'overlappingTarget': { - 'type': 'node', - 'path': '2,HTML,1,BODY,10,DIV,0,DIV,2,A', - 'selector': 'body > div > div > a', - 'nodeLabel': 'big enough target', - }, - 'tapTargetScore': 1440, - 'overlappingTargetScore': 432, - 'overlapScoreRatio': 0.3, - 'size': '100x30', - 'width': 100, - 'height': 30, - }, - { - 'tapTarget': { - 'type': 'node', - 'path': '2,HTML,1,BODY,3,DIV,24,A', - 'selector': 'body > div > a', - 'nodeLabel': 'left', - }, - 'overlappingTarget': { - 'type': 'node', - 'path': '2,HTML,1,BODY,3,DIV,25,A', - 'selector': 'body > div > a', - 'nodeLabel': 'right', - }, - 'tapTargetScore': 1920, - 'overlappingTargetScore': 560, - 'overlapScoreRatio': 0.2916666666666667, - 'size': '40x40', - 'width': 40, - 'height': 40, - }, - ], - }, - }, - }, - }, - artifacts: { - TapTargets: expectedGatheredTapTargets.map(({node}) => ({node})), - }, -}; - -export default { - id: 'seo-tap-targets', - expectations, - config, -}; diff --git a/core/audits/seo/tap-targets.js b/core/audits/seo/tap-targets.js deleted file mode 100644 index 5458db00e539..000000000000 --- a/core/audits/seo/tap-targets.js +++ /dev/null @@ -1,352 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Checks that links, buttons, etc. are sufficiently large and that there's - * no other tap target that's too close so that the user might accidentally tap on. - */ - -import {Audit} from '../audit.js'; -import {ViewportMeta} from '../../computed/viewport-meta.js'; -import { - rectsTouchOrOverlap, - getRectOverlapArea, - getRectAtCenter, - allRectsContainedWithinEachOther, - getLargestRect, - getBoundingRectWithPadding, -} from '../../lib/rect-helpers.js'; -import {getTappableRectsFromClientRects} from '../../lib/tappable-rects.js'; -import * as i18n from '../../lib/i18n/i18n.js'; - -const UIStrings = { - /** Title of a Lighthouse audit that provides detail on whether tap targets (like buttons and links) on a page are big enough so they can easily be tapped on a mobile device. This descriptive title is shown when tap targets are easy to tap on. */ - title: 'Tap targets are sized appropriately', - /** Descriptive title of a Lighthouse audit that provides detail on whether tap targets (like buttons and links) on a page are big enough so they can easily be tapped on a mobile device. This descriptive title is shown when tap targets are not easy to tap on. */ - failureTitle: 'Tap targets are not sized appropriately', - /** Description of a Lighthouse audit that tells the user why buttons and links need to be big enough and what 'big enough' means. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */ - description: 'Interactive elements like buttons and links should be large enough (48x48px), or have enough space around them, to be easy enough to tap without overlapping onto other elements. [Learn more about tap targets](https://developer.chrome.com/docs/lighthouse/seo/tap-targets/).', - /** Label of a table column that identifies tap targets (like buttons and links) that have failed the audit and aren't easy to tap on. */ - tapTargetHeader: 'Tap Target', - /** Label of a table column that identifies a tap target (like a link or button) that overlaps with another tap target. */ - overlappingTargetHeader: 'Overlapping Target', - /** Explanatory message stating that there was a failure in an audit caused by the viewport meta tag not being optimized for mobile screens, which caused tap targets like buttons and links to be too small to tap on. */ - /* eslint-disable-next-line max-len */ - explanationViewportMetaNotOptimized: 'Tap targets are too small because there\'s no viewport meta tag optimized for mobile screens', - /** Explanatory message stating that a certain percentage of the tap targets (like buttons and links) on the page are of an appropriately large size. */ - displayValue: '{decimalProportion, number, percent} appropriately sized tap targets', -}; - -const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); - -const FINGER_SIZE_PX = 48; -// Ratio of the finger area tapping on an unintended element -// to the finger area tapping on the intended element -const MAX_ACCEPTABLE_OVERLAP_SCORE_RATIO = 0.25; - -/** - * Returns a tap target augmented with a bounding rect for quick overlapping - * rejections. Rect contains all the client rects, padded to half FINGER_SIZE_PX. - * @param {LH.Artifacts.TapTarget[]} targets - * @return {BoundedTapTarget[]} - */ -function getBoundedTapTargets(targets) { - return targets.map(tapTarget => { - return { - tapTarget, - paddedBoundsRect: getBoundingRectWithPadding(tapTarget.clientRects, FINGER_SIZE_PX), - }; - }); -} - -/** - * @param {LH.Artifacts.Rect} cr - */ -function clientRectBelowMinimumSize(cr) { - return cr.width < FINGER_SIZE_PX || cr.height < FINGER_SIZE_PX; -} - -/** - * A target is "too small" if none of its clientRects are at least the size of a finger. - * @param {BoundedTapTarget[]} targets - * @return {BoundedTapTarget[]} - */ -function getTooSmallTargets(targets) { - return targets.filter(target => { - return target.tapTarget.clientRects.every(clientRectBelowMinimumSize); - }); -} - -/** - * @param {BoundedTapTarget[]} tooSmallTargets - * @param {BoundedTapTarget[]} allTargets - * @return {TapTargetOverlapFailure[]} - */ -function getAllOverlapFailures(tooSmallTargets, allTargets) { - /** @type {TapTargetOverlapFailure[]} */ - const failures = []; - - tooSmallTargets.forEach(target => { - // Convert client rects to unique tappable areas from a user's perspective - const tappableRects = getTappableRectsFromClientRects(target.tapTarget.clientRects); - - for (const maybeOverlappingTarget of allTargets) { - if (maybeOverlappingTarget === target) { - // Checking the same target with itself, skip. - continue; - } - - if (!rectsTouchOrOverlap(target.paddedBoundsRect, maybeOverlappingTarget.paddedBoundsRect)) { - // Bounding boxes (padded with half FINGER_SIZE_PX) don't overlap, skip. - continue; - } - - if (target.tapTarget.href === maybeOverlappingTarget.tapTarget.href) { - const isHttpOrHttpsLink = /https?:\/\//.test(target.tapTarget.href); - if (isHttpOrHttpsLink) { - // No overlap because same target action, skip. - continue; - } - } - - const maybeOverlappingRects = maybeOverlappingTarget.tapTarget.clientRects; - if (allRectsContainedWithinEachOther(tappableRects, maybeOverlappingRects)) { - // If one tap target is fully contained within the other that's - // probably intentional (e.g. an item with a delete button inside). - // We'll miss some problems because of this, but that's better - // than falsely reporting a failure. - continue; - } - - const rectFailure = getOverlapFailureForTargetPair(tappableRects, maybeOverlappingRects); - if (rectFailure) { - failures.push({ - ...rectFailure, - tapTarget: target.tapTarget, - overlappingTarget: maybeOverlappingTarget.tapTarget, - }); - } - } - }); - - return failures; -} - -/** - * @param {LH.Artifacts.Rect[]} tappableRects - * @param {LH.Artifacts.Rect[]} maybeOverlappingRects - * @return {ClientRectOverlapFailure | null} - */ -function getOverlapFailureForTargetPair(tappableRects, maybeOverlappingRects) { - /** @type ClientRectOverlapFailure | null */ - let greatestFailure = null; - - for (const targetCR of tappableRects) { - const fingerRect = getRectAtCenter(targetCR, FINGER_SIZE_PX); - // Score indicates how much of the finger area overlaps each target when the user - // taps on the center of targetCR - const tapTargetScore = getRectOverlapArea(fingerRect, targetCR); - - for (const maybeOverlappingCR of maybeOverlappingRects) { - const overlappingTargetScore = getRectOverlapArea(fingerRect, maybeOverlappingCR); - - const overlapScoreRatio = overlappingTargetScore / tapTargetScore; - if (overlapScoreRatio < MAX_ACCEPTABLE_OVERLAP_SCORE_RATIO) { - // low score means it's clear that the user tried to tap on the targetCR, - // rather than the other tap target client rect - continue; - } - - // only update our state if this was the biggest failure we've seen for this pair - if (!greatestFailure || overlapScoreRatio > greatestFailure.overlapScoreRatio) { - greatestFailure = { - overlapScoreRatio, - tapTargetScore, - overlappingTargetScore, - }; - } - } - } - return greatestFailure; -} - -/** - * Only report one failure if two targets overlap each other - * @param {TapTargetOverlapFailure[]} overlapFailures - * @return {TapTargetOverlapFailure[]} - */ -function mergeSymmetricFailures(overlapFailures) { - /** @type TapTargetOverlapFailure[] */ - const failuresAfterMerging = []; - - overlapFailures.forEach((failure, overlapFailureIndex) => { - const symmetricFailure = overlapFailures.find(f => - f.tapTarget === failure.overlappingTarget && - f.overlappingTarget === failure.tapTarget - ); - - if (!symmetricFailure) { - failuresAfterMerging.push(failure); - return; - } - - const {overlapScoreRatio: failureOSR} = failure; - const {overlapScoreRatio: symmetricOSR} = symmetricFailure; - // Push if: - // - the current failure has a higher OSR - // - OSRs are the same, and the current failure comes before its symmetric partner in the list - // Otherwise do nothing and let the symmetric partner be pushed later. - if (failureOSR > symmetricOSR || ( - failureOSR === symmetricOSR && - overlapFailureIndex < overlapFailures.indexOf(symmetricFailure) - )) { - failuresAfterMerging.push(failure); - } - }); - - return failuresAfterMerging; -} - -/** - * @param {TapTargetOverlapFailure[]} overlapFailures - * @return {TapTargetTableItem[]} - */ -function getTableItems(overlapFailures) { - const tableItems = overlapFailures.map(failure => { - const largestCR = getLargestRect(failure.tapTarget.clientRects); - const width = Math.floor(largestCR.width); - const height = Math.floor(largestCR.height); - const size = width + 'x' + height; - return { - tapTarget: Audit.makeNodeItem(failure.tapTarget.node), - overlappingTarget: Audit.makeNodeItem(failure.overlappingTarget.node), - tapTargetScore: failure.tapTargetScore, - overlappingTargetScore: failure.overlappingTargetScore, - overlapScoreRatio: failure.overlapScoreRatio, - size, - width, - height, - }; - }); - - tableItems.sort((a, b) => { - return b.overlapScoreRatio - a.overlapScoreRatio; - }); - - return tableItems; -} - -class TapTargets extends Audit { - /** - * @return {LH.Audit.Meta} - */ - static get meta() { - return { - id: 'tap-targets', - title: str_(UIStrings.title), - failureTitle: str_(UIStrings.failureTitle), - description: str_(UIStrings.description), - requiredArtifacts: ['MetaElements', 'TapTargets'], - }; - } - - /** - * @param {LH.Artifacts} artifacts - * @param {LH.Audit.Context} context - * @return {Promise} - */ - static async audit(artifacts, context) { - if (context.settings.formFactor === 'desktop') { - // Tap target sizes aren't important for desktop SEO, so disable the audit there. - // On desktop people also tend to have more precise pointing devices than fingers. - return { - score: 1, - notApplicable: true, - }; - } - - const viewportMeta = await ViewportMeta.request(artifacts.MetaElements, context); - if (!viewportMeta.isMobileOptimized) { - return { - score: 0, - explanation: str_(UIStrings.explanationViewportMetaNotOptimized), - }; - } - - // Augment the targets with padded bounding rects for quick intersection testing. - const boundedTapTargets = getBoundedTapTargets(artifacts.TapTargets); - - const tooSmallTargets = getTooSmallTargets(boundedTapTargets); - const overlapFailures = getAllOverlapFailures(tooSmallTargets, boundedTapTargets); - const overlapFailuresForDisplay = mergeSymmetricFailures(overlapFailures); - const tableItems = getTableItems(overlapFailuresForDisplay); - - /** @type {LH.Audit.Details.Table['headings']} */ - const headings = [ - {key: 'tapTarget', valueType: 'node', label: str_(UIStrings.tapTargetHeader)}, - {key: 'size', valueType: 'text', label: str_(i18n.UIStrings.columnSize)}, - {key: 'overlappingTarget', valueType: 'node', label: str_(UIStrings.overlappingTargetHeader)}, - ]; - - const details = Audit.makeTableDetails(headings, tableItems); - - const tapTargetCount = artifacts.TapTargets.length; - const failingTapTargetCount = new Set(overlapFailures.map(f => f.tapTarget)).size; - const passingTapTargetCount = tapTargetCount - failingTapTargetCount; - - let score = 1; - let passingTapTargetRatio = 1; - if (failingTapTargetCount > 0) { - passingTapTargetRatio = (passingTapTargetCount / tapTargetCount); - // If there are any failures then we don't want the audit to pass, - // so keep the score below 90. - score = passingTapTargetRatio * 0.89; - } - const displayValue = str_(UIStrings.displayValue, {decimalProportion: passingTapTargetRatio}); - - return { - score, - details, - displayValue, - }; - } -} - -TapTargets.FINGER_SIZE_PX = FINGER_SIZE_PX; - -export default TapTargets; -export {UIStrings}; - - -/** @typedef {{ - overlapScoreRatio: number; - tapTargetScore: number; - overlappingTargetScore: number; -}} ClientRectOverlapFailure */ - -/** @typedef {{ - overlapScoreRatio: number; - tapTargetScore: number; - overlappingTargetScore: number; - tapTarget: LH.Artifacts.TapTarget; - overlappingTarget: LH.Artifacts.TapTarget; -}} TapTargetOverlapFailure */ - -/** @typedef {{ - paddedBoundsRect: LH.Artifacts.Rect; - tapTarget: LH.Artifacts.TapTarget; -}} BoundedTapTarget */ - -/** @typedef {{ - tapTarget: LH.Audit.Details.NodeValue; - overlappingTarget: LH.Audit.Details.NodeValue; - size: string; - overlapScoreRatio: number; - height: number; - width: number; - tapTargetScore: number; - overlappingTargetScore: number; -}} TapTargetTableItem */ diff --git a/core/config/default-config.js b/core/config/default-config.js index 5cbac51a61b4..c0102c8f155b 100644 --- a/core/config/default-config.js +++ b/core/config/default-config.js @@ -153,7 +153,6 @@ const defaultConfig = { {id: 'SourceMaps', gatherer: 'source-maps'}, {id: 'Stacks', gatherer: 'stacks'}, {id: 'TagsBlockingFirstPaint', gatherer: 'dobetterweb/tags-blocking-first-paint'}, - {id: 'TapTargets', gatherer: 'seo/tap-targets'}, {id: 'TraceElements', gatherer: 'trace-elements'}, {id: 'ViewportDimensions', gatherer: 'viewport-dimensions'}, {id: 'WebAppManifest', gatherer: 'web-app-manifest'}, @@ -330,7 +329,6 @@ const defaultConfig = { 'seo/crawlable-anchors', 'seo/is-crawlable', 'seo/robots-txt', - 'seo/tap-targets', 'seo/hreflang', 'seo/plugins', 'seo/canonical', @@ -551,6 +549,7 @@ const defaultConfig = { {id: 'skip-link', weight: 3, group: 'a11y-names-labels'}, {id: 'tabindex', weight: 7, group: 'a11y-navigation'}, {id: 'table-duplicate-name', weight: 1, group: 'a11y-tables-lists'}, + {id: 'target-size', weight: 7, group: 'a11y-best-practices'}, {id: 'td-headers-attr', weight: 7, group: 'a11y-tables-lists'}, {id: 'th-has-data-cells', weight: 7, group: 'a11y-tables-lists'}, {id: 'valid-lang', weight: 7, group: 'a11y-language'}, @@ -570,7 +569,6 @@ const defaultConfig = { {id: 'empty-heading', weight: 0, group: 'hidden'}, {id: 'identical-links-same-purpose', weight: 0, group: 'hidden'}, {id: 'landmark-one-main', weight: 0, group: 'hidden'}, - {id: 'target-size', weight: 0, group: 'hidden'}, {id: 'label-content-name-mismatch', weight: 0, group: 'hidden'}, {id: 'table-fake-caption', weight: 0, group: 'hidden'}, {id: 'td-has-header', weight: 0, group: 'hidden'}, @@ -620,7 +618,6 @@ const defaultConfig = { {id: 'canonical', weight: 1, group: 'seo-content'}, {id: 'font-size', weight: 1, group: 'seo-mobile'}, {id: 'plugins', weight: 1, group: 'seo-content'}, - {id: 'tap-targets', weight: 1, group: 'seo-mobile'}, // Manual audits {id: 'structured-data', weight: 0}, ], diff --git a/core/gather/gatherers/seo/tap-targets.js b/core/gather/gatherers/seo/tap-targets.js deleted file mode 100644 index 36537ab0934f..000000000000 --- a/core/gather/gatherers/seo/tap-targets.js +++ /dev/null @@ -1,389 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/* global document, window, getComputedStyle, getElementsInDocument, Node, getNodeDetails, getRectCenterPoint */ - -import BaseGatherer from '../../base-gatherer.js'; -import {pageFunctions} from '../../../lib/page-functions.js'; -import * as RectHelpers from '../../../lib/rect-helpers.js'; - -const TARGET_SELECTORS = [ - 'button', - 'a', - 'input', - 'textarea', - 'select', - 'option', - '[role=button]', - '[role=checkbox]', - '[role=link]', - '[role=menuitem]', - '[role=menuitemcheckbox]', - '[role=menuitemradio]', - '[role=option]', - '[role=scrollbar]', - '[role=slider]', - '[role=spinbutton]', -]; -const tapTargetsSelector = TARGET_SELECTORS.join(','); - -/** - * @param {HTMLElement} element - * @return {boolean} - */ -/* c8 ignore start */ -function elementIsVisible(element) { - return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); -} -/* c8 ignore stop */ - -/** - * @param {Element} element - * @return {LH.Artifacts.Rect[]} - */ -/* c8 ignore start */ -function getClientRects(element) { - const clientRects = Array.from( - element.getClientRects() - ).map(clientRect => { - // Contents of DOMRect get lost when returned from Runtime.evaluate call, - // so we convert them to plain objects. - const {width, height, left, top, right, bottom} = clientRect; - return {width, height, left, top, right, bottom}; - }); - - for (const child of element.children) { - clientRects.push(...getClientRects(child)); - } - - return clientRects; -} -/* c8 ignore stop */ - -/** - * @param {Element} element - * @param {string} tapTargetsSelector - * @return {boolean} - */ -/* c8 ignore start */ -function elementHasAncestorTapTarget(element, tapTargetsSelector) { - if (!element.parentElement) { - return false; - } - if (element.parentElement.matches(tapTargetsSelector)) { - return true; - } - return elementHasAncestorTapTarget(element.parentElement, tapTargetsSelector); -} -/* c8 ignore stop */ - -/** - * @param {Element} element - */ -/* c8 ignore start */ -function hasTextNodeSiblingsFormingTextBlock(element) { - if (!element.parentElement) { - return false; - } - - const parentElement = element.parentElement; - - const nodeText = element.textContent || ''; - const parentText = parentElement.textContent || ''; - if (parentText.length - nodeText.length < 5) { - // Parent text mostly consists of this node, so the parent - // is not a text block container - return false; - } - - for (const sibling of element.parentElement.childNodes) { - if (sibling === element) { - continue; - } - const siblingTextContent = (sibling.textContent || '').trim(); - // Only count text in text nodes so that a series of e.g. buttons isn't counted - // as a text block. - // This works reasonably well, but means we miss text blocks where all text is e.g. - // wrapped in spans - if (sibling.nodeType === Node.TEXT_NODE && siblingTextContent.length > 0) { - return true; - } - } - - return false; -} -/* c8 ignore stop */ - -/** - * Check if element is in a block of text, such as paragraph with a bunch of links in it. - * Makes a reasonable guess, but for example gets it wrong if the element is surrounded by other - * HTML elements instead of direct text nodes. - * @param {Element} element - * @return {boolean} - */ -/* c8 ignore start */ -function elementIsInTextBlock(element) { - const {display} = getComputedStyle(element); - if (display !== 'inline' && display !== 'inline-block') { - return false; - } - - if (hasTextNodeSiblingsFormingTextBlock(element)) { - return true; - } else if (element.parentElement) { - return elementIsInTextBlock(element.parentElement); - } else { - return false; - } -} -/* c8 ignore stop */ - -/** - * @param {Element} el - * @param {{x: number, y: number}} elCenterPoint - */ -/* c8 ignore start */ -function elementCenterIsAtZAxisTop(el, elCenterPoint) { - const viewportHeight = window.innerHeight; - const targetScrollY = Math.floor(elCenterPoint.y / viewportHeight) * viewportHeight; - if (window.scrollY !== targetScrollY) { - window.scrollTo(0, targetScrollY); - } - - const topEl = document.elementFromPoint( - elCenterPoint.x, - elCenterPoint.y - window.scrollY - ); - - return topEl === el || el.contains(topEl); -} -/* c8 ignore stop */ - -/** - * Finds all position sticky/absolute elements on the page and adds a class - * that disables pointer events on them. - * @param {string} className - * @return {() => void} - undo function to re-enable pointer events - */ -/* c8 ignore start */ -function disableFixedAndStickyElementPointerEvents(className) { - document.querySelectorAll('*').forEach(el => { - const position = getComputedStyle(el).position; - if (position === 'fixed' || position === 'sticky') { - el.classList.add(className); - } - }); - - return function undo() { - Array.from(document.getElementsByClassName(className)).forEach(el => { - el.classList.remove(className); - }); - }; -} -/* c8 ignore stop */ - -/** - * @param {string} tapTargetsSelector - * @param {string} className - * @return {LH.Artifacts.TapTarget[]} - */ -/* c8 ignore start */ -function gatherTapTargets(tapTargetsSelector, className) { - /** @type {LH.Artifacts.TapTarget[]} */ - const targets = []; - - // Capture element positions relative to the top of the page - window.scrollTo(0, 0); - - /** @type {HTMLElement[]} */ - // @ts-expect-error - getElementsInDocument put into scope via stringification - const tapTargetElements = getElementsInDocument(tapTargetsSelector); - - /** @type {{ - tapTargetElement: Element, - clientRects: LH.Artifacts.Rect[] - }[]} */ - const tapTargetsWithClientRects = []; - tapTargetElements.forEach(tapTargetElement => { - // Filter out tap targets that are likely to cause false failures: - if (elementHasAncestorTapTarget(tapTargetElement, tapTargetsSelector)) { - // This is usually intentional, either the tap targets trigger the same action - // or there's a child with a related action (like a delete button for an item) - return; - } - if (elementIsInTextBlock(tapTargetElement)) { - // Links inside text blocks cause a lot of failures, and there's also an exception for them - // in the Web Content Accessibility Guidelines https://www.w3.org/TR/WCAG21/#target-size - return; - } - if (!elementIsVisible(tapTargetElement)) { - return; - } - - tapTargetsWithClientRects.push({ - tapTargetElement, - clientRects: getClientRects(tapTargetElement), - }); - }); - - // Disable pointer events so that tap targets below them don't get - // detected as non-tappable (they are tappable, just not while the viewport - // is at the current scroll position) - const reenableFixedAndStickyElementPointerEvents = - disableFixedAndStickyElementPointerEvents(className); - - /** @type {{ - tapTargetElement: Element, - visibleClientRects: LH.Artifacts.Rect[] - }[]} */ - const tapTargetsWithVisibleClientRects = []; - // We use separate loop here to get visible client rects because that involves - // scrolling around the page for elementCenterIsAtZAxisTop, which would affect the - // client rect positions. - tapTargetsWithClientRects.forEach(({tapTargetElement, clientRects}) => { - // Filter out empty client rects - let visibleClientRects = clientRects.filter(cr => cr.width !== 0 && cr.height !== 0); - - // Filter out client rects that are invisible, e.g because they are in a position absolute element - // with a lower z-index than the main content. - // This will also filter out all position fixed or sticky tap targets elements because we disable pointer - // events on them before running this. That's the correct behavior because whether a position fixed/stick - // element overlaps with another tap target depends on the scroll position. - visibleClientRects = visibleClientRects.filter(rect => { - // Just checking the center can cause false failures for large partially hidden tap targets, - // but that should be a rare edge case - // @ts-expect-error - put into scope via stringification - const rectCenterPoint = getRectCenterPoint(rect); - return elementCenterIsAtZAxisTop(tapTargetElement, rectCenterPoint); - }); - - if (visibleClientRects.length > 0) { - tapTargetsWithVisibleClientRects.push({ - tapTargetElement, - visibleClientRects, - }); - } - }); - - for (const {tapTargetElement, visibleClientRects} of tapTargetsWithVisibleClientRects) { - targets.push({ - clientRects: visibleClientRects, - href: /** @type {HTMLAnchorElement} */(tapTargetElement)['href'] || '', - // @ts-expect-error - getNodeDetails put into scope via stringification - node: getNodeDetails(tapTargetElement), - }); - } - - reenableFixedAndStickyElementPointerEvents(); - - return targets; -} - -/** - * @param {string} tapTargetsSelector - * @param {string} className - * @return {LH.Artifacts.TapTarget[]} - */ -function gatherTapTargetsAndResetScroll(tapTargetsSelector, className) { - const originalScrollPosition = { - x: window.scrollX, - y: window.scrollY, - }; - - try { - return gatherTapTargets(tapTargetsSelector, className); - } finally { - window.scrollTo(originalScrollPosition.x, originalScrollPosition.y); - } -} -/* c8 ignore stop */ - -class TapTargets extends BaseGatherer { - constructor() { - super(); - /** - * This needs to be in the constructor. - * https://github.com/GoogleChrome/lighthouse/issues/12134 - * @type {LH.Gatherer.GathererMeta} - */ - this.meta = { - supportedModes: ['snapshot', 'navigation'], - }; - } - - /** - * @param {LH.Gatherer.ProtocolSession} session - * @param {string} className - * @return {Promise} - */ - async addStyleRule(session, className) { - const frameTreeResponse = await session.sendCommand('Page.getFrameTree'); - const {styleSheetId} = await session.sendCommand('CSS.createStyleSheet', { - frameId: frameTreeResponse.frameTree.frame.id, - }); - const ruleText = `.${className} { pointer-events: none !important }`; - await session.sendCommand('CSS.setStyleSheetText', { - styleSheetId, - text: ruleText, - }); - return styleSheetId; - } - - /** - * @param {LH.Gatherer.ProtocolSession} session - * @param {string} styleSheetId - */ - async removeStyleRule(session, styleSheetId) { - await session.sendCommand('CSS.setStyleSheetText', { - styleSheetId, - text: '', - }); - } - - /** - * @param {LH.Gatherer.Context} passContext - * @return {Promise} All visible tap targets with their positions and sizes - */ - async getArtifact(passContext) { - const session = passContext.driver.defaultSession; - await session.sendCommand('DOM.enable'); - await session.sendCommand('CSS.enable'); - - const className = 'lighthouse-disable-pointer-events'; - const styleSheetId = await this.addStyleRule(session, className); - - const tapTargets = - await passContext.driver.executionContext.evaluate(gatherTapTargetsAndResetScroll, { - args: [tapTargetsSelector, className], - useIsolation: true, - deps: [ - pageFunctions.getNodeDetails, - pageFunctions.getElementsInDocument, - disableFixedAndStickyElementPointerEvents, - elementIsVisible, - elementHasAncestorTapTarget, - elementCenterIsAtZAxisTop, - getClientRects, - hasTextNodeSiblingsFormingTextBlock, - elementIsInTextBlock, - RectHelpers.getRectCenterPoint, - pageFunctions.getNodePath, - pageFunctions.getNodeSelector, - pageFunctions.getNodeLabel, - gatherTapTargets, - ], - }); - - await this.removeStyleRule(session, styleSheetId); - - await session.sendCommand('CSS.disable'); - await session.sendCommand('DOM.disable'); - - return tapTargets; - } -} - -export default TapTargets; diff --git a/core/test/audits/seo/tap-targets-test.js b/core/test/audits/seo/tap-targets-test.js deleted file mode 100644 index 7414cd0fd285..000000000000 --- a/core/test/audits/seo/tap-targets-test.js +++ /dev/null @@ -1,235 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert/strict'; - -import TapTargetsAudit from '../../../audits/seo/tap-targets.js'; -import * as constants from '../../../config/constants.js'; - -/** @param {LH.SharedFlagsSettings['formFactor']} formFactor */ -const getFakeContext = (formFactor = 'mobile') => ({ - computedCache: new Map(), - settings: { - formFactor: formFactor, - screenEmulation: constants.screenEmulationMetrics[formFactor], - }, -}); - -function auditTapTargets(tapTargets, {MetaElements = [{ - name: 'viewport', - content: 'width=device-width', -}]} = {}, context = getFakeContext()) { - const artifacts = { - TapTargets: tapTargets, - MetaElements, - }; - - return TapTargetsAudit.audit(artifacts, context); -} - -const tapTargetSize = 10; -const minimalOverlapCausingDistance = (TapTargetsAudit.FINGER_SIZE_PX - tapTargetSize) / 2; -// 3px means it'll have 10x3=30px overlap with the finger, which is 30% of the tap targets own score -// and the failure cutoff is 25% -const pxOverlappedByFinger = 3; -const minimalFailingOverlapCausingDistance = minimalOverlapCausingDistance + pxOverlappedByFinger; - -function getBorderlineTapTargets(options = {}) { - function makeClientRects({x, y}) { - return { - left: x, - top: y, - width: tapTargetSize, - height: tapTargetSize, - bottom: y + tapTargetSize, - right: x + tapTargetSize, - }; - } - - const mainTapTarget = { - node: { - snippet: '
', - }, - clientRects: [ - makeClientRects({ - x: 0, - y: 0, - }), - ], - }; - const tapTargetBelow = { - node: { - snippet: '', - }, - clientRects: [ - makeClientRects({ - x: 0, - y: mainTapTarget.clientRects[0].top + TapTargetsAudit.FINGER_SIZE_PX, - }), - ], - }; - const tapTargetToTheRight = { - node: { - snippet: '', - }, - clientRects: [ - makeClientRects({ - x: mainTapTarget.clientRects[0].left + TapTargetsAudit.FINGER_SIZE_PX, - y: 0, - }), - ], - }; - - if (options.reduceRightWidth) { - tapTargetToTheRight.clientRects[0].width -= 1; - tapTargetToTheRight.clientRects[0].right -= 1; - } - if (options.increaseRightWidth) { - tapTargetToTheRight.clientRects[0].width += 10; - tapTargetToTheRight.clientRects[0].right += 10; - } - - const targets = [mainTapTarget, tapTargetBelow, tapTargetToTheRight]; - - const overlapAmount = minimalFailingOverlapCausingDistance; - if (options.overlapRight) { - tapTargetToTheRight.clientRects[0].left -= overlapAmount; - tapTargetToTheRight.clientRects[0].right -= overlapAmount; - } - if (options.overlapBelow) { - tapTargetBelow.clientRects[0].top -= overlapAmount; - tapTargetBelow.clientRects[0].bottom -= overlapAmount; - } - if (options.addFullyContainedTapTarget) { - targets.push({ - snippet: '', - clientRects: [ - makeClientRects({ - x: 0, - y: 0, - }), - ], - }); - } - if (options.overlapSecondClientRect) { - mainTapTarget.clientRects.push( - makeClientRects({ - x: 0, - y: overlapAmount, - }) - ); - } - - return targets; -} - -describe('SEO: Tap targets audit', () => { - it('passes when there are no tap targets', async () => { - const auditResult = await auditTapTargets([]); - assert.equal(auditResult.score, 1); - expect(auditResult.displayValue).toBeDisplayString('100% appropriately sized tap targets'); - assert.equal(auditResult.score, 1); - }); - - it('passes when tap targets don\'t overlap', async () => { - const auditResult = await auditTapTargets(getBorderlineTapTargets()); - assert.equal(auditResult.score, 1); - }); - - it('passes when a target is fully contained in an overlapping target', async () => { - const auditResult = await auditTapTargets(getBorderlineTapTargets({ - addFullyContainedTapTarget: true, - })); - assert.equal(auditResult.score, 1); - }); - - it('fails if two tap targets overlaps each other horizontally', async () => { - const auditResult = await auditTapTargets( - getBorderlineTapTargets({ - overlapRight: true, - }) - ); - assert.equal(auditResult.score.toFixed(3), '0.297'); - assert.equal(Math.round(auditResult.score * 100), 30); - const failure = auditResult.details.items[0]; - assert.equal(failure.tapTarget.snippet, '
'); - assert.equal(failure.overlappingTarget.snippet, ''); - assert.equal(failure.size, '10x10'); - // Includes data for debugging/adjusting the scoring logic later on - assert.equal(failure.tapTargetScore, tapTargetSize * tapTargetSize); - assert.equal(failure.overlappingTargetScore, tapTargetSize * pxOverlappedByFinger); - assert.equal(failure.overlapScoreRatio, 0.3); - assert.equal(failure.width, 10); - assert.equal(failure.height, 10); - }); - - it('fails if a tap target overlaps vertically', async () => { - const auditResult = await auditTapTargets( - getBorderlineTapTargets({ - overlapBelow: true, - }) - ); - assert.equal(auditResult.score.toFixed(3), '0.297'); - }); - - it('fails when one of the client rects overlaps', async () => { - const auditResult = await auditTapTargets( - getBorderlineTapTargets({ - overlapSecondClientRect: true, - }) - ); - assert.equal(auditResult.score.toFixed(3), '0.297'); - }); - - it('reports 2 items if a target overlapped both vertically and horizontally', async () => { - // Main is overlapped by right + below, right and below are each overlapped by main - const auditResult = await auditTapTargets( - getBorderlineTapTargets({ - overlapRight: true, - reduceRightWidth: true, - overlapBelow: true, - }) - ); - assert.equal(Math.round(auditResult.score * 100), 0); // all tap targets are overlapped by something - const failures = auditResult.details.items; - assert.equal(failures.length, 2); - // Right and Main overlap each other, but Right has a worse score because it's smaller - // so it's the failure that appears in the report - assert.equal(failures[0].tapTarget.snippet, ''); - }); - - it('reports 1 failure if only one tap target involved in an overlap fails', async () => { - const auditResult = await auditTapTargets( - getBorderlineTapTargets({ - overlapRight: true, - increaseRightWidth: true, - }) - ); - assert.equal(Math.round(auditResult.score * 100), 59); - const failures = auditResult.details.items; - //
fails, but doesn't - assert.equal(failures[0].tapTarget.snippet, '
'); - }); - - it('fails if no meta viewport tag is provided', async () => { - const auditResult = await auditTapTargets([], {MetaElements: []}); - assert.equal(auditResult.score, 0); - - expect(auditResult.explanation).toBeDisplayString( - /* eslint-disable max-len */ - 'Tap targets are too small because there\'s no viewport meta tag optimized for mobile screens'); - }); - - it('is not applicable on desktop', async () => { - const desktopContext = getFakeContext('desktop'); - - const auditResult = await auditTapTargets(getBorderlineTapTargets({ - overlapSecondClientRect: true, - }), undefined, desktopContext); - assert.equal(auditResult.score, 1); - assert.equal(auditResult.notApplicable, true); - }); -}); diff --git a/core/test/fixtures/user-flows/reports/sample-flow-result.json b/core/test/fixtures/user-flows/reports/sample-flow-result.json index ae01fbf2752b..0fdde8b71e31 100644 --- a/core/test/fixtures/user-flows/reports/sample-flow-result.json +++ b/core/test/fixtures/user-flows/reports/sample-flow-result.json @@ -3705,19 +3705,6 @@ "score": null, "scoreDisplayMode": "notApplicable" }, - "tap-targets": { - "id": "tap-targets", - "title": "Tap targets are sized appropriately", - "description": "Interactive elements like buttons and links should be large enough (48x48px), or have enough space around them, to be easy enough to tap without overlapping onto other elements. [Learn more about tap targets](https://developer.chrome.com/docs/lighthouse/seo/tap-targets/).", - "score": 1, - "scoreDisplayMode": "binary", - "displayValue": "100% appropriately sized tap targets", - "details": { - "type": "table", - "headings": [], - "items": [] - } - }, "hreflang": { "id": "hreflang", "title": "Document has a valid `hreflang`", @@ -4388,6 +4375,11 @@ "weight": 1, "group": "a11y-tables-lists" }, + { + "id": "target-size", + "weight": 0, + "group": "a11y-best-practices" + }, { "id": "td-headers-attr", "weight": 0, @@ -4463,11 +4455,6 @@ "weight": 0, "group": "hidden" }, - { - "id": "target-size", - "weight": 0, - "group": "hidden" - }, { "id": "label-content-name-mismatch", "weight": 0, @@ -4648,11 +4635,6 @@ "weight": 1, "group": "seo-content" }, - { - "id": "tap-targets", - "weight": 1, - "group": "seo-mobile" - }, { "id": "structured-data", "weight": 0 @@ -6970,48 +6952,42 @@ }, { "startTime": 278, - "name": "lh:audit:tap-targets", - "duration": 1, - "entryType": "measure" - }, - { - "startTime": 279, "name": "lh:audit:hreflang", "duration": 1, "entryType": "measure" }, { - "startTime": 280, + "startTime": 279, "name": "lh:audit:plugins", "duration": 1, "entryType": "measure" }, { - "startTime": 281, + "startTime": 280, "name": "lh:audit:canonical", "duration": 1, "entryType": "measure" }, { - "startTime": 282, + "startTime": 281, "name": "lh:audit:structured-data", "duration": 1, "entryType": "measure" }, { - "startTime": 283, + "startTime": 282, "name": "lh:audit:bf-cache", "duration": 1, "entryType": "measure" }, { - "startTime": 284, + "startTime": 283, "name": "lh:runner:generate", "duration": 1, "entryType": "measure" } ], - "total": 285 + "total": 284 }, "i18n": { "rendererFormattedStrings": { @@ -8259,20 +8235,6 @@ "core/audits/seo/robots-txt.js | description": [ "audits[robots-txt].description" ], - "core/audits/seo/tap-targets.js | title": [ - "audits[tap-targets].title" - ], - "core/audits/seo/tap-targets.js | description": [ - "audits[tap-targets].description" - ], - "core/audits/seo/tap-targets.js | displayValue": [ - { - "values": { - "decimalProportion": 1 - }, - "path": "audits[tap-targets].displayValue" - } - ], "core/audits/seo/hreflang.js | title": [ "audits.hreflang.title" ], @@ -14959,76 +14921,6 @@ "score": null, "scoreDisplayMode": "notApplicable" }, - "tap-targets": { - "id": "tap-targets", - "title": "Tap targets are not sized appropriately", - "description": "Interactive elements like buttons and links should be large enough (48x48px), or have enough space around them, to be easy enough to tap without overlapping onto other elements. [Learn more about tap targets](https://developer.chrome.com/docs/lighthouse/seo/tap-targets/).", - "score": 0.76, - "scoreDisplayMode": "binary", - "displayValue": "86% appropriately sized tap targets", - "details": { - "type": "table", - "headings": [ - { - "key": "tapTarget", - "valueType": "node", - "label": "Tap Target" - }, - { - "key": "size", - "valueType": "text", - "label": "Size" - }, - { - "key": "overlappingTarget", - "valueType": "node", - "label": "Overlapping Target" - } - ], - "items": [ - { - "tapTarget": { - "type": "node", - "lhId": "1-50-A", - "path": "1,HTML,1,BODY,0,DIV,0,DIV,5,FOOTER,2,DIV,0,DIV,0,A", - "selector": "footer.w-full > div.w-full > div > a", - "boundingRect": { - "top": 608, - "bottom": 620, - "left": 147, - "right": 213, - "width": 66, - "height": 12 - }, - "snippet": "
", - "nodeLabel": "Terms of Service" - }, - "overlappingTarget": { - "type": "node", - "lhId": "1-51-A", - "path": "1,HTML,1,BODY,0,DIV,0,DIV,5,FOOTER,2,DIV,1,DIV,0,A", - "selector": "footer.w-full > div.w-full > div > a", - "boundingRect": { - "top": 620, - "bottom": 632, - "left": 153, - "right": 207, - "width": 54, - "height": 12 - }, - "snippet": "", - "nodeLabel": "Privacy Policy" - }, - "tapTargetScore": 552, - "overlappingTargetScore": 552, - "overlapScoreRatio": 1, - "size": "65x11", - "width": 65, - "height": 11 - } - ] - } - }, "plugins": { "id": "plugins", "title": "Document avoids plugins", @@ -15395,6 +15287,11 @@ "weight": 1, "group": "a11y-tables-lists" }, + { + "id": "target-size", + "weight": 0, + "group": "a11y-best-practices" + }, { "id": "td-headers-attr", "weight": 0, @@ -15470,11 +15367,6 @@ "weight": 0, "group": "hidden" }, - { - "id": "target-size", - "weight": 0, - "group": "hidden" - }, { "id": "label-content-name-mismatch", "weight": 0, @@ -15585,18 +15477,13 @@ "weight": 1, "group": "seo-content" }, - { - "id": "tap-targets", - "weight": 1, - "group": "seo-mobile" - }, { "id": "structured-data", "weight": 0 } ], "id": "seo", - "score": 0.86 + "score": 0.88 } }, "categoryGroups": { @@ -17053,30 +16940,24 @@ }, { "startTime": 112, - "name": "lh:audit:tap-targets", - "duration": 1, - "entryType": "measure" - }, - { - "startTime": 113, "name": "lh:audit:plugins", "duration": 1, "entryType": "measure" }, { - "startTime": 114, + "startTime": 113, "name": "lh:audit:structured-data", "duration": 1, "entryType": "measure" }, { - "startTime": 115, + "startTime": 114, "name": "lh:runner:generate", "duration": 1, "entryType": "measure" } ], - "total": 116 + "total": 115 }, "i18n": { "rendererFormattedStrings": { @@ -17670,29 +17551,6 @@ "core/audits/seo/robots-txt.js | description": [ "audits[robots-txt].description" ], - "core/audits/seo/tap-targets.js | failureTitle": [ - "audits[tap-targets].title" - ], - "core/audits/seo/tap-targets.js | description": [ - "audits[tap-targets].description" - ], - "core/audits/seo/tap-targets.js | displayValue": [ - { - "values": { - "decimalProportion": 0.8571428571428571 - }, - "path": "audits[tap-targets].displayValue" - } - ], - "core/audits/seo/tap-targets.js | tapTargetHeader": [ - "audits[tap-targets].details.headings[0].label" - ], - "core/lib/i18n/i18n.js | columnSize": [ - "audits[tap-targets].details.headings[1].label" - ], - "core/audits/seo/tap-targets.js | overlappingTargetHeader": [ - "audits[tap-targets].details.headings[2].label" - ], "core/audits/seo/plugins.js | title": [ "audits.plugins.title" ], @@ -21611,76 +21469,6 @@ "score": null, "scoreDisplayMode": "notApplicable" }, - "tap-targets": { - "id": "tap-targets", - "title": "Tap targets are not sized appropriately", - "description": "Interactive elements like buttons and links should be large enough (48x48px), or have enough space around them, to be easy enough to tap without overlapping onto other elements. [Learn more about tap targets](https://developer.chrome.com/docs/lighthouse/seo/tap-targets/).", - "score": 0.67, - "scoreDisplayMode": "binary", - "displayValue": "75% appropriately sized tap targets", - "details": { - "type": "table", - "headings": [ - { - "key": "tapTarget", - "valueType": "node", - "label": "Tap Target" - }, - { - "key": "size", - "valueType": "text", - "label": "Size" - }, - { - "key": "overlappingTarget", - "valueType": "node", - "label": "Overlapping Target" - } - ], - "items": [ - { - "tapTarget": { - "type": "node", - "lhId": "1-11-A", - "path": "1,HTML,1,BODY,0,DIV,0,DIV,5,FOOTER,2,DIV,0,DIV,0,A", - "selector": "footer.w-full > div.w-full > div > a", - "boundingRect": { - "top": 608, - "bottom": 620, - "left": 147, - "right": 213, - "width": 66, - "height": 12 - }, - "snippet": "", - "nodeLabel": "Terms of Service" - }, - "overlappingTarget": { - "type": "node", - "lhId": "1-12-A", - "path": "1,HTML,1,BODY,0,DIV,0,DIV,5,FOOTER,2,DIV,1,DIV,0,A", - "selector": "footer.w-full > div.w-full > div > a", - "boundingRect": { - "top": 620, - "bottom": 632, - "left": 153, - "right": 207, - "width": 54, - "height": 12 - }, - "snippet": "", - "nodeLabel": "Privacy Policy" - }, - "tapTargetScore": 552, - "overlappingTargetScore": 552, - "overlapScoreRatio": 1, - "size": "65x11", - "width": 65, - "height": 11 - } - ] - } - }, "hreflang": { "id": "hreflang", "title": "Document has a valid `hreflang`", @@ -22351,6 +22139,11 @@ "weight": 1, "group": "a11y-tables-lists" }, + { + "id": "target-size", + "weight": 0, + "group": "a11y-best-practices" + }, { "id": "td-headers-attr", "weight": 0, @@ -22426,11 +22219,6 @@ "weight": 0, "group": "hidden" }, - { - "id": "target-size", - "weight": 0, - "group": "hidden" - }, { "id": "label-content-name-mismatch", "weight": 0, @@ -22611,18 +22399,13 @@ "weight": 1, "group": "seo-content" }, - { - "id": "tap-targets", - "weight": 1, - "group": "seo-mobile" - }, { "id": "structured-data", "weight": 0 } ], "id": "seo", - "score": 0.89 + "score": 0.91 }, "pwa": { "title": "PWA", @@ -24907,48 +24690,42 @@ }, { "startTime": 275, - "name": "lh:audit:tap-targets", - "duration": 1, - "entryType": "measure" - }, - { - "startTime": 276, "name": "lh:audit:hreflang", "duration": 1, "entryType": "measure" }, { - "startTime": 277, + "startTime": 276, "name": "lh:audit:plugins", "duration": 1, "entryType": "measure" }, { - "startTime": 278, + "startTime": 277, "name": "lh:audit:canonical", "duration": 1, "entryType": "measure" }, { - "startTime": 279, + "startTime": 278, "name": "lh:audit:structured-data", "duration": 1, "entryType": "measure" }, { - "startTime": 280, + "startTime": 279, "name": "lh:audit:bf-cache", "duration": 1, "entryType": "measure" }, { - "startTime": 281, + "startTime": 280, "name": "lh:runner:generate", "duration": 1, "entryType": "measure" } ], - "total": 282 + "total": 281 }, "i18n": { "rendererFormattedStrings": { @@ -26196,29 +25973,6 @@ "core/audits/seo/robots-txt.js | description": [ "audits[robots-txt].description" ], - "core/audits/seo/tap-targets.js | failureTitle": [ - "audits[tap-targets].title" - ], - "core/audits/seo/tap-targets.js | description": [ - "audits[tap-targets].description" - ], - "core/audits/seo/tap-targets.js | displayValue": [ - { - "values": { - "decimalProportion": 0.75 - }, - "path": "audits[tap-targets].displayValue" - } - ], - "core/audits/seo/tap-targets.js | tapTargetHeader": [ - "audits[tap-targets].details.headings[0].label" - ], - "core/lib/i18n/i18n.js | columnSize": [ - "audits[tap-targets].details.headings[1].label" - ], - "core/audits/seo/tap-targets.js | overlappingTargetHeader": [ - "audits[tap-targets].details.headings[2].label" - ], "core/audits/seo/hreflang.js | title": [ "audits.hreflang.title" ], diff --git a/core/test/results/sample_v2.json b/core/test/results/sample_v2.json index 5b220a4a6213..32870a7e1bdb 100644 --- a/core/test/results/sample_v2.json +++ b/core/test/results/sample_v2.json @@ -5641,76 +5641,6 @@ "score": null, "scoreDisplayMode": "notApplicable" }, - "tap-targets": { - "id": "tap-targets", - "title": "Tap targets are not sized appropriately", - "description": "Interactive elements like buttons and links should be large enough (48x48px), or have enough space around them, to be easy enough to tap without overlapping onto other elements. [Learn more about tap targets](https://developer.chrome.com/docs/lighthouse/seo/tap-targets/).", - "score": 0, - "scoreDisplayMode": "binary", - "displayValue": "0% appropriately sized tap targets", - "details": { - "type": "table", - "headings": [ - { - "key": "tapTarget", - "valueType": "node", - "label": "Tap Target" - }, - { - "key": "size", - "valueType": "text", - "label": "Size" - }, - { - "key": "overlappingTarget", - "valueType": "node", - "label": "Overlapping Target" - } - ], - "items": [ - { - "tapTarget": { - "type": "node", - "lhId": "5-46-BUTTON", - "path": "3,HTML,1,BODY,30,BUTTON", - "selector": "body > button.small-button", - "boundingRect": { - "top": 484, - "bottom": 505, - "left": 8, - "right": 208, - "width": 200, - "height": 21 - }, - "snippet": "