New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@guidepup/virtual-screen-reader

Package Overview
Dependencies
Maintainers
1
Versions
43
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@guidepup/virtual-screen-reader - npm Package Compare versions

Comparing version 0.1.0 to 0.1.1

lib/getAccessibleDescription.d.ts

3

lib/createAccessibilityTree.d.ts
export interface AccessibilityNode {
accessibleDescription: string;
accessibleName: string;
childrenPresentational: boolean;
node: Node;
role: string;
}
export declare function isHiddenFromAccessibilityTree(node: Node): boolean;
export declare function createAccessibilityTree(node: Node): AccessibilityNode[];
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAccessibilityTree = exports.isHiddenFromAccessibilityTree = void 0;
const getAccessibleName_1 = require("./getAccessibleName");
const getRole_1 = require("./getRole");
exports.createAccessibilityTree = void 0;
const getNodeAccessibilityData_1 = require("./getNodeAccessibilityData");
const isElement_1 = require("./isElement");
const dom_accessibility_api_1 = require("dom-accessibility-api");
// TODO: This isn't fully compliant, see "Children Presentational" point
// See https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
function isHiddenFromAccessibilityTree(node) {

@@ -16,8 +13,18 @@ if (node.nodeType === Node.TEXT_NODE && !!node.textContent.trim()) {

}
exports.isHiddenFromAccessibilityTree = isHiddenFromAccessibilityTree;
function shouldIgnoreChildren(tree) {
const { accessibleName, node } = tree;
return (
// TODO: improve comparison on whether the children are superfluous
// to include.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
accessibleName === (node.textContent || node.value)?.trim());
}
function flattenTree(tree) {
const { children, ...treeNode } = tree;
const flattenedTree = [...children.flatMap((child) => flattenTree(child))];
const isAnnounced = treeNode.accessibleName || treeNode.role;
const isRoleContainer = !treeNode.accessibleName && treeNode.role;
const ignoreChildren = shouldIgnoreChildren(tree);
const flattenedTree = ignoreChildren
? []
: [...children.flatMap((child) => flattenTree(child))];
const isRoleContainer = flattenedTree.length && !ignoreChildren && treeNode.role;
if (isAnnounced) {

@@ -28,3 +35,5 @@ flattenedTree.unshift(treeNode);

flattenedTree.push({
accessibleName: "",
accessibleDescription: "",
accessibleName: treeNode.accessibleName,
childrenPresentational: treeNode.childrenPresentational,
node: treeNode.node,

@@ -37,5 +46,2 @@ role: `end of ${treeNode.role}`,

function growTree(node, tree) {
if (tree.accessibleName) {
return tree;
}
node.childNodes.forEach((childNode) => {

@@ -45,8 +51,13 @@ if (isHiddenFromAccessibilityTree(childNode)) {

}
const accessibleName = (0, getAccessibleName_1.getAccessibleName)(childNode);
const { accessibleDescription, accessibleName, childrenPresentational, role, } = (0, getNodeAccessibilityData_1.getNodeAccessibilityData)({
node: childNode,
inheritedImplicitPresentational: tree.childrenPresentational,
});
tree.children.push(growTree(childNode, {
accessibleDescription,
accessibleName,
children: [],
childrenPresentational,
node: childNode,
role: (0, getRole_1.getRole)(childNode, accessibleName),
role,
}));

@@ -60,8 +71,13 @@ });

}
const accessibleName = (0, getAccessibleName_1.getAccessibleName)(node);
const { accessibleDescription, accessibleName, childrenPresentational, role, } = (0, getNodeAccessibilityData_1.getNodeAccessibilityData)({
node,
inheritedImplicitPresentational: false,
});
const tree = growTree(node, {
accessibleDescription,
accessibleName,
children: [],
node: node,
role: (0, getRole_1.getRole)(node, accessibleName),
childrenPresentational,
node,
role,
});

@@ -68,0 +84,0 @@ return flattenTree(tree);

@@ -1,1 +0,7 @@

export declare function getRole(node: Node, accessibleName: string): string;
export declare const presentationRoles: string[];
export declare const childrenPresentationalRoles: string[];
export declare function getRole({ accessibleName, inheritedImplicitPresentational, node, }: {
accessibleName: string;
inheritedImplicitPresentational: boolean;
node: Node;
}): string;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getRole = void 0;
exports.getRole = exports.childrenPresentationalRoles = exports.presentationRoles = void 0;
const dom_accessibility_api_1 = require("dom-accessibility-api");
const dom_1 = require("@testing-library/dom");
const isElement_1 = require("./isElement");
const aria_query_1 = require("aria-query");
const ignoredRoles = ["presentation", "none"];
exports.presentationRoles = ["presentation", "none"];
const allowedNonAbstractRoles = aria_query_1.roles

@@ -12,10 +13,57 @@ .entries()

.map(([key]) => key);
exports.childrenPresentationalRoles = aria_query_1.roles
.entries()
.filter(([, { childrenPresentational }]) => childrenPresentational)
.map(([key]) => key);
const rolesRequiringName = ["form", "region"];
function getExplicitRole(node, accessibleName) {
const rawRole = node.getAttribute("role")?.trim();
if (!rawRole) {
const globalStatesAndProperties = [
"aria-atomic",
"aria-braillelabel",
"aria-brailleroledescription",
"aria-busy",
"aria-controls",
"aria-describedby",
"aria-description",
"aria-details",
"aria-dropeffect",
"aria-flowto",
"aria-grabbed",
"aria-hidden",
"aria-keyshortcuts",
"aria-label",
"aria-labelledby",
"aria-live",
"aria-owns",
"aria-relevant",
"aria-roledescription",
];
const FOCUSABLE_SELECTOR = [
"input:not([type=hidden]):not([disabled])",
"button:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
'[contenteditable=""]',
'[contenteditable="true"]',
"a[href]",
"[tabindex]:not([disabled])",
].join(", ");
function isFocusable(node) {
return node.matches(FOCUSABLE_SELECTOR);
}
function hasGlobalStateOrProperty(node) {
return globalStatesAndProperties.some((global) => node.hasAttribute(global));
}
function getExplicitRole({ accessibleName, inheritedImplicitPresentational, node, }) {
const rawRoles = node.getAttribute("role")?.trim().split(" ") ?? [];
// "Children Presentational: True"
// https://w3c.github.io/aria/#tree_exclusion
if (inheritedImplicitPresentational) {
rawRoles.unshift("none");
}
if (!rawRoles?.length) {
return "";
}
const filteredRoles = rawRole
.split(" ")
// TODO: allowed child element exceptions for
// https://w3c.github.io/aria/#conflict_resolution_presentation_none
const filteredRoles = rawRoles
/**

@@ -44,12 +92,38 @@ * As stated in the Definition of Roles section, it is considered an

*/
.filter((role) => !!accessibleName || !rolesRequiringName.includes(role));
.filter((role) => !!accessibleName || !rolesRequiringName.includes(role))
/**
* If an element is focusable, user agents MUST ignore the
* presentation/none role and expose the element with its implicit role, in
* order to ensure that the element is operable.
*
* If an element has global WAI-ARIA states or properties, user agents MUST
* ignore the presentation role and instead expose the element's implicit
* role. However, if an element has only non-global, role-specific WAI-ARIA
* states or properties, the element MUST NOT be exposed unless the
* presentational role is inherited and an explicit non-presentational role
* is applied.
*
* https://w3c.github.io/aria/#conflict_resolution_presentation_none
*/
.filter((role) => {
if (!exports.presentationRoles.includes(role)) {
return true;
}
if (hasGlobalStateOrProperty(node) || isFocusable(node)) {
return false;
}
return true;
});
return filteredRoles?.[0] ?? "";
}
// TODO: https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none
function getRole(node, accessibleName) {
function getRole({ accessibleName, inheritedImplicitPresentational, node, }) {
if (!(0, isElement_1.isElement)(node)) {
return "";
}
const target = node.cloneNode(true);
const explicitRole = getExplicitRole(target, accessibleName);
const target = node.cloneNode();
const explicitRole = getExplicitRole({
accessibleName,
inheritedImplicitPresentational,
node: target,
});
if (explicitRole) {

@@ -61,5 +135,10 @@ target.setAttribute("role", explicitRole);

}
const role = (0, dom_accessibility_api_1.getRole)(target) ?? "";
return ignoredRoles.includes(role) ? "" : role;
let role = (0, dom_accessibility_api_1.getRole)(target) ?? "";
if (!role) {
// TODO: decide if we just replace dom-accessibility-api above with
// @testing-library/dom here rather than doing this fallback
role = Object.keys((0, dom_1.getRoles)(target))?.[0] ?? "";
}
return role;
}
exports.getRole = getRole;

@@ -8,4 +8,30 @@ "use strict";

const user_event_1 = require("@testing-library/user-event");
// TODO: monitor focus change and update the screen reader active element
// TODO: handle aria-live, role="polite", role="alert", and other interruptions
// TODO: monitor focus change and update the screen reader active element.
// TODO: handle aria-live, role="polite", role="alert", and other interruptions.
const observeDOM = (function () {
const MutationObserver =
// @ts-expect-error WebKitMutationObserver is non-standard WebKit fallback for old implementations
window.MutationObserver || window.WebKitMutationObserver;
return function observeDOM(node, onChange) {
if (!node || node.nodeType !== 1) {
return;
}
const callback = () => onChange();
if (MutationObserver) {
const mutationObserver = new MutationObserver(callback);
mutationObserver.observe(node, { childList: true, subtree: true });
return () => {
mutationObserver.disconnect();
};
}
else if (window.addEventListener) {
node.addEventListener("DOMNodeInserted", callback, false);
node.addEventListener("DOMNodeRemoved", callback, false);
return () => {
node.removeEventListener("DOMNodeInserted", callback, false);
node.removeEventListener("DOMNodeRemoved", callback, false);
};
}
};
})();
class Virtual {

@@ -16,2 +42,4 @@ #activeNode = null;

#spokenPhraseLog = [];
#treeCache = null;
#disconnectDOMObserver = null;
#checkContainer() {

@@ -23,7 +51,15 @@ if (!this.#container) {

#getAccessibilityTree() {
return (0, createAccessibilityTree_1.createAccessibilityTree)(this.#container);
if (!this.#treeCache) {
this.#treeCache = (0, createAccessibilityTree_1.createAccessibilityTree)(this.#container);
}
return this.#treeCache;
}
#invalidateTreeCache() {
this.#treeCache = null;
}
#updateState(accessibilityNode) {
const { role, accessibleName } = accessibilityNode;
const spokenPhrase = [role, accessibleName].filter(Boolean).join(", ");
const { accessibleDescription, accessibleName, role } = accessibilityNode;
const spokenPhrase = [role, accessibleName, accessibleDescription]
.filter(Boolean)
.join(", ");
this.#activeNode = accessibilityNode;

@@ -51,5 +87,9 @@ this.#itemTextLog.push(accessibleName);

this.#updateState(tree[0]);
const invalidateTreeCache = () => this.#invalidateTreeCache();
this.#disconnectDOMObserver = observeDOM(container, invalidateTreeCache);
return;
}
async stop() {
this.#disconnectDOMObserver?.();
this.#invalidateTreeCache();
this.#activeNode = null;

@@ -56,0 +96,0 @@ this.#container = null;

{
"name": "@guidepup/virtual-screen-reader",
"version": "0.1.0",
"version": "0.1.1",
"description": "Virtual screen reader driver for unit test automation.",

@@ -5,0 +5,0 @@ "main": "lib/index.js",

@@ -44,10 +44,2 @@ <h1 align="center">Guidepup Virtual Screen Reader</h1>

import { virtual } from "@guidepup/virtual-screen-reader";
import {
getByLabelText,
getByText,
getByTestId,
queryByTestId,
waitFor,
} from "@testing-library/dom";
import "@testing-library/jest-dom";

@@ -54,0 +46,0 @@ function setupBasicPage() {

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc