Skip to content

Commit 63ea940

Browse files
JDOM10KeylaMunnoz
andauthored
feat(dom): toHaveAccessibleDescription (#174)
Co-authored-by: KeylaMunnoz <109166857+KeylaMunnoz@users.noreply.github.com>
1 parent bc37b05 commit 63ea940

6 files changed

Lines changed: 107 additions & 132 deletions

File tree

‎packages/dom/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"test": "NODE_ENV=test mocha"
3636
},
3737
"dependencies": {
38+
"dom-accessibility-api": "^0.5.16",
3839
"fast-deep-equal": "^3.1.3",
3940
"tslib": "^2.8.1"
4041
},

‎packages/dom/src/lib/ElementAssertion.ts‎

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { Assertion, AssertionError } from "@assertive-ts/core";
2+
import { computeAccessibleDescription } from "dom-accessibility-api";
23
import equal from "fast-deep-equal";
34

4-
import { getAccessibleDescription, isValidAriaPressed } from "./helpers/accessibility";
5+
import {
6+
describeAccessibleExpectation,
7+
isValidAriaPressed,
8+
matchesAccessibleExpectation,
9+
} from "./helpers/accessibility";
510
import { isButtonElement, isElementEmpty, normalizeHtml } from "./helpers/dom";
611
import { getExpectedAndReceivedStyles } from "./helpers/styles";
712

@@ -293,63 +298,51 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
293298
/**
294299
* Asserts that the element has an accessible description.
295300
*
296-
* The accessible description is computed from the `aria-describedby`
297-
* attribute, which references one or more elements by ID. The text
298-
* content of those elements is combined to form the description.
301+
* The accessible description is computed following the
302+
* [accname](https://www.w3.org/TR/accname/) specification, taking
303+
* into account `aria-describedby`, `aria-description`, and `title`,
304+
* among others.
299305
*
300306
* @example
301307
* ```
302308
* // Check if element has any description
303-
* expect(element).toHaveDescription();
309+
* expect(element).toHaveAccessibleDescription();
304310
*
305311
* // Check if element has specific description text
306-
* expect(element).toHaveDescription('Expected description text');
312+
* expect(element).toHaveAccessibleDescription("Expected description text");
307313
*
308314
* // Check if element description matches a regex pattern
309-
* expect(element).toHaveDescription(/description pattern/i);
315+
* expect(element).toHaveAccessibleDescription(/description pattern/i);
310316
* ```
311317
*
312318
* @param expectedDescription
313319
* - Optional expected description (string or RegExp).
314320
* @returns the assertion instance.
315321
*/
316-
317-
public toHaveDescription(expectedDescription?: RegExp | string): this {
318-
const description = getAccessibleDescription(this.actual);
322+
public toHaveAccessibleDescription(expectedDescription?: RegExp | string): this {
323+
const description = computeAccessibleDescription(this.actual);
319324
const hasExpectedValue = expectedDescription !== undefined;
320325

321-
const matchesExpectation = (desc: string): boolean => {
322-
if (!hasExpectedValue) {
323-
return Boolean(desc);
324-
}
325-
return expectedDescription instanceof RegExp
326-
? expectedDescription.test(desc)
327-
: desc === expectedDescription;
328-
};
329-
330-
const formatExpectation = (isRegExp: boolean): string =>
331-
isRegExp ? `matching ${expectedDescription}` : `"${expectedDescription}"`;
326+
const expectation = describeAccessibleExpectation(expectedDescription);
332327

333328
const error = new AssertionError({
334329
actual: description,
335330
expected: expectedDescription,
336331
message: hasExpectedValue
337-
? `Expected the element to have description ${formatExpectation(expectedDescription instanceof RegExp)}, `
338-
+ `but received "${description}"`
339-
: "Expected the element to have a description",
332+
? `Expected the element to have accessible description ${expectation}, but received "${description}"`
333+
: "Expected the element to have an accessible description",
340334
});
341335

342336
const invertedError = new AssertionError({
343337
actual: description,
344338
expected: expectedDescription,
345339
message: hasExpectedValue
346-
? `Expected the element NOT to have description ${formatExpectation(expectedDescription instanceof RegExp)}, `
347-
+ `but received "${description}"`
348-
: `Expected the element NOT to have a description, but received "${description}"`,
340+
? `Expected the element NOT to have accessible description ${expectation}, but received "${description}"`
341+
: `Expected the element NOT to have an accessible description, but received "${description}"`,
349342
});
350343

351344
return this.execute({
352-
assertWhen: matchesExpectation(description),
345+
assertWhen: matchesAccessibleExpectation(description, expectedDescription),
353346
error,
354347
invertedError,
355348
});
Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,24 @@
1-
function normalizeText(text: string): string {
2-
return text.replace(/\s+/g, " ").trim();
1+
export function isValidAriaPressed(element: Element): boolean {
2+
const pressedAttribute = element.getAttribute("aria-pressed");
3+
return pressedAttribute !== null && ["true", "false", "mixed"].includes(pressedAttribute);
34
}
45

5-
export function getAccessibleDescription(actual: Element): string {
6-
const ariaDescribedBy = actual.getAttribute("aria-describedby");
7-
8-
if (!ariaDescribedBy) {
9-
return "";
6+
export function matchesAccessibleExpectation(actual: string, expected?: RegExp | string): boolean {
7+
if (expected === undefined) {
8+
return Boolean(actual);
109
}
1110

12-
const descriptionIds = ariaDescribedBy.split(/\s+/).filter(Boolean);
13-
14-
const getElementText = (id: string): null | string => {
15-
const element = actual.ownerDocument.getElementById(id);
16-
17-
if (!element || !element.textContent) {
18-
return null;
19-
}
20-
21-
return element.textContent;
22-
};
23-
24-
const combinedText = descriptionIds
25-
.map(getElementText)
26-
.filter((text): text is string => text !== null)
27-
.join(" ");
28-
29-
return normalizeText(combinedText);
11+
return expected instanceof RegExp
12+
? expected.test(actual)
13+
: actual === expected;
3014
}
3115

32-
export function isValidAriaPressed(element: Element): boolean {
33-
const pressedAttribute = element.getAttribute("aria-pressed");
34-
return pressedAttribute !== null && ["true", "false", "mixed"].includes(pressedAttribute);
16+
export function describeAccessibleExpectation(expected?: RegExp | string): string {
17+
if (expected === undefined) {
18+
return "";
19+
}
20+
21+
return expected instanceof RegExp
22+
? `matching ${expected}`
23+
: `"${expected}"`;
3524
}

‎packages/dom/src/lib/helpers/dom.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export function isElementEmpty(element: Element): boolean {
55
return nonCommentChildNodes.length === 0;
66
}
77

8+
// Callers are internal: the public assertions already reject non-string
9+
// and empty `htmlText` values before reaching here.
810
export function normalizeHtml(htmlText: string, ownerDocument: Document): string {
911
const div = ownerDocument.createElement("div");
1012
div.innerHTML = htmlText;

‎packages/dom/test/unit/lib/ElementAssertion.test.tsx‎

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -469,19 +469,22 @@ describe("[Unit] ElementAssertion.test.ts", () => {
469469
});
470470
});
471471

472-
describe(".toHaveDescription", () => {
472+
describe(".toHaveAccessibleDescription", () => {
473473
context("when checking for any description", () => {
474474
context("when the element has a description", () => {
475475
it("returns the assertion instance", () => {
476476
const { getByTestId } = render(<DescriptionTestComponent />);
477477
const button = getByTestId("button-single");
478478
const test = new ElementAssertion(button);
479479

480-
expect(test.toHaveDescription()).toBeEqual(test);
480+
expect(test.toHaveAccessibleDescription()).toBeEqual(test);
481481

482-
expect(() => test.not.toHaveDescription())
482+
expect(() => test.not.toHaveAccessibleDescription())
483483
.toThrowError(AssertionError)
484-
.toHaveMessage('Expected the element NOT to have a description, but received "This is a description"');
484+
.toHaveMessage(
485+
"Expected the element NOT to have an accessible description, "
486+
+ 'but received "This is a description"',
487+
);
485488
});
486489
});
487490

@@ -491,11 +494,11 @@ describe("[Unit] ElementAssertion.test.ts", () => {
491494
const button = getByTestId("button-no-description");
492495
const test = new ElementAssertion(button);
493496

494-
expect(() => test.toHaveDescription())
497+
expect(() => test.toHaveAccessibleDescription())
495498
.toThrowError(AssertionError)
496-
.toHaveMessage("Expected the element to have a description");
499+
.toHaveMessage("Expected the element to have an accessible description");
497500

498-
expect(test.not.toHaveDescription()).toBeEqual(test);
501+
expect(test.not.toHaveAccessibleDescription()).toBeEqual(test);
499502
});
500503
});
501504
});
@@ -507,12 +510,12 @@ describe("[Unit] ElementAssertion.test.ts", () => {
507510
const button = getByTestId("button-single");
508511
const test = new ElementAssertion(button);
509512

510-
expect(test.toHaveDescription("This is a description")).toBeEqual(test);
513+
expect(test.toHaveAccessibleDescription("This is a description")).toBeEqual(test);
511514

512-
expect(() => test.not.toHaveDescription("This is a description"))
515+
expect(() => test.not.toHaveAccessibleDescription("This is a description"))
513516
.toThrowError(AssertionError)
514517
.toHaveMessage(
515-
'Expected the element NOT to have description "This is a description", '
518+
'Expected the element NOT to have accessible description "This is a description", '
516519
+ 'but received "This is a description"',
517520
);
518521
});
@@ -524,12 +527,12 @@ describe("[Unit] ElementAssertion.test.ts", () => {
524527
const button = getByTestId("button-multiple");
525528
const test = new ElementAssertion(button);
526529

527-
expect(test.toHaveDescription("This is a description Additional info")).toBeEqual(test);
530+
expect(test.toHaveAccessibleDescription("This is a description Additional info")).toBeEqual(test);
528531

529-
expect(() => test.not.toHaveDescription("This is a description Additional info"))
532+
expect(() => test.not.toHaveAccessibleDescription("This is a description Additional info"))
530533
.toThrowError(AssertionError)
531534
.toHaveMessage(
532-
'Expected the element NOT to have description "This is a description Additional info", '
535+
'Expected the element NOT to have accessible description "This is a description Additional info", '
533536
+ 'but received "This is a description Additional info"',
534537
);
535538
});
@@ -541,13 +544,14 @@ describe("[Unit] ElementAssertion.test.ts", () => {
541544
const button = getByTestId("button-single");
542545
const test = new ElementAssertion(button);
543546

544-
expect(() => test.toHaveDescription("Wrong description"))
547+
expect(() => test.toHaveAccessibleDescription("Wrong description"))
545548
.toThrowError(AssertionError)
546549
.toHaveMessage(
547-
'Expected the element to have description "Wrong description", but received "This is a description"',
550+
'Expected the element to have accessible description "Wrong description", '
551+
+ 'but received "This is a description"',
548552
);
549553

550-
expect(test.not.toHaveDescription("Wrong description")).toBeEqual(test);
554+
expect(test.not.toHaveAccessibleDescription("Wrong description")).toBeEqual(test);
551555
});
552556
});
553557
});
@@ -559,12 +563,12 @@ describe("[Unit] ElementAssertion.test.ts", () => {
559563
const button = getByTestId("button-single");
560564
const test = new ElementAssertion(button);
561565

562-
expect(test.toHaveDescription(/description/i)).toBeEqual(test);
566+
expect(test.toHaveAccessibleDescription(/description/i)).toBeEqual(test);
563567

564-
expect(() => test.not.toHaveDescription(/description/i))
568+
expect(() => test.not.toHaveAccessibleDescription(/description/i))
565569
.toThrowError(AssertionError)
566570
.toHaveMessage(
567-
"Expected the element NOT to have description matching /description/i, "
571+
"Expected the element NOT to have accessible description matching /description/i, "
568572
+ 'but received "This is a description"',
569573
);
570574
});
@@ -576,14 +580,14 @@ describe("[Unit] ElementAssertion.test.ts", () => {
576580
const button = getByTestId("button-single");
577581
const test = new ElementAssertion(button);
578582

579-
expect(() => test.toHaveDescription(/wrong pattern/))
583+
expect(() => test.toHaveAccessibleDescription(/wrong pattern/))
580584
.toThrowError(AssertionError)
581585
.toHaveMessage(
582-
"Expected the element to have description matching /wrong pattern/, "
586+
"Expected the element to have accessible description matching /wrong pattern/, "
583587
+ 'but received "This is a description"',
584588
);
585589

586-
expect(test.not.toHaveDescription(/wrong pattern/)).toBeEqual(test);
590+
expect(test.not.toHaveAccessibleDescription(/wrong pattern/)).toBeEqual(test);
587591
});
588592
});
589593
});

0 commit comments

Comments
 (0)