accessibility-testing-toolkit
Advanced tools
Comparing version 1.0.3 to 1.1.0-beta.0
@@ -33,14 +33,206 @@ var __defProp = Object.defineProperty; | ||
// src/tree/role-helpers.ts | ||
import { elementRoles } from "aria-query"; | ||
// src/type-guards.ts | ||
var isOptionElement = (element) => element.tagName === "OPTION"; | ||
var isInputElement = (element) => element.tagName === "INPUT"; | ||
var isDetailsElement = (element) => element.tagName === "DETAILS"; | ||
var isDetailsElement = (element) => element.tagName.toLowerCase() === "details"; | ||
var isSummaryElement = (element) => element.tagName.toLowerCase() === "summary"; | ||
var isDefined = (value) => value !== void 0 && value !== null; | ||
var isA11yTreeNode = (value) => typeof value === "object" && value !== null && "name" in value && "role" in value; | ||
var isA11yTreeNodeMatch = (value) => typeof value === "object" && value !== null && ("name" in value || "role" in value || "description" in value || "state" in value || "children" in value); | ||
var isTextMatcher = (value) => typeof value === "string" || typeof value === "number" || value instanceof RegExp || typeof value === "function"; | ||
// src/helpers/by-role.ts | ||
function anyObjectPropertiesAreDefined(obj) { | ||
return Object.values(obj).some((value) => value !== void 0); | ||
} | ||
function byRole(role, nameOrPropertiesOrChildren, childrenIfProvided) { | ||
const a11yNode = { | ||
role | ||
}; | ||
if (typeof nameOrPropertiesOrChildren === "undefined") { | ||
return a11yNode; | ||
} else if (isTextMatcher(nameOrPropertiesOrChildren)) { | ||
a11yNode.name = nameOrPropertiesOrChildren; | ||
if (Array.isArray(childrenIfProvided)) { | ||
a11yNode.children = childrenIfProvided; | ||
} | ||
} else if (Array.isArray(nameOrPropertiesOrChildren)) { | ||
a11yNode.children = nameOrPropertiesOrChildren; | ||
} else if (typeof nameOrPropertiesOrChildren === "object") { | ||
const _a = nameOrPropertiesOrChildren, { name, description } = _a, stateAndQueries = __objRest(_a, ["name", "description"]); | ||
a11yNode.name = name; | ||
a11yNode.description = description; | ||
const _b = stateAndQueries, { level, value } = _b, state = __objRest(_b, ["level", "value"]); | ||
const queries = { level, value }; | ||
a11yNode.state = anyObjectPropertiesAreDefined(state) ? state : void 0; | ||
a11yNode.queries = anyObjectPropertiesAreDefined(queries) ? queries : void 0; | ||
if (Array.isArray(childrenIfProvided)) { | ||
a11yNode.children = childrenIfProvided; | ||
} | ||
} | ||
return a11yNode; | ||
} | ||
// src/tree/leafs.ts | ||
var StaticText = class { | ||
constructor(text) { | ||
this.text = text; | ||
} | ||
toString() { | ||
if (this.text === null) { | ||
return "null"; | ||
} | ||
return this.text; | ||
} | ||
}; | ||
// src/helpers.ts | ||
import _deepClone from "lodash.clonedeep"; | ||
import _isEqual from "lodash.isequal"; | ||
var containerAttributeValues = { | ||
name: "", | ||
role: "generic", | ||
description: "" | ||
}; | ||
var defaultState = { | ||
busy: false, | ||
checked: void 0, | ||
current: false, | ||
disabled: false, | ||
expanded: void 0, | ||
pressed: void 0, | ||
selected: void 0 | ||
}; | ||
var defaultQueries = { | ||
level: void 0, | ||
value: { | ||
min: void 0, | ||
max: void 0, | ||
now: void 0, | ||
text: void 0 | ||
} | ||
}; | ||
var isDefaultState = (state) => { | ||
return _isEqual(state, defaultState); | ||
}; | ||
var isDefaultQueries = (queries) => { | ||
return _isEqual(queries, defaultQueries); | ||
}; | ||
var isDefaultAttributeValues = (node) => { | ||
return (node.role === containerAttributeValues.role || node.role === void 0) && node.name === containerAttributeValues.name && node.description === containerAttributeValues.description; | ||
}; | ||
var isContainer = (node) => { | ||
return isDefaultAttributeValues(node) && isDefaultState(node.state) && isDefaultQueries(node.queries); | ||
}; | ||
// src/pretty-tree/omit-default-values.ts | ||
function isNestedObject(value) { | ||
return typeof value === "object" && value !== null && !(value instanceof RegExp); | ||
} | ||
function omitDefaultValues(target, defaults) { | ||
const filteredObject = {}; | ||
Object.keys(target).forEach((key) => { | ||
const targetValue = target[key]; | ||
const defaultValue = defaults[key]; | ||
if (targetValue !== defaultValue && targetValue !== void 0) { | ||
filteredObject[key] = targetValue; | ||
} else if (isNestedObject(targetValue) && isNestedObject(defaultValue)) { | ||
const result = omitDefaultValues(targetValue, defaultValue); | ||
if (Object.keys(result).length > 0) { | ||
filteredObject[key] = result; | ||
} | ||
} | ||
}); | ||
return filteredObject; | ||
} | ||
// src/pretty-tree/render-properties.ts | ||
var renderPropertyValue = (value) => { | ||
switch (typeof value) { | ||
case "function": | ||
return "Function"; | ||
case "number": | ||
case "boolean": | ||
return String(value); | ||
case "undefined": | ||
return ""; | ||
case "object": | ||
if (value instanceof RegExp) { | ||
return value.toString(); | ||
} | ||
return ""; | ||
default: | ||
return `"${value}"`; | ||
} | ||
}; | ||
var renderProperties = (obj, prefix = "") => { | ||
const keyValuePairs = Object.entries(obj).flatMap(([key, value]) => { | ||
if (value === void 0) { | ||
return []; | ||
} | ||
const fullKey = prefix ? `${prefix}.${key}` : key; | ||
if (typeof value === "object" && !(value instanceof RegExp) && !(value instanceof Function)) { | ||
return renderProperties(value, fullKey); | ||
} else { | ||
const renderedValue = renderPropertyValue(value); | ||
return renderedValue ? `${fullKey}=${renderedValue}` : []; | ||
} | ||
}); | ||
return keyValuePairs.join(" "); | ||
}; | ||
// src/pretty-tree/pretty-tree.ts | ||
var renderStringMatcher = (textMatcher) => { | ||
if (textMatcher instanceof RegExp || typeof textMatcher === "number") { | ||
return textMatcher.toString(); | ||
} | ||
if (typeof textMatcher === "function") { | ||
return textMatcher.toString(); | ||
} | ||
return `"${textMatcher}"`; | ||
}; | ||
var getPrettyTree = (tree, depth = 0) => { | ||
const indentation = " ".repeat(depth); | ||
if (typeof tree === "string" || typeof tree === "number") { | ||
return `${indentation}"${tree}" | ||
`; | ||
} | ||
if (tree instanceof RegExp) { | ||
return `${indentation}${tree.toString()} | ||
`; | ||
} | ||
if (tree instanceof StaticText) { | ||
return `${indentation}StaticText "${tree.text}" | ||
`; | ||
} | ||
if (tree === void 0) { | ||
return `${indentation}undefined | ||
`; | ||
} | ||
if (typeof tree === "function") { | ||
return `${indentation}${tree.toString()} | ||
`; | ||
} | ||
const filteredState = tree.state ? omitDefaultValues(tree.state, defaultState) : void 0; | ||
const filteredQueries = tree.queries ? omitDefaultValues(tree.queries, defaultQueries) : void 0; | ||
const nameString = tree.name ? ` ${renderStringMatcher(tree.name)}` : ""; | ||
const stateString = filteredState ? ` ${renderProperties(filteredState)}` : ""; | ||
const queriesString = filteredQueries ? ` ${renderProperties(filteredQueries)}` : ""; | ||
let output = `${indentation}${tree.role}${nameString}${stateString}${queriesString} | ||
`; | ||
if (tree.description) { | ||
output += `${indentation} description: ${renderStringMatcher( | ||
tree.description | ||
)} | ||
`; | ||
} | ||
if (tree.children) { | ||
tree.children.forEach((child) => { | ||
output += getPrettyTree(child, depth + 1); | ||
}); | ||
} | ||
return output; | ||
}; | ||
// src/tree/role-helpers.ts | ||
import { elementRoles } from "aria-query"; | ||
// src/tree/virtual-roles.ts | ||
@@ -342,15 +534,2 @@ var nonLandmarkVirtualRoles = [ | ||
// src/tree/leafs.ts | ||
var StaticText = class { | ||
constructor(text) { | ||
this.text = text; | ||
} | ||
toString() { | ||
if (this.text === null) { | ||
return "null"; | ||
} | ||
return this.text; | ||
} | ||
}; | ||
// src/config.ts | ||
@@ -365,3 +544,3 @@ var config = { | ||
// src/tree/accessibility-tree.ts | ||
// src/tree/context.ts | ||
var isNonLandmarkRole = (element, role) => ["article", "aside", "main", "nav", "section"].includes( | ||
@@ -371,4 +550,20 @@ element.tagName.toLowerCase() | ||
var isList = (role) => role === "list"; | ||
var isClosedDetails = (element) => isDetailsElement(element) && !element.open; | ||
// src/tree/overrides.ts | ||
var isContentInsideClosedDetails = (element) => { | ||
const parent = element.parentElement; | ||
if (!parent) { | ||
return false; | ||
} | ||
return isClosedDetails(parent) && (element instanceof HTMLElement ? !isSummaryElement(element) : true); | ||
}; | ||
var isInaccessibleOverride = (element) => { | ||
return isContentInsideClosedDetails(element); | ||
}; | ||
// src/tree/accessibility-tree.ts | ||
var defaultOptions = { | ||
isListSubtree: false, | ||
isClosedDetailsSubtree: false, | ||
isNonLandmarkSubtree: false | ||
@@ -378,2 +573,3 @@ }; | ||
isListSubtree: userListSubtree = defaultOptions.isListSubtree, | ||
isClosedDetailsSubtree: userIsClosedDetailsSubtree = defaultOptions.isClosedDetailsSubtree, | ||
isNonLandmarkSubtree: userNonLandmarkSubtree = defaultOptions.isNonLandmarkSubtree, | ||
@@ -383,3 +579,3 @@ isInaccessibleOptions = getConfig().isInaccessibleOptions | ||
function assembleTree(element2, context) { | ||
if (isInaccessible(element2, context.isInaccessibleOptions)) { | ||
if (isInaccessible(element2, context.isInaccessibleOptions) || isInaccessibleOverride(element2)) { | ||
return null; | ||
@@ -416,2 +612,3 @@ } | ||
isListSubtree: context.isListSubtree || isList(role), | ||
isClosedDetailsSubtree: context.isClosedDetailsSubtree || isClosedDetails(element2), | ||
isNonLandmarkSubtree: context.isNonLandmarkSubtree || isNonLandmarkRole(element2, role), | ||
@@ -422,3 +619,3 @@ isInaccessibleOptions | ||
if (child instanceof Text) { | ||
if (child.textContent === null) { | ||
if (child.textContent === null || isInaccessibleOverride(child)) { | ||
return void 0; | ||
@@ -434,2 +631,3 @@ } | ||
isListSubtree: userListSubtree, | ||
isClosedDetailsSubtree: userIsClosedDetailsSubtree, | ||
isNonLandmarkSubtree: userNonLandmarkSubtree, | ||
@@ -440,43 +638,2 @@ isInaccessibleOptions | ||
// src/helpers.ts | ||
import _deepClone from "lodash.clonedeep"; | ||
import _isEqual from "lodash.isequal"; | ||
var containerAttributeValues = { | ||
name: "", | ||
role: "generic", | ||
description: "" | ||
}; | ||
var defaultState = { | ||
busy: false, | ||
checked: void 0, | ||
current: false, | ||
disabled: false, | ||
expanded: void 0, | ||
pressed: void 0, | ||
selected: void 0 | ||
}; | ||
var getDefaultState = () => _deepClone(defaultState); | ||
var defaultQueries = { | ||
level: void 0, | ||
value: { | ||
min: void 0, | ||
max: void 0, | ||
now: void 0, | ||
text: void 0 | ||
} | ||
}; | ||
var getDefaultQueries = () => _deepClone(defaultQueries); | ||
var isDefaultState = (state) => { | ||
return _isEqual(state, defaultState); | ||
}; | ||
var isDefaultQueries = (queries) => { | ||
return _isEqual(queries, defaultQueries); | ||
}; | ||
var isDefaultAttributeValues = (node) => { | ||
return (node.role === containerAttributeValues.role || node.role === void 0) && node.name === containerAttributeValues.name && node.description === containerAttributeValues.description; | ||
}; | ||
var isContainer = (node) => { | ||
return isDefaultAttributeValues(node) && isDefaultState(node.state) && isDefaultQueries(node.queries); | ||
}; | ||
// src/tree/prune-container-nodes.ts | ||
@@ -502,459 +659,2 @@ var pruneContainerNodes = (node) => { | ||
// src/prepare-diff.ts | ||
import _cloneDeep from "lodash.clonedeep"; | ||
var getReceivedName = (node) => { | ||
if (node instanceof StaticText || !node) { | ||
return void 0; | ||
} | ||
return node.name; | ||
}; | ||
var getReceivedRole = (node) => { | ||
var _a; | ||
if (node instanceof StaticText || !node) { | ||
return ""; | ||
} | ||
return (_a = node.role) != null ? _a : void 0; | ||
}; | ||
var getReceivedDescription = (node) => { | ||
if (node instanceof StaticText || !node) { | ||
return void 0; | ||
} | ||
return node.description; | ||
}; | ||
var getReceivedChildren = (node) => { | ||
if (node instanceof StaticText || !node) { | ||
return void 0; | ||
} | ||
return node.children; | ||
}; | ||
var computeTextValue = (received, expected, element) => { | ||
if (received === void 0) { | ||
return expected; | ||
} | ||
if (expected === void 0) { | ||
return received; | ||
} | ||
if (expected instanceof RegExp) { | ||
return expected.test(received) ? received : expected; | ||
} | ||
if (typeof expected === "function") { | ||
return expected(received, element) ? received : expected; | ||
} | ||
if (typeof expected === "string" || typeof expected === "number") { | ||
return received === expected.toString() ? received : expected; | ||
} | ||
return void 0; | ||
}; | ||
var computeDiffState = (received, expected) => { | ||
var _a, _b, _c, _d, _e, _f, _g; | ||
let state = received instanceof StaticText || !received ? void 0 : __spreadValues({}, received.state); | ||
if (expected == null ? void 0 : expected.state) { | ||
if (state === void 0) { | ||
state = getDefaultState(); | ||
} | ||
state.busy = (_a = expected.state.busy) != null ? _a : state.busy; | ||
state.checked = (_b = expected.state.checked) != null ? _b : state.checked; | ||
state.current = (_c = expected.state.current) != null ? _c : state.current; | ||
state.disabled = (_d = expected.state.disabled) != null ? _d : state.disabled; | ||
state.expanded = (_e = expected.state.expanded) != null ? _e : state.expanded; | ||
state.pressed = (_f = expected.state.pressed) != null ? _f : state.pressed; | ||
state.selected = (_g = expected.state.selected) != null ? _g : state.selected; | ||
} | ||
return state; | ||
}; | ||
var computeDiffQueries = (received, expected, element) => { | ||
let queries = received instanceof StaticText || !received ? void 0 : _cloneDeep(received.queries); | ||
if (expected == null ? void 0 : expected.queries) { | ||
if (queries === void 0) { | ||
queries = getDefaultQueries(); | ||
} | ||
queries.level = expected.queries.level; | ||
if (expected.queries.value !== void 0) { | ||
if (expected.queries.value.min !== void 0) { | ||
queries.value.min = expected.queries.value.min; | ||
} | ||
if (expected.queries.value.max !== void 0) { | ||
queries.value.max = expected.queries.value.max; | ||
} | ||
if (expected.queries.value.now !== void 0) { | ||
queries.value.now = expected.queries.value.now; | ||
} | ||
if (expected.queries.value.text !== void 0) { | ||
queries.value.text = computeTextValue( | ||
isA11yTreeNode(received) ? received == null ? void 0 : received.queries.value.text : void 0, | ||
expected.queries.value.text, | ||
element | ||
); | ||
} | ||
} | ||
} | ||
return queries; | ||
}; | ||
var matchToNode = (received, expected) => { | ||
var _a, _b, _c, _d; | ||
if (!received && !expected) { | ||
return void 0; | ||
} | ||
const element = isA11yTreeNode(received) ? received.element : null; | ||
const role = (_a = expected == null ? void 0 : expected.role) != null ? _a : getReceivedRole(received); | ||
const name = computeTextValue( | ||
getReceivedName(received), | ||
expected == null ? void 0 : expected.name, | ||
element | ||
); | ||
const description = computeTextValue( | ||
getReceivedDescription(received), | ||
expected == null ? void 0 : expected.description, | ||
element | ||
); | ||
const state = computeDiffState(received, expected); | ||
const queries = computeDiffQueries(received, expected, element); | ||
const receivedChildren = getReceivedChildren(received); | ||
const expectedChildren = expected == null ? void 0 : expected.children; | ||
const maxOfChildrenLengths = Math.max( | ||
(_c = (_b = expected == null ? void 0 : expected.children) == null ? void 0 : _b.length) != null ? _c : 0, | ||
(_d = receivedChildren == null ? void 0 : receivedChildren.length) != null ? _d : 0 | ||
); | ||
const children = Array.from({ length: maxOfChildrenLengths }).map((_, index) => { | ||
const receivedChild = receivedChildren == null ? void 0 : receivedChildren[index]; | ||
const expectedChild = expectedChildren == null ? void 0 : expectedChildren[index]; | ||
if (typeof expectedChild === "string") { | ||
return new StaticText(expectedChild); | ||
} | ||
if (expectedChild instanceof RegExp) { | ||
if (receivedChild instanceof StaticText && receivedChild.text && expectedChild.test(receivedChild.text)) { | ||
return receivedChild; | ||
} | ||
return expectedChild; | ||
} | ||
if (expectedChild === void 0) { | ||
if (name && maxOfChildrenLengths === 1) { | ||
if (name instanceof RegExp || typeof name === "function") { | ||
return name; | ||
} | ||
return new StaticText( | ||
typeof name === "string" ? name : name.toString() | ||
); | ||
} | ||
return void 0; | ||
} | ||
return matchToNode(receivedChild, expectedChild); | ||
}).filter(Boolean); | ||
const result = { | ||
role, | ||
name, | ||
description, | ||
state, | ||
queries, | ||
children | ||
}; | ||
return result; | ||
}; | ||
// src/pretty-tree/omit-default-values.ts | ||
function isNestedObject(value) { | ||
return typeof value === "object" && value !== null && !(value instanceof RegExp); | ||
} | ||
function omitDefaultValues(target, defaults) { | ||
const filteredObject = {}; | ||
Object.keys(target).forEach((key) => { | ||
const targetValue = target[key]; | ||
const defaultValue = defaults[key]; | ||
if (targetValue !== defaultValue && targetValue !== void 0) { | ||
filteredObject[key] = targetValue; | ||
} else if (isNestedObject(targetValue) && isNestedObject(defaultValue)) { | ||
const result = omitDefaultValues(targetValue, defaultValue); | ||
if (Object.keys(result).length > 0) { | ||
filteredObject[key] = result; | ||
} | ||
} | ||
}); | ||
return filteredObject; | ||
} | ||
// src/pretty-tree/render-properties.ts | ||
var renderPropertyValue = (value) => { | ||
switch (typeof value) { | ||
case "function": | ||
return "Function"; | ||
case "number": | ||
case "boolean": | ||
return String(value); | ||
case "undefined": | ||
return ""; | ||
case "object": | ||
if (value instanceof RegExp) { | ||
return value.toString(); | ||
} | ||
return ""; | ||
default: | ||
return `"${value}"`; | ||
} | ||
}; | ||
var renderProperties = (obj, prefix = "") => { | ||
const keyValuePairs = Object.entries(obj).flatMap(([key, value]) => { | ||
if (value === void 0) { | ||
return []; | ||
} | ||
const fullKey = prefix ? `${prefix}.${key}` : key; | ||
if (typeof value === "object" && !(value instanceof RegExp) && !(value instanceof Function)) { | ||
return renderProperties(value, fullKey); | ||
} else { | ||
const renderedValue = renderPropertyValue(value); | ||
return renderedValue ? `${fullKey}=${renderedValue}` : []; | ||
} | ||
}); | ||
return keyValuePairs.join(" "); | ||
}; | ||
// src/pretty-tree/pretty-tree.ts | ||
var renderStringMatcher = (textMatcher) => { | ||
if (textMatcher instanceof RegExp || typeof textMatcher === "number") { | ||
return textMatcher.toString(); | ||
} | ||
if (typeof textMatcher === "function") { | ||
return textMatcher.toString(); | ||
} | ||
return `"${textMatcher}"`; | ||
}; | ||
var getPrettyTree = (tree, depth = 0) => { | ||
const indentation = " ".repeat(depth); | ||
if (typeof tree === "string" || typeof tree === "number") { | ||
return `${indentation}"${tree}" | ||
`; | ||
} | ||
if (tree instanceof RegExp) { | ||
return `${indentation}${tree.toString()} | ||
`; | ||
} | ||
if (tree instanceof StaticText) { | ||
return `${indentation}StaticText "${tree.text}" | ||
`; | ||
} | ||
if (tree === void 0) { | ||
return `${indentation}undefined | ||
`; | ||
} | ||
if (typeof tree === "function") { | ||
return `${indentation}${tree.toString()} | ||
`; | ||
} | ||
const filteredState = tree.state ? omitDefaultValues(tree.state, defaultState) : void 0; | ||
const filteredQueries = tree.queries ? omitDefaultValues(tree.queries, defaultQueries) : void 0; | ||
const nameString = tree.name ? ` ${renderStringMatcher(tree.name)}` : ""; | ||
const stateString = filteredState ? ` ${renderProperties(filteredState)}` : ""; | ||
const queriesString = filteredQueries ? ` ${renderProperties(filteredQueries)}` : ""; | ||
let output = `${indentation}${tree.role}${nameString}${stateString}${queriesString} | ||
`; | ||
if (tree.description) { | ||
output += `${indentation} description: ${renderStringMatcher( | ||
tree.description | ||
)} | ||
`; | ||
} | ||
if (tree.children) { | ||
tree.children.forEach((child) => { | ||
output += getPrettyTree(child, depth + 1); | ||
}); | ||
} | ||
return output; | ||
}; | ||
// src/testers/text.ts | ||
var textTester = (received, expected, element) => { | ||
if (typeof expected === "undefined") { | ||
return true; | ||
} | ||
if (typeof expected === "string") { | ||
return received === expected; | ||
} | ||
if (typeof expected === "number") { | ||
return received === expected.toString(); | ||
} | ||
if (expected instanceof RegExp) { | ||
return expected.test(received); | ||
} | ||
if (typeof expected === "function") { | ||
return expected(received, element); | ||
} | ||
return false; | ||
}; | ||
// src/testers/queries.ts | ||
var queriesTester = (received, expected) => { | ||
if (typeof expected === "undefined") { | ||
return true; | ||
} | ||
let result = true; | ||
if (expected.level !== void 0) { | ||
result && (result = received.level === expected.level); | ||
} | ||
if (expected.value !== void 0) { | ||
if (expected.value.min !== void 0) { | ||
result && (result = received.value.min === expected.value.min); | ||
} | ||
if (expected.value.max !== void 0) { | ||
result && (result = received.value.max === expected.value.max); | ||
} | ||
if (expected.value.now !== void 0) { | ||
result && (result = received.value.now === expected.value.now); | ||
} | ||
if (expected.value.text !== void 0) { | ||
if (received.value.text === void 0) { | ||
result && (result = received.value.text === expected.value.text); | ||
} else { | ||
result && (result = textTester(received.value.text, expected.value.text, null)); | ||
} | ||
} | ||
} | ||
return result; | ||
}; | ||
// src/testers/role.ts | ||
var roleTester = (received, expected) => { | ||
if (typeof expected === "undefined") { | ||
return true; | ||
} | ||
if (typeof expected === "string") { | ||
return received === expected; | ||
} | ||
return false; | ||
}; | ||
// src/testers/state.ts | ||
var stateTester = (received, expected) => { | ||
if (typeof expected === "undefined") { | ||
return true; | ||
} | ||
const expectedKeys = Object.keys( | ||
expected | ||
); | ||
return expectedKeys.every((key) => { | ||
if (typeof expected[key] === "undefined") { | ||
return true; | ||
} | ||
if (typeof expected[key] === "boolean") { | ||
return received[key] === expected[key]; | ||
} | ||
if (typeof expected[key] === "string") { | ||
return received[key] === expected[key]; | ||
} | ||
return false; | ||
}); | ||
}; | ||
// src/testers/node.ts | ||
function nodeTester(received, expected) { | ||
if (!roleTester(received.role, expected.role)) { | ||
return false; | ||
} | ||
if (!textTester(received.name, expected.name, received.element)) { | ||
return false; | ||
} | ||
if (!textTester(received.description, expected.description, received.element)) { | ||
return false; | ||
} | ||
if (!stateTester(received.state, expected.state)) { | ||
return false; | ||
} | ||
if (!queriesTester(received.queries, expected.queries)) { | ||
return false; | ||
} | ||
let isEqual = true; | ||
if (expected.children !== void 0) { | ||
if (received.children === void 0) { | ||
throw new Error("treeNodeMatcher: a.children is undefined"); | ||
} | ||
if (received.children.length !== expected.children.length) { | ||
return false; | ||
} | ||
for (let i = 0; i < received.children.length; i++) { | ||
if (!isEqual) { | ||
break; | ||
} | ||
const child = received.children[i]; | ||
const childMatcher = expected.children[i]; | ||
if (isA11yTreeNode(child) && isA11yTreeNodeMatch(childMatcher)) { | ||
isEqual && (isEqual = nodeTester(child, childMatcher)); | ||
continue; | ||
} | ||
if (child instanceof StaticText && isTextMatcher(childMatcher)) { | ||
isEqual && (isEqual = textTester(child.text, childMatcher, null)); | ||
continue; | ||
} | ||
isEqual = false; | ||
} | ||
} | ||
return isEqual; | ||
} | ||
// src/matchers.ts | ||
expect.extend({ | ||
toHaveA11yTree(received, expected, options) { | ||
if (received instanceof HTMLElement) { | ||
const tree = getAccessibilityTree(received, options); | ||
if (tree === null) { | ||
return { | ||
message: () => "Failed to get accessible tree", | ||
pass: false | ||
}; | ||
} | ||
const flatTree = pruneContainerNodes(tree); | ||
if (flatTree === null) { | ||
return { | ||
message: () => "Failed to flatten accessible tree", | ||
pass: false | ||
}; | ||
} | ||
received = flatTree; | ||
} | ||
const pass = nodeTester(received, expected); | ||
const expectedPreparedForDiff = matchToNode(received, expected); | ||
const receivedPrettyTree = getPrettyTree(received); | ||
const expectedPrettyTree = getPrettyTree(expectedPreparedForDiff); | ||
if (pass) { | ||
return { | ||
message: () => `${this.utils.diff(expectedPrettyTree, receivedPrettyTree)}`, | ||
pass: true | ||
}; | ||
} | ||
return { | ||
message: () => `${this.utils.diff(expectedPrettyTree, receivedPrettyTree)}`, | ||
pass: false | ||
}; | ||
} | ||
}); | ||
// src/helpers/by-role.ts | ||
function anyObjectPropertiesAreDefined(obj) { | ||
return Object.values(obj).some((value) => value !== void 0); | ||
} | ||
function byRole(role, nameOrPropertiesOrChildren, childrenIfProvided) { | ||
const a11yNode = { | ||
role | ||
}; | ||
if (typeof nameOrPropertiesOrChildren === "undefined") { | ||
return a11yNode; | ||
} else if (isTextMatcher(nameOrPropertiesOrChildren)) { | ||
a11yNode.name = nameOrPropertiesOrChildren; | ||
if (Array.isArray(childrenIfProvided)) { | ||
a11yNode.children = childrenIfProvided; | ||
} | ||
} else if (Array.isArray(nameOrPropertiesOrChildren)) { | ||
a11yNode.children = nameOrPropertiesOrChildren; | ||
} else if (typeof nameOrPropertiesOrChildren === "object") { | ||
const _a = nameOrPropertiesOrChildren, { name, description } = _a, stateAndQueries = __objRest(_a, ["name", "description"]); | ||
a11yNode.name = name; | ||
a11yNode.description = description; | ||
const _b = stateAndQueries, { level, value } = _b, state = __objRest(_b, ["level", "value"]); | ||
const queries = { level, value }; | ||
a11yNode.state = anyObjectPropertiesAreDefined(state) ? state : void 0; | ||
a11yNode.queries = anyObjectPropertiesAreDefined(queries) ? queries : void 0; | ||
if (Array.isArray(childrenIfProvided)) { | ||
a11yNode.children = childrenIfProvided; | ||
} | ||
} | ||
return a11yNode; | ||
} | ||
// src/index.ts | ||
@@ -967,2 +667,3 @@ import { isSubtreeInaccessible } from "dom-accessibility-api"; | ||
getConfig, | ||
getPrettyTree, | ||
isSubtreeInaccessible, | ||
@@ -969,0 +670,0 @@ pruneContainerNodes |
@@ -1,118 +0,7 @@ | ||
import { ARIARoleDefinitionKey } from 'aria-query'; | ||
import { A as A11yTreeNodeMatch, a as A11yTreeNodeStateMatch, b as A11yTreeNodeQueriesMatch, c as A11yTreeForDiff, S as StaticText, T as TextMatcher, M as MatcherOptions, d as A11yTreeNode } from './matchers-09042272.js'; | ||
export { g as A11yTreeNodeState, f as ARIARoleDefinitionKeyExtended, e as AsNonLandmarkRoles, V as VirtualRoles } from './matchers-09042272.js'; | ||
import { IsInaccessibleOptions } from 'dom-accessibility-api'; | ||
export { isSubtreeInaccessible } from 'dom-accessibility-api'; | ||
import 'aria-query'; | ||
declare class StaticText { | ||
text: string; | ||
constructor(text: string); | ||
toString(): string; | ||
} | ||
type AsNonLandmarkRoles = 'HeaderAsNonLandmark' | 'FooterAsNonLandmark'; | ||
type VirtualRoles = 'Abbr' | 'Audio' | 'Canvas' | 'Details' | 'DescriptionListDetails' | 'DescriptionList' | 'DescriptionListTerm' | 'DisclosureTriangle' | 'EmbeddedObject' | 'Figcaption' | 'PluginObject' | 'LabelText' | 'LineBreak' | 'Video'; | ||
type ARIARoleDefinitionKeyExtended = ARIARoleDefinitionKey | AsNonLandmarkRoles | VirtualRoles; | ||
type TextMatcherFunction = (content: string, element: Element | null) => boolean; | ||
type TextMatcher = string | number | RegExp | TextMatcherFunction; | ||
type A11yTreeNodeContext = { | ||
isListSubtree?: boolean; | ||
isNonLandmarkSubtree?: boolean; | ||
isInaccessibleOptions?: IsInaccessibleOptions; | ||
}; | ||
type A11yTreeNodeState = { | ||
busy: boolean; | ||
checked?: boolean; | ||
current: string | boolean; | ||
disabled: boolean; | ||
expanded?: boolean; | ||
pressed?: boolean; | ||
selected?: boolean; | ||
}; | ||
type A11yTreeNodeQueries = { | ||
level?: number; | ||
value: { | ||
min?: number; | ||
max?: number; | ||
now?: number; | ||
text?: string; | ||
}; | ||
}; | ||
type A11yTreeNode = { | ||
element: HTMLElement; | ||
role: HTMLElement['role'] | undefined; | ||
name: string; | ||
description: string; | ||
state: A11yTreeNodeState; | ||
queries: A11yTreeNodeQueries; | ||
children?: (A11yTreeNode | StaticText)[]; | ||
}; | ||
type A11yTreeNodeStateMatch = Partial<A11yTreeNodeState>; | ||
type A11yTreeNodeQueriesMatch = { | ||
level?: number; | ||
value?: { | ||
min?: number; | ||
max?: number; | ||
now?: number; | ||
text?: TextMatcher; | ||
}; | ||
}; | ||
type A11yTreeNodeMatch = { | ||
role?: HTMLElement['role']; | ||
name?: TextMatcher; | ||
description?: TextMatcher; | ||
state?: A11yTreeNodeStateMatch; | ||
queries?: A11yTreeNodeQueriesMatch; | ||
children?: (A11yTreeNodeMatch | string | RegExp)[]; | ||
}; | ||
type MatcherOptions = A11yTreeNodeContext; | ||
declare global { | ||
namespace jest { | ||
interface Matchers<R> { | ||
/** | ||
* @description | ||
* Asserts that the accessibility tree for an element matches the expected structure. | ||
* An accessibility tree represents how a user agent (such as a screen reader) processes and communicates | ||
* accessibility information from the DOM. Use this matcher to check whether the pertinent accessibility | ||
* properties and relationships are correctly established, ensuring an accessible experience for users | ||
* of assistive technologies. | ||
* | ||
* The expected structure is defined by the `A11yTreeNodeMatch` interface, which can accommodate various | ||
* properties like role, name, description, state, and children. | ||
* | ||
* The second parameter allows for optional configuration settings | ||
* | ||
* @example | ||
* // Example with byRole hierarchy: | ||
* <nav aria-label="Main navigation"> | ||
* <ul> | ||
* <li><a href="/home">Home</a></li> | ||
* <li><a href="/about">About</a></li> | ||
* </ul> | ||
* <button disabled>Click me</button> | ||
* </nav> | ||
* | ||
* expect(screen.getByRole('navigation', { name: 'Main navigation' })).toHaveA11yTree( | ||
* byRole('navigation', [ | ||
* byRole('list', [ | ||
* byRole('listitem', [ | ||
* byRole('link', { name: 'Home' }), | ||
* ]), | ||
* byRole('listitem', [ | ||
* byRole('link', { name: 'About' }), | ||
* ]), | ||
* ]), | ||
* byRole('button', { name: 'Click me', disabled: true }), | ||
* ]) | ||
* ); | ||
* | ||
* @see | ||
* - [W3C Accessibility Tree](https://www.w3.org/TR/wai-aria/#accessibility_tree) | ||
* - [ARIA in HTML](https://www.w3.org/TR/html-aria/) | ||
*/ | ||
toHaveA11yTree(match?: A11yTreeNodeMatch, options?: MatcherOptions): R; | ||
} | ||
} | ||
} | ||
type NameDescriptionStateQueries = Omit<A11yTreeNodeMatch, 'role' | 'children'> & A11yTreeNodeStateMatch & A11yTreeNodeQueriesMatch; | ||
@@ -126,4 +15,6 @@ declare function byRole(role: A11yTreeNodeMatch['role'], properties: NameDescriptionStateQueries): A11yTreeNodeMatch; | ||
declare const getAccessibilityTree: (element: HTMLElement, { isListSubtree: userListSubtree, isNonLandmarkSubtree: userNonLandmarkSubtree, isInaccessibleOptions, }?: MatcherOptions) => A11yTreeNode | null; | ||
declare const getPrettyTree: (tree: A11yTreeForDiff | StaticText | TextMatcher | undefined, depth?: number) => string; | ||
declare const getAccessibilityTree: (element: HTMLElement, { isListSubtree: userListSubtree, isClosedDetailsSubtree: userIsClosedDetailsSubtree, isNonLandmarkSubtree: userNonLandmarkSubtree, isInaccessibleOptions, }?: MatcherOptions) => A11yTreeNode | null; | ||
/** | ||
@@ -141,2 +32,2 @@ * Removes container nodes from the tree, flattening the tree. | ||
export { A11yTreeNode, A11yTreeNodeMatch, A11yTreeNodeState, ARIARoleDefinitionKeyExtended, AsNonLandmarkRoles, MatcherOptions, VirtualRoles, byRole, configToolkit, getAccessibilityTree, getConfig, pruneContainerNodes }; | ||
export { A11yTreeNode, A11yTreeNodeMatch, byRole, configToolkit, getAccessibilityTree, getConfig, getPrettyTree, pruneContainerNodes }; |
@@ -66,2 +66,3 @@ "use strict"; | ||
getConfig: () => getConfig, | ||
getPrettyTree: () => getPrettyTree, | ||
isSubtreeInaccessible: () => import_dom_accessibility_api2.isSubtreeInaccessible, | ||
@@ -72,14 +73,206 @@ pruneContainerNodes: () => pruneContainerNodes | ||
// src/tree/role-helpers.ts | ||
var import_aria_query = require("aria-query"); | ||
// src/type-guards.ts | ||
var isOptionElement = (element) => element.tagName === "OPTION"; | ||
var isInputElement = (element) => element.tagName === "INPUT"; | ||
var isDetailsElement = (element) => element.tagName === "DETAILS"; | ||
var isDetailsElement = (element) => element.tagName.toLowerCase() === "details"; | ||
var isSummaryElement = (element) => element.tagName.toLowerCase() === "summary"; | ||
var isDefined = (value) => value !== void 0 && value !== null; | ||
var isA11yTreeNode = (value) => typeof value === "object" && value !== null && "name" in value && "role" in value; | ||
var isA11yTreeNodeMatch = (value) => typeof value === "object" && value !== null && ("name" in value || "role" in value || "description" in value || "state" in value || "children" in value); | ||
var isTextMatcher = (value) => typeof value === "string" || typeof value === "number" || value instanceof RegExp || typeof value === "function"; | ||
// src/helpers/by-role.ts | ||
function anyObjectPropertiesAreDefined(obj) { | ||
return Object.values(obj).some((value) => value !== void 0); | ||
} | ||
function byRole(role, nameOrPropertiesOrChildren, childrenIfProvided) { | ||
const a11yNode = { | ||
role | ||
}; | ||
if (typeof nameOrPropertiesOrChildren === "undefined") { | ||
return a11yNode; | ||
} else if (isTextMatcher(nameOrPropertiesOrChildren)) { | ||
a11yNode.name = nameOrPropertiesOrChildren; | ||
if (Array.isArray(childrenIfProvided)) { | ||
a11yNode.children = childrenIfProvided; | ||
} | ||
} else if (Array.isArray(nameOrPropertiesOrChildren)) { | ||
a11yNode.children = nameOrPropertiesOrChildren; | ||
} else if (typeof nameOrPropertiesOrChildren === "object") { | ||
const _a = nameOrPropertiesOrChildren, { name, description } = _a, stateAndQueries = __objRest(_a, ["name", "description"]); | ||
a11yNode.name = name; | ||
a11yNode.description = description; | ||
const _b = stateAndQueries, { level, value } = _b, state = __objRest(_b, ["level", "value"]); | ||
const queries = { level, value }; | ||
a11yNode.state = anyObjectPropertiesAreDefined(state) ? state : void 0; | ||
a11yNode.queries = anyObjectPropertiesAreDefined(queries) ? queries : void 0; | ||
if (Array.isArray(childrenIfProvided)) { | ||
a11yNode.children = childrenIfProvided; | ||
} | ||
} | ||
return a11yNode; | ||
} | ||
// src/tree/leafs.ts | ||
var StaticText = class { | ||
constructor(text) { | ||
this.text = text; | ||
} | ||
toString() { | ||
if (this.text === null) { | ||
return "null"; | ||
} | ||
return this.text; | ||
} | ||
}; | ||
// src/helpers.ts | ||
var import_lodash = __toESM(require("lodash.clonedeep")); | ||
var import_lodash2 = __toESM(require("lodash.isequal")); | ||
var containerAttributeValues = { | ||
name: "", | ||
role: "generic", | ||
description: "" | ||
}; | ||
var defaultState = { | ||
busy: false, | ||
checked: void 0, | ||
current: false, | ||
disabled: false, | ||
expanded: void 0, | ||
pressed: void 0, | ||
selected: void 0 | ||
}; | ||
var defaultQueries = { | ||
level: void 0, | ||
value: { | ||
min: void 0, | ||
max: void 0, | ||
now: void 0, | ||
text: void 0 | ||
} | ||
}; | ||
var isDefaultState = (state) => { | ||
return (0, import_lodash2.default)(state, defaultState); | ||
}; | ||
var isDefaultQueries = (queries) => { | ||
return (0, import_lodash2.default)(queries, defaultQueries); | ||
}; | ||
var isDefaultAttributeValues = (node) => { | ||
return (node.role === containerAttributeValues.role || node.role === void 0) && node.name === containerAttributeValues.name && node.description === containerAttributeValues.description; | ||
}; | ||
var isContainer = (node) => { | ||
return isDefaultAttributeValues(node) && isDefaultState(node.state) && isDefaultQueries(node.queries); | ||
}; | ||
// src/pretty-tree/omit-default-values.ts | ||
function isNestedObject(value) { | ||
return typeof value === "object" && value !== null && !(value instanceof RegExp); | ||
} | ||
function omitDefaultValues(target, defaults) { | ||
const filteredObject = {}; | ||
Object.keys(target).forEach((key) => { | ||
const targetValue = target[key]; | ||
const defaultValue = defaults[key]; | ||
if (targetValue !== defaultValue && targetValue !== void 0) { | ||
filteredObject[key] = targetValue; | ||
} else if (isNestedObject(targetValue) && isNestedObject(defaultValue)) { | ||
const result = omitDefaultValues(targetValue, defaultValue); | ||
if (Object.keys(result).length > 0) { | ||
filteredObject[key] = result; | ||
} | ||
} | ||
}); | ||
return filteredObject; | ||
} | ||
// src/pretty-tree/render-properties.ts | ||
var renderPropertyValue = (value) => { | ||
switch (typeof value) { | ||
case "function": | ||
return "Function"; | ||
case "number": | ||
case "boolean": | ||
return String(value); | ||
case "undefined": | ||
return ""; | ||
case "object": | ||
if (value instanceof RegExp) { | ||
return value.toString(); | ||
} | ||
return ""; | ||
default: | ||
return `"${value}"`; | ||
} | ||
}; | ||
var renderProperties = (obj, prefix = "") => { | ||
const keyValuePairs = Object.entries(obj).flatMap(([key, value]) => { | ||
if (value === void 0) { | ||
return []; | ||
} | ||
const fullKey = prefix ? `${prefix}.${key}` : key; | ||
if (typeof value === "object" && !(value instanceof RegExp) && !(value instanceof Function)) { | ||
return renderProperties(value, fullKey); | ||
} else { | ||
const renderedValue = renderPropertyValue(value); | ||
return renderedValue ? `${fullKey}=${renderedValue}` : []; | ||
} | ||
}); | ||
return keyValuePairs.join(" "); | ||
}; | ||
// src/pretty-tree/pretty-tree.ts | ||
var renderStringMatcher = (textMatcher) => { | ||
if (textMatcher instanceof RegExp || typeof textMatcher === "number") { | ||
return textMatcher.toString(); | ||
} | ||
if (typeof textMatcher === "function") { | ||
return textMatcher.toString(); | ||
} | ||
return `"${textMatcher}"`; | ||
}; | ||
var getPrettyTree = (tree, depth = 0) => { | ||
const indentation = " ".repeat(depth); | ||
if (typeof tree === "string" || typeof tree === "number") { | ||
return `${indentation}"${tree}" | ||
`; | ||
} | ||
if (tree instanceof RegExp) { | ||
return `${indentation}${tree.toString()} | ||
`; | ||
} | ||
if (tree instanceof StaticText) { | ||
return `${indentation}StaticText "${tree.text}" | ||
`; | ||
} | ||
if (tree === void 0) { | ||
return `${indentation}undefined | ||
`; | ||
} | ||
if (typeof tree === "function") { | ||
return `${indentation}${tree.toString()} | ||
`; | ||
} | ||
const filteredState = tree.state ? omitDefaultValues(tree.state, defaultState) : void 0; | ||
const filteredQueries = tree.queries ? omitDefaultValues(tree.queries, defaultQueries) : void 0; | ||
const nameString = tree.name ? ` ${renderStringMatcher(tree.name)}` : ""; | ||
const stateString = filteredState ? ` ${renderProperties(filteredState)}` : ""; | ||
const queriesString = filteredQueries ? ` ${renderProperties(filteredQueries)}` : ""; | ||
let output = `${indentation}${tree.role}${nameString}${stateString}${queriesString} | ||
`; | ||
if (tree.description) { | ||
output += `${indentation} description: ${renderStringMatcher( | ||
tree.description | ||
)} | ||
`; | ||
} | ||
if (tree.children) { | ||
tree.children.forEach((child) => { | ||
output += getPrettyTree(child, depth + 1); | ||
}); | ||
} | ||
return output; | ||
}; | ||
// src/tree/role-helpers.ts | ||
var import_aria_query = require("aria-query"); | ||
// src/tree/virtual-roles.ts | ||
@@ -376,15 +569,2 @@ var nonLandmarkVirtualRoles = [ | ||
// src/tree/leafs.ts | ||
var StaticText = class { | ||
constructor(text) { | ||
this.text = text; | ||
} | ||
toString() { | ||
if (this.text === null) { | ||
return "null"; | ||
} | ||
return this.text; | ||
} | ||
}; | ||
// src/config.ts | ||
@@ -399,3 +579,3 @@ var config = { | ||
// src/tree/accessibility-tree.ts | ||
// src/tree/context.ts | ||
var isNonLandmarkRole = (element, role) => ["article", "aside", "main", "nav", "section"].includes( | ||
@@ -405,4 +585,20 @@ element.tagName.toLowerCase() | ||
var isList = (role) => role === "list"; | ||
var isClosedDetails = (element) => isDetailsElement(element) && !element.open; | ||
// src/tree/overrides.ts | ||
var isContentInsideClosedDetails = (element) => { | ||
const parent = element.parentElement; | ||
if (!parent) { | ||
return false; | ||
} | ||
return isClosedDetails(parent) && (element instanceof HTMLElement ? !isSummaryElement(element) : true); | ||
}; | ||
var isInaccessibleOverride = (element) => { | ||
return isContentInsideClosedDetails(element); | ||
}; | ||
// src/tree/accessibility-tree.ts | ||
var defaultOptions = { | ||
isListSubtree: false, | ||
isClosedDetailsSubtree: false, | ||
isNonLandmarkSubtree: false | ||
@@ -412,2 +608,3 @@ }; | ||
isListSubtree: userListSubtree = defaultOptions.isListSubtree, | ||
isClosedDetailsSubtree: userIsClosedDetailsSubtree = defaultOptions.isClosedDetailsSubtree, | ||
isNonLandmarkSubtree: userNonLandmarkSubtree = defaultOptions.isNonLandmarkSubtree, | ||
@@ -417,3 +614,3 @@ isInaccessibleOptions = getConfig().isInaccessibleOptions | ||
function assembleTree(element2, context) { | ||
if ((0, import_dom_accessibility_api.isInaccessible)(element2, context.isInaccessibleOptions)) { | ||
if ((0, import_dom_accessibility_api.isInaccessible)(element2, context.isInaccessibleOptions) || isInaccessibleOverride(element2)) { | ||
return null; | ||
@@ -450,2 +647,3 @@ } | ||
isListSubtree: context.isListSubtree || isList(role), | ||
isClosedDetailsSubtree: context.isClosedDetailsSubtree || isClosedDetails(element2), | ||
isNonLandmarkSubtree: context.isNonLandmarkSubtree || isNonLandmarkRole(element2, role), | ||
@@ -456,3 +654,3 @@ isInaccessibleOptions | ||
if (child instanceof Text) { | ||
if (child.textContent === null) { | ||
if (child.textContent === null || isInaccessibleOverride(child)) { | ||
return void 0; | ||
@@ -468,2 +666,3 @@ } | ||
isListSubtree: userListSubtree, | ||
isClosedDetailsSubtree: userIsClosedDetailsSubtree, | ||
isNonLandmarkSubtree: userNonLandmarkSubtree, | ||
@@ -474,43 +673,2 @@ isInaccessibleOptions | ||
// src/helpers.ts | ||
var import_lodash = __toESM(require("lodash.clonedeep")); | ||
var import_lodash2 = __toESM(require("lodash.isequal")); | ||
var containerAttributeValues = { | ||
name: "", | ||
role: "generic", | ||
description: "" | ||
}; | ||
var defaultState = { | ||
busy: false, | ||
checked: void 0, | ||
current: false, | ||
disabled: false, | ||
expanded: void 0, | ||
pressed: void 0, | ||
selected: void 0 | ||
}; | ||
var getDefaultState = () => (0, import_lodash.default)(defaultState); | ||
var defaultQueries = { | ||
level: void 0, | ||
value: { | ||
min: void 0, | ||
max: void 0, | ||
now: void 0, | ||
text: void 0 | ||
} | ||
}; | ||
var getDefaultQueries = () => (0, import_lodash.default)(defaultQueries); | ||
var isDefaultState = (state) => { | ||
return (0, import_lodash2.default)(state, defaultState); | ||
}; | ||
var isDefaultQueries = (queries) => { | ||
return (0, import_lodash2.default)(queries, defaultQueries); | ||
}; | ||
var isDefaultAttributeValues = (node) => { | ||
return (node.role === containerAttributeValues.role || node.role === void 0) && node.name === containerAttributeValues.name && node.description === containerAttributeValues.description; | ||
}; | ||
var isContainer = (node) => { | ||
return isDefaultAttributeValues(node) && isDefaultState(node.state) && isDefaultQueries(node.queries); | ||
}; | ||
// src/tree/prune-container-nodes.ts | ||
@@ -536,459 +694,2 @@ var pruneContainerNodes = (node) => { | ||
// src/prepare-diff.ts | ||
var import_lodash3 = __toESM(require("lodash.clonedeep")); | ||
var getReceivedName = (node) => { | ||
if (node instanceof StaticText || !node) { | ||
return void 0; | ||
} | ||
return node.name; | ||
}; | ||
var getReceivedRole = (node) => { | ||
var _a; | ||
if (node instanceof StaticText || !node) { | ||
return ""; | ||
} | ||
return (_a = node.role) != null ? _a : void 0; | ||
}; | ||
var getReceivedDescription = (node) => { | ||
if (node instanceof StaticText || !node) { | ||
return void 0; | ||
} | ||
return node.description; | ||
}; | ||
var getReceivedChildren = (node) => { | ||
if (node instanceof StaticText || !node) { | ||
return void 0; | ||
} | ||
return node.children; | ||
}; | ||
var computeTextValue = (received, expected, element) => { | ||
if (received === void 0) { | ||
return expected; | ||
} | ||
if (expected === void 0) { | ||
return received; | ||
} | ||
if (expected instanceof RegExp) { | ||
return expected.test(received) ? received : expected; | ||
} | ||
if (typeof expected === "function") { | ||
return expected(received, element) ? received : expected; | ||
} | ||
if (typeof expected === "string" || typeof expected === "number") { | ||
return received === expected.toString() ? received : expected; | ||
} | ||
return void 0; | ||
}; | ||
var computeDiffState = (received, expected) => { | ||
var _a, _b, _c, _d, _e, _f, _g; | ||
let state = received instanceof StaticText || !received ? void 0 : __spreadValues({}, received.state); | ||
if (expected == null ? void 0 : expected.state) { | ||
if (state === void 0) { | ||
state = getDefaultState(); | ||
} | ||
state.busy = (_a = expected.state.busy) != null ? _a : state.busy; | ||
state.checked = (_b = expected.state.checked) != null ? _b : state.checked; | ||
state.current = (_c = expected.state.current) != null ? _c : state.current; | ||
state.disabled = (_d = expected.state.disabled) != null ? _d : state.disabled; | ||
state.expanded = (_e = expected.state.expanded) != null ? _e : state.expanded; | ||
state.pressed = (_f = expected.state.pressed) != null ? _f : state.pressed; | ||
state.selected = (_g = expected.state.selected) != null ? _g : state.selected; | ||
} | ||
return state; | ||
}; | ||
var computeDiffQueries = (received, expected, element) => { | ||
let queries = received instanceof StaticText || !received ? void 0 : (0, import_lodash3.default)(received.queries); | ||
if (expected == null ? void 0 : expected.queries) { | ||
if (queries === void 0) { | ||
queries = getDefaultQueries(); | ||
} | ||
queries.level = expected.queries.level; | ||
if (expected.queries.value !== void 0) { | ||
if (expected.queries.value.min !== void 0) { | ||
queries.value.min = expected.queries.value.min; | ||
} | ||
if (expected.queries.value.max !== void 0) { | ||
queries.value.max = expected.queries.value.max; | ||
} | ||
if (expected.queries.value.now !== void 0) { | ||
queries.value.now = expected.queries.value.now; | ||
} | ||
if (expected.queries.value.text !== void 0) { | ||
queries.value.text = computeTextValue( | ||
isA11yTreeNode(received) ? received == null ? void 0 : received.queries.value.text : void 0, | ||
expected.queries.value.text, | ||
element | ||
); | ||
} | ||
} | ||
} | ||
return queries; | ||
}; | ||
var matchToNode = (received, expected) => { | ||
var _a, _b, _c, _d; | ||
if (!received && !expected) { | ||
return void 0; | ||
} | ||
const element = isA11yTreeNode(received) ? received.element : null; | ||
const role = (_a = expected == null ? void 0 : expected.role) != null ? _a : getReceivedRole(received); | ||
const name = computeTextValue( | ||
getReceivedName(received), | ||
expected == null ? void 0 : expected.name, | ||
element | ||
); | ||
const description = computeTextValue( | ||
getReceivedDescription(received), | ||
expected == null ? void 0 : expected.description, | ||
element | ||
); | ||
const state = computeDiffState(received, expected); | ||
const queries = computeDiffQueries(received, expected, element); | ||
const receivedChildren = getReceivedChildren(received); | ||
const expectedChildren = expected == null ? void 0 : expected.children; | ||
const maxOfChildrenLengths = Math.max( | ||
(_c = (_b = expected == null ? void 0 : expected.children) == null ? void 0 : _b.length) != null ? _c : 0, | ||
(_d = receivedChildren == null ? void 0 : receivedChildren.length) != null ? _d : 0 | ||
); | ||
const children = Array.from({ length: maxOfChildrenLengths }).map((_, index) => { | ||
const receivedChild = receivedChildren == null ? void 0 : receivedChildren[index]; | ||
const expectedChild = expectedChildren == null ? void 0 : expectedChildren[index]; | ||
if (typeof expectedChild === "string") { | ||
return new StaticText(expectedChild); | ||
} | ||
if (expectedChild instanceof RegExp) { | ||
if (receivedChild instanceof StaticText && receivedChild.text && expectedChild.test(receivedChild.text)) { | ||
return receivedChild; | ||
} | ||
return expectedChild; | ||
} | ||
if (expectedChild === void 0) { | ||
if (name && maxOfChildrenLengths === 1) { | ||
if (name instanceof RegExp || typeof name === "function") { | ||
return name; | ||
} | ||
return new StaticText( | ||
typeof name === "string" ? name : name.toString() | ||
); | ||
} | ||
return void 0; | ||
} | ||
return matchToNode(receivedChild, expectedChild); | ||
}).filter(Boolean); | ||
const result = { | ||
role, | ||
name, | ||
description, | ||
state, | ||
queries, | ||
children | ||
}; | ||
return result; | ||
}; | ||
// src/pretty-tree/omit-default-values.ts | ||
function isNestedObject(value) { | ||
return typeof value === "object" && value !== null && !(value instanceof RegExp); | ||
} | ||
function omitDefaultValues(target, defaults) { | ||
const filteredObject = {}; | ||
Object.keys(target).forEach((key) => { | ||
const targetValue = target[key]; | ||
const defaultValue = defaults[key]; | ||
if (targetValue !== defaultValue && targetValue !== void 0) { | ||
filteredObject[key] = targetValue; | ||
} else if (isNestedObject(targetValue) && isNestedObject(defaultValue)) { | ||
const result = omitDefaultValues(targetValue, defaultValue); | ||
if (Object.keys(result).length > 0) { | ||
filteredObject[key] = result; | ||
} | ||
} | ||
}); | ||
return filteredObject; | ||
} | ||
// src/pretty-tree/render-properties.ts | ||
var renderPropertyValue = (value) => { | ||
switch (typeof value) { | ||
case "function": | ||
return "Function"; | ||
case "number": | ||
case "boolean": | ||
return String(value); | ||
case "undefined": | ||
return ""; | ||
case "object": | ||
if (value instanceof RegExp) { | ||
return value.toString(); | ||
} | ||
return ""; | ||
default: | ||
return `"${value}"`; | ||
} | ||
}; | ||
var renderProperties = (obj, prefix = "") => { | ||
const keyValuePairs = Object.entries(obj).flatMap(([key, value]) => { | ||
if (value === void 0) { | ||
return []; | ||
} | ||
const fullKey = prefix ? `${prefix}.${key}` : key; | ||
if (typeof value === "object" && !(value instanceof RegExp) && !(value instanceof Function)) { | ||
return renderProperties(value, fullKey); | ||
} else { | ||
const renderedValue = renderPropertyValue(value); | ||
return renderedValue ? `${fullKey}=${renderedValue}` : []; | ||
} | ||
}); | ||
return keyValuePairs.join(" "); | ||
}; | ||
// src/pretty-tree/pretty-tree.ts | ||
var renderStringMatcher = (textMatcher) => { | ||
if (textMatcher instanceof RegExp || typeof textMatcher === "number") { | ||
return textMatcher.toString(); | ||
} | ||
if (typeof textMatcher === "function") { | ||
return textMatcher.toString(); | ||
} | ||
return `"${textMatcher}"`; | ||
}; | ||
var getPrettyTree = (tree, depth = 0) => { | ||
const indentation = " ".repeat(depth); | ||
if (typeof tree === "string" || typeof tree === "number") { | ||
return `${indentation}"${tree}" | ||
`; | ||
} | ||
if (tree instanceof RegExp) { | ||
return `${indentation}${tree.toString()} | ||
`; | ||
} | ||
if (tree instanceof StaticText) { | ||
return `${indentation}StaticText "${tree.text}" | ||
`; | ||
} | ||
if (tree === void 0) { | ||
return `${indentation}undefined | ||
`; | ||
} | ||
if (typeof tree === "function") { | ||
return `${indentation}${tree.toString()} | ||
`; | ||
} | ||
const filteredState = tree.state ? omitDefaultValues(tree.state, defaultState) : void 0; | ||
const filteredQueries = tree.queries ? omitDefaultValues(tree.queries, defaultQueries) : void 0; | ||
const nameString = tree.name ? ` ${renderStringMatcher(tree.name)}` : ""; | ||
const stateString = filteredState ? ` ${renderProperties(filteredState)}` : ""; | ||
const queriesString = filteredQueries ? ` ${renderProperties(filteredQueries)}` : ""; | ||
let output = `${indentation}${tree.role}${nameString}${stateString}${queriesString} | ||
`; | ||
if (tree.description) { | ||
output += `${indentation} description: ${renderStringMatcher( | ||
tree.description | ||
)} | ||
`; | ||
} | ||
if (tree.children) { | ||
tree.children.forEach((child) => { | ||
output += getPrettyTree(child, depth + 1); | ||
}); | ||
} | ||
return output; | ||
}; | ||
// src/testers/text.ts | ||
var textTester = (received, expected, element) => { | ||
if (typeof expected === "undefined") { | ||
return true; | ||
} | ||
if (typeof expected === "string") { | ||
return received === expected; | ||
} | ||
if (typeof expected === "number") { | ||
return received === expected.toString(); | ||
} | ||
if (expected instanceof RegExp) { | ||
return expected.test(received); | ||
} | ||
if (typeof expected === "function") { | ||
return expected(received, element); | ||
} | ||
return false; | ||
}; | ||
// src/testers/queries.ts | ||
var queriesTester = (received, expected) => { | ||
if (typeof expected === "undefined") { | ||
return true; | ||
} | ||
let result = true; | ||
if (expected.level !== void 0) { | ||
result && (result = received.level === expected.level); | ||
} | ||
if (expected.value !== void 0) { | ||
if (expected.value.min !== void 0) { | ||
result && (result = received.value.min === expected.value.min); | ||
} | ||
if (expected.value.max !== void 0) { | ||
result && (result = received.value.max === expected.value.max); | ||
} | ||
if (expected.value.now !== void 0) { | ||
result && (result = received.value.now === expected.value.now); | ||
} | ||
if (expected.value.text !== void 0) { | ||
if (received.value.text === void 0) { | ||
result && (result = received.value.text === expected.value.text); | ||
} else { | ||
result && (result = textTester(received.value.text, expected.value.text, null)); | ||
} | ||
} | ||
} | ||
return result; | ||
}; | ||
// src/testers/role.ts | ||
var roleTester = (received, expected) => { | ||
if (typeof expected === "undefined") { | ||
return true; | ||
} | ||
if (typeof expected === "string") { | ||
return received === expected; | ||
} | ||
return false; | ||
}; | ||
// src/testers/state.ts | ||
var stateTester = (received, expected) => { | ||
if (typeof expected === "undefined") { | ||
return true; | ||
} | ||
const expectedKeys = Object.keys( | ||
expected | ||
); | ||
return expectedKeys.every((key) => { | ||
if (typeof expected[key] === "undefined") { | ||
return true; | ||
} | ||
if (typeof expected[key] === "boolean") { | ||
return received[key] === expected[key]; | ||
} | ||
if (typeof expected[key] === "string") { | ||
return received[key] === expected[key]; | ||
} | ||
return false; | ||
}); | ||
}; | ||
// src/testers/node.ts | ||
function nodeTester(received, expected) { | ||
if (!roleTester(received.role, expected.role)) { | ||
return false; | ||
} | ||
if (!textTester(received.name, expected.name, received.element)) { | ||
return false; | ||
} | ||
if (!textTester(received.description, expected.description, received.element)) { | ||
return false; | ||
} | ||
if (!stateTester(received.state, expected.state)) { | ||
return false; | ||
} | ||
if (!queriesTester(received.queries, expected.queries)) { | ||
return false; | ||
} | ||
let isEqual = true; | ||
if (expected.children !== void 0) { | ||
if (received.children === void 0) { | ||
throw new Error("treeNodeMatcher: a.children is undefined"); | ||
} | ||
if (received.children.length !== expected.children.length) { | ||
return false; | ||
} | ||
for (let i = 0; i < received.children.length; i++) { | ||
if (!isEqual) { | ||
break; | ||
} | ||
const child = received.children[i]; | ||
const childMatcher = expected.children[i]; | ||
if (isA11yTreeNode(child) && isA11yTreeNodeMatch(childMatcher)) { | ||
isEqual && (isEqual = nodeTester(child, childMatcher)); | ||
continue; | ||
} | ||
if (child instanceof StaticText && isTextMatcher(childMatcher)) { | ||
isEqual && (isEqual = textTester(child.text, childMatcher, null)); | ||
continue; | ||
} | ||
isEqual = false; | ||
} | ||
} | ||
return isEqual; | ||
} | ||
// src/matchers.ts | ||
expect.extend({ | ||
toHaveA11yTree(received, expected, options) { | ||
if (received instanceof HTMLElement) { | ||
const tree = getAccessibilityTree(received, options); | ||
if (tree === null) { | ||
return { | ||
message: () => "Failed to get accessible tree", | ||
pass: false | ||
}; | ||
} | ||
const flatTree = pruneContainerNodes(tree); | ||
if (flatTree === null) { | ||
return { | ||
message: () => "Failed to flatten accessible tree", | ||
pass: false | ||
}; | ||
} | ||
received = flatTree; | ||
} | ||
const pass = nodeTester(received, expected); | ||
const expectedPreparedForDiff = matchToNode(received, expected); | ||
const receivedPrettyTree = getPrettyTree(received); | ||
const expectedPrettyTree = getPrettyTree(expectedPreparedForDiff); | ||
if (pass) { | ||
return { | ||
message: () => `${this.utils.diff(expectedPrettyTree, receivedPrettyTree)}`, | ||
pass: true | ||
}; | ||
} | ||
return { | ||
message: () => `${this.utils.diff(expectedPrettyTree, receivedPrettyTree)}`, | ||
pass: false | ||
}; | ||
} | ||
}); | ||
// src/helpers/by-role.ts | ||
function anyObjectPropertiesAreDefined(obj) { | ||
return Object.values(obj).some((value) => value !== void 0); | ||
} | ||
function byRole(role, nameOrPropertiesOrChildren, childrenIfProvided) { | ||
const a11yNode = { | ||
role | ||
}; | ||
if (typeof nameOrPropertiesOrChildren === "undefined") { | ||
return a11yNode; | ||
} else if (isTextMatcher(nameOrPropertiesOrChildren)) { | ||
a11yNode.name = nameOrPropertiesOrChildren; | ||
if (Array.isArray(childrenIfProvided)) { | ||
a11yNode.children = childrenIfProvided; | ||
} | ||
} else if (Array.isArray(nameOrPropertiesOrChildren)) { | ||
a11yNode.children = nameOrPropertiesOrChildren; | ||
} else if (typeof nameOrPropertiesOrChildren === "object") { | ||
const _a = nameOrPropertiesOrChildren, { name, description } = _a, stateAndQueries = __objRest(_a, ["name", "description"]); | ||
a11yNode.name = name; | ||
a11yNode.description = description; | ||
const _b = stateAndQueries, { level, value } = _b, state = __objRest(_b, ["level", "value"]); | ||
const queries = { level, value }; | ||
a11yNode.state = anyObjectPropertiesAreDefined(state) ? state : void 0; | ||
a11yNode.queries = anyObjectPropertiesAreDefined(queries) ? queries : void 0; | ||
if (Array.isArray(childrenIfProvided)) { | ||
a11yNode.children = childrenIfProvided; | ||
} | ||
} | ||
return a11yNode; | ||
} | ||
// src/index.ts | ||
@@ -1002,2 +703,3 @@ var import_dom_accessibility_api2 = require("dom-accessibility-api"); | ||
getConfig, | ||
getPrettyTree, | ||
isSubtreeInaccessible, | ||
@@ -1004,0 +706,0 @@ pruneContainerNodes |
{ | ||
"name": "accessibility-testing-toolkit", | ||
"version": "1.0.3", | ||
"version": "1.1.0-beta.0", | ||
"author": "Ivan Galiatin", | ||
@@ -12,11 +12,22 @@ "license": "MIT", | ||
"keywords": [ | ||
"testing", | ||
"jsdom", | ||
"jest", | ||
"accessibility", | ||
"accessibility tree", | ||
"aria" | ||
"aria", | ||
"dom", | ||
"jest", | ||
"jsdom", | ||
"testing" | ||
], | ||
"main": "dist/index.js", | ||
"typings": "dist/index.d.ts", | ||
"exports": { | ||
".": { | ||
"import": "./dist/esm/index.js", | ||
"require": "./dist/index.js" | ||
}, | ||
"./matchers": { | ||
"import": "./dist/esm/matchers.js", | ||
"require": "./dist/matchers.js" | ||
} | ||
}, | ||
"files": [ | ||
@@ -53,3 +64,4 @@ "/dist", | ||
"entry": [ | ||
"src/index.ts" | ||
"src/index.ts", | ||
"src/matchers.ts" | ||
], | ||
@@ -56,0 +68,0 @@ "splitting": false, |
@@ -21,7 +21,7 @@ # accessibility-testing-toolkit | ||
Import `accessibility-testing-toolkit` in your project once. The best place is to do it in your test setup file: | ||
Import `accessibility-testing-toolkit/matchers` in your project once. The best place is to do it in your test setup file: | ||
```js | ||
// In your own jest-setup.js | ||
import 'accessibility-testing-toolkit'; | ||
import 'accessibility-testing-toolkit/matchers'; | ||
@@ -125,4 +125,4 @@ // In jest.config.js add | ||
- Elements with the `hidden` attribute or `aria-hidden="true"`. | ||
- Styles that set `display: none` or `visibility: hidden`. | ||
- Elements with the `hidden` attribute or `aria-hidden="true"` | ||
- Styles that set `display: none` or `visibility: hidden` | ||
@@ -169,3 +169,2 @@ In testing environments, relying on attribute checks may be necessary since `getComputedStyle` may not reflect styles defined in external stylesheets. | ||
- `canvas`: Mapped to `Canvas` | ||
- `details`: Mapped to `Details` | ||
- `dd`: Mapped to `DescriptionListDetails` | ||
@@ -172,0 +171,0 @@ - `dl`: Mapped to `DescriptionList` |
@@ -1,3 +0,1 @@ | ||
export { MatcherOptions } from './types/matchers.d'; | ||
import './matchers'; | ||
export * from './helpers/by-role'; | ||
@@ -12,2 +10,3 @@ export { | ||
} from './types/types'; | ||
export { getPrettyTree } from './pretty-tree/pretty-tree'; | ||
export { getAccessibilityTree } from './tree/accessibility-tree'; | ||
@@ -14,0 +13,0 @@ export { pruneContainerNodes } from './tree/prune-container-nodes'; |
@@ -10,2 +10,4 @@ import { A11yTreeNode, A11yTreeNodeMatch } from './types/types'; | ||
export { MatcherOptions } from './types/matchers.d'; | ||
expect.extend({ | ||
@@ -12,0 +14,0 @@ toHaveA11yTree( |
@@ -1,27 +0,7 @@ | ||
// import { defaultState } from '../helpers'; | ||
import { defaultQueries, defaultState } from '../helpers'; | ||
import { StaticText } from '../tree/leafs'; | ||
import { | ||
A11yTreeForDiff, | ||
// A11yTreeNodeState, | ||
// A11yTreeNodeStateForDiff, | ||
TextMatcher, | ||
} from '../types/types'; | ||
import { A11yTreeForDiff, TextMatcher } from '../types/types'; | ||
import { omitDefaultValues } from './omit-default-values'; | ||
import { renderProperties } from './render-properties'; | ||
// const getStateDetails = ( | ||
// state: A11yTreeNodeState | A11yTreeNodeStateForDiff | ||
// ): string => { | ||
// const nonDefaultEntries = Object.entries(state).filter( | ||
// ([key, value]) => value !== defaultState[key as keyof A11yTreeNodeState] | ||
// ); | ||
// return nonDefaultEntries.length > 0 | ||
// ? `${nonDefaultEntries | ||
// .map(([key, value]) => `${key}: ${value}`) | ||
// .join(', ')}` | ||
// : ''; | ||
// }; | ||
const renderStringMatcher = (textMatcher: TextMatcher): string => { | ||
@@ -28,0 +8,0 @@ if (textMatcher instanceof RegExp || typeof textMatcher === 'number') { |
@@ -27,14 +27,8 @@ import { | ||
import { getConfig } from '../config'; | ||
import { isClosedDetails, isList, isNonLandmarkRole } from './context'; | ||
import { isInaccessibleOverride } from './overrides'; | ||
// if a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region | ||
const isNonLandmarkRole = (element: HTMLElement, role: string) => | ||
['article', 'aside', 'main', 'nav', 'section'].includes( | ||
element.tagName.toLowerCase() | ||
) || | ||
['aricle', 'complementary', 'main', 'navigation', 'region'].includes(role); | ||
const isList = (role: HTMLElement['role']) => role === 'list'; | ||
const defaultOptions = { | ||
isListSubtree: false, | ||
isClosedDetailsSubtree: false, | ||
isNonLandmarkSubtree: false, | ||
@@ -47,2 +41,4 @@ } satisfies MatcherOptions; | ||
isListSubtree: userListSubtree = defaultOptions.isListSubtree, | ||
isClosedDetailsSubtree: | ||
userIsClosedDetailsSubtree = defaultOptions.isClosedDetailsSubtree, | ||
isNonLandmarkSubtree: | ||
@@ -57,3 +53,6 @@ userNonLandmarkSubtree = defaultOptions.isNonLandmarkSubtree, | ||
): A11yTreeNode | null { | ||
if (isInaccessible(element, context.isInaccessibleOptions)) { | ||
if ( | ||
isInaccessible(element, context.isInaccessibleOptions) || | ||
isInaccessibleOverride(element) | ||
) { | ||
return null; | ||
@@ -94,2 +93,4 @@ } | ||
isListSubtree: context.isListSubtree || isList(role), | ||
isClosedDetailsSubtree: | ||
context.isClosedDetailsSubtree || isClosedDetails(element), | ||
isNonLandmarkSubtree: | ||
@@ -103,3 +104,3 @@ context.isNonLandmarkSubtree || | ||
if (child instanceof Text) { | ||
if (child.textContent === null) { | ||
if (child.textContent === null || isInaccessibleOverride(child)) { | ||
return undefined; | ||
@@ -119,2 +120,3 @@ } | ||
isListSubtree: userListSubtree, | ||
isClosedDetailsSubtree: userIsClosedDetailsSubtree, | ||
isNonLandmarkSubtree: userNonLandmarkSubtree, | ||
@@ -121,0 +123,0 @@ isInaccessibleOptions, |
@@ -12,4 +12,7 @@ import { A11yTreeNode, A11yTreeNodeMatch, TextMatcher } from './types/types'; | ||
element: HTMLElement | ||
): element is HTMLDetailsElement => element.tagName === 'DETAILS'; | ||
): element is HTMLDetailsElement => element.tagName.toLowerCase() === 'details'; | ||
const isSummaryElement = (element: HTMLElement): element is HTMLElement => | ||
element.tagName.toLowerCase() === 'summary'; | ||
const isDefined = <T>(value: T | undefined | null): value is T => | ||
@@ -48,2 +51,3 @@ value !== undefined && value !== null; | ||
isDetailsElement, | ||
isSummaryElement, | ||
isDefined, | ||
@@ -50,0 +54,0 @@ isStaticTextMatcher, |
@@ -42,2 +42,3 @@ import type { ARIARoleDefinitionKey } from 'aria-query'; | ||
isListSubtree?: boolean; | ||
isClosedDetailsSubtree?: boolean; | ||
isNonLandmarkSubtree?: boolean; | ||
@@ -44,0 +45,0 @@ isInaccessibleOptions?: IsInaccessibleOptions; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
460320
60
6979
2
275