Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow mocking getComputedStyle #41

Merged
merged 2 commits into from
Nov 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/lemon-pets-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"dom-accessibility-api": minor
---

Add option to mock window.getComputedStyle

This option has two use cases in mind:

1. fake the style and assume everything is visible.
This increases performance (window.getComputedStyle) is expensive) by not distinguishing between various levels of visual impairments. If one can't see the name with a screen reader then neither will a sighted user
2. Wrap a cache provider around `window.getComputedStyle`. We don't implement any because the returned `CSSStyleDeclaration` is only live in a browser. `jsdom` does not implement live declarations.
37 changes: 37 additions & 0 deletions sources/__tests__/accessible-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,40 @@ describe("prohibited naming", () => {
}
);
});

describe("options.getComputedStyle", () => {
beforeEach(() => {
jest.spyOn(window, "getComputedStyle");
});

afterEach(() => {
jest.restoreAllMocks();
});

it("uses window.getComputedStyle by default", () => {
const container = renderIntoDocument("<button>test</button>");

computeAccessibleName(container.querySelector("button"));

// also mixing in a regression test for the number of calls
expect(window.getComputedStyle).toHaveBeenCalledTimes(4);
});

it("can be mocked with a fake", () => {
const container = renderIntoDocument("<button>test</button>");

const name = computeAccessibleName(container.querySelector("button"), {
getComputedStyle: () => {
const declaration = new CSSStyleDeclaration();
declaration.content = "'foo'";
declaration.display = "block";
declaration.visibility = "visible";

return declaration;
}
});

expect(name).toEqual("foo test foo");
expect(window.getComputedStyle).not.toHaveBeenCalled();
});
});
58 changes: 47 additions & 11 deletions sources/accessible-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,33 @@ type FlatString = string & {
__flat: true;
};

/**
* interface for an options-bag where `window.getComputedStyle` can be mocked
*/
interface GetComputedStyleOptions {
getComputedStyle?: typeof window.getComputedStyle;
}
/**
* Small utility that handles all the JS quirks with `this` which is important
* if no mock is provided.
* @param element
* @param options - These are not optional to prevent accidentally calling it without options in `computeAccessibleName`
*/
function createGetComputedStyle(
element: Element,
options: GetComputedStyleOptions
): typeof window.getComputedStyle {
const window = safeWindow(element);
const {
// This might be overengineered. I don't know what happens if I call
// window.getComputedStyle(elementFromAnotherWindow) or if I don't bind it
// the type declarations don't require a `this`
getComputedStyle = window.getComputedStyle.bind(window)
} = options;

return getComputedStyle;
}

/**
*
* @param {string} string -
Expand Down Expand Up @@ -48,10 +75,14 @@ function prohibitsNaming(node: Node): boolean {

/**
*
* @param {Node} node -
* @param node -
* @param options - These are not optional to prevent accidentally calling it without options in `computeAccessibleName`
* @returns {boolean} -
*/
function isHidden(node: Node): node is Element {
function isHidden(
node: Node,
options: GetComputedStyleOptions
): node is Element {
if (!isElement(node)) {
return false;
}
Expand All @@ -63,7 +94,7 @@ function isHidden(node: Node): node is Element {
return true;
}

const style = safeWindow(node).getComputedStyle(node);
const style = createGetComputedStyle(node, options)(node);
return (
style.getPropertyValue("display") === "none" ||
style.getPropertyValue("visibility") === "hidden"
Expand Down Expand Up @@ -252,9 +283,13 @@ function getTextualContent(declaration: CSSStyleDeclaration): string {
/**
* implements https://w3c.github.io/accname/#mapping_additional_nd_te
* @param root
* @param context
* @param [options]
* @parma [options.getComputedStyle] - mock window.getComputedStyle. Needs `content`, `display` and `visibility`
*/
export function computeAccessibleName(root: Element): string {
export function computeAccessibleName(
root: Element,
options: GetComputedStyleOptions = {}
): string {
const consultedNodes = new Set<Node>();

if (prohibitsNaming(root)) {
Expand All @@ -268,7 +303,10 @@ export function computeAccessibleName(root: Element): string {
): string {
let accumulatedText = "";
if (isElement(node)) {
const pseudoBefore = safeWindow(node).getComputedStyle(node, "::before");
const pseudoBefore = createGetComputedStyle(node, options)(
node,
"::before"
);
const beforeContent = getTextualContent(pseudoBefore);
accumulatedText = `${beforeContent} ${accumulatedText}`;
}
Expand All @@ -282,15 +320,13 @@ export function computeAccessibleName(root: Element): string {
// TODO: Unclear why display affects delimiter
const display =
isElement(node) &&
safeWindow(node)
.getComputedStyle(node)
.getPropertyValue("display");
createGetComputedStyle(node, options)(node).getPropertyValue("display");
const separator = display !== "inline" ? " " : "";
accumulatedText += `${separator}${result}`;
}

if (isElement(node)) {
const pseudoAfter = safeWindow(node).getComputedStyle(node, ":after");
const pseudoAfter = createGetComputedStyle(node, options)(node, ":after");
const afterContent = getTextualContent(pseudoAfter);
accumulatedText = `${accumulatedText} ${afterContent}`;
}
Expand Down Expand Up @@ -381,7 +417,7 @@ export function computeAccessibleName(root: Element): string {
}

// 2A
if (isHidden(current) && !context.isReferenced) {
if (isHidden(current, options) && !context.isReferenced) {
consultedNodes.add(current);
return "" as FlatString;
}
Expand Down