@afixt/test-utils 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,29 @@
1
+ const isHidden = require('./isHidden.js');
2
+ const hasHiddenParent = require('./hasHiddenParent.js');
3
+
1
4
  /**
2
- * Determines if an element is focusable.
5
+ * Determines if an element is focusable via user interaction (tab order).
6
+ * Elements with tabindex="-1" are programmatically focusable via .focus()
7
+ * but are NOT considered focusable by this function, as they are not
8
+ * reachable through keyboard navigation.
3
9
  * @param {Element} element - The HTML element to check.
4
10
  * @returns {boolean} - Returns true if the element is focusable, otherwise false.
5
11
  */
6
12
  function isFocusable(element) {
7
- if (!element) return false;
13
+ if (!element) {
14
+ return false;
15
+ }
8
16
 
9
17
  const nodeName = element.nodeName.toLowerCase();
10
- const tabIndex = element.getAttribute("tabindex");
18
+ const tabIndex = element.getAttribute('tabindex');
11
19
 
12
20
  // The element and all of its ancestors must be visible
13
- if (element.closest(":hidden")) {
21
+ if (isHidden(element) || hasHiddenParent(element)) {
14
22
  return false;
15
23
  }
16
24
 
17
- // If tabindex is defined, its value must be greater than or equal to 0
18
- if (!isNaN(tabIndex) && tabIndex < 0) {
25
+ // If tabindex is defined, its value must be >= 0
26
+ if (tabIndex !== null && !isNaN(tabIndex) && parseInt(tabIndex, 10) < 0) {
19
27
  return false;
20
28
  }
21
29
 
@@ -25,14 +33,26 @@ function isFocusable(element) {
25
33
  }
26
34
 
27
35
  // If the element is a link, href must be defined
28
- if (nodeName === "a" || nodeName === "area") {
29
- return element.hasAttribute("href");
36
+ if (nodeName === 'a' || nodeName === 'area') {
37
+ return element.hasAttribute('href');
38
+ }
39
+
40
+ // contenteditable elements are focusable
41
+ if (
42
+ element.getAttribute('contenteditable') === 'true' ||
43
+ element.getAttribute('contenteditable') === ''
44
+ ) {
45
+ return true;
46
+ }
47
+
48
+ // Any element with a non-negative tabindex is focusable
49
+ if (tabIndex !== null && !isNaN(tabIndex) && parseInt(tabIndex, 10) >= 0) {
50
+ return true;
30
51
  }
31
52
 
32
- // This is some other page element that is not normally focusable
33
53
  return false;
34
54
  }
35
55
 
36
56
  module.exports = {
37
- isFocusable
57
+ isFocusable,
38
58
  };
package/src/isHidden.js CHANGED
@@ -1,17 +1,53 @@
1
-
2
1
  /**
3
2
  * Checks if a given DOM element is hidden.
4
3
  *
5
- * An element is considered hidden if its `display` style is set to "none"
6
- * or if it has the `hidden` attribute.
4
+ * By default checks: computed display:none, visibility:hidden, and the hidden attribute.
5
+ * Additional checks can be enabled via the options parameter.
7
6
  *
8
7
  * @param {HTMLElement} element - The DOM element to check.
9
- * @returns {boolean} - Returns `true` if the element is hidden, otherwise `false`.
8
+ * @param {Object} [options={}] - Optional configuration.
9
+ * @param {boolean} [options.checkAriaHidden=false] - Also check aria-hidden="true".
10
+ * @param {boolean} [options.checkOpacity=false] - Also treat opacity:0 as hidden.
11
+ * @param {boolean} [options.checkDimensions=false] - Also treat zero width+height as hidden.
12
+ * @returns {boolean} True if the element is hidden.
10
13
  */
11
- const isHidden = (element) => {
12
- return (
13
- element.style.display === "none" || element.hasAttribute("hidden")
14
- );
14
+ const isHidden = (element, options = {}) => {
15
+ if (!element || !(element instanceof Element)) {
16
+ return false;
17
+ }
18
+
19
+ const { checkAriaHidden = false, checkOpacity = false, checkDimensions = false } = options;
20
+
21
+ // Check the hidden attribute
22
+ if (element.hasAttribute('hidden')) {
23
+ return true;
24
+ }
25
+
26
+ // Use computed style to catch CSS class/stylesheet rules
27
+ const style = window.getComputedStyle(element);
28
+
29
+ if (style.display === 'none') {
30
+ return true;
31
+ }
32
+
33
+ if (style.visibility === 'hidden') {
34
+ return true;
35
+ }
36
+
37
+ // Optional checks
38
+ if (checkAriaHidden && element.getAttribute('aria-hidden') === 'true') {
39
+ return true;
40
+ }
41
+
42
+ if (checkOpacity && style.opacity === '0') {
43
+ return true;
44
+ }
45
+
46
+ if (checkDimensions && element.offsetWidth === 0 && element.offsetHeight === 0) {
47
+ return true;
48
+ }
49
+
50
+ return false;
15
51
  };
16
52
 
17
53
  module.exports = isHidden;
@@ -5,6 +5,33 @@ const eventListenersMap = new WeakMap();
5
5
  const originalAddEventListener = EventTarget.prototype.addEventListener;
6
6
  const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
7
7
 
8
+ const KNOWN_EVENT_PROPERTIES = [
9
+ 'onclick',
10
+ 'ondblclick',
11
+ 'oncontextmenu',
12
+ 'onmousedown',
13
+ 'onmouseup',
14
+ 'onmouseover',
15
+ 'onmouseout',
16
+ 'onmouseenter',
17
+ 'onmouseleave',
18
+ 'onkeydown',
19
+ 'onkeyup',
20
+ 'onkeypress',
21
+ 'onfocus',
22
+ 'onblur',
23
+ 'onchange',
24
+ 'oninput',
25
+ 'onsubmit',
26
+ 'ontouchstart',
27
+ 'ontouchend',
28
+ 'ontouchmove',
29
+ 'onscroll',
30
+ 'onresize',
31
+ 'onload',
32
+ 'onerror',
33
+ ];
34
+
8
35
  // Override addEventListener to track event listeners
9
36
  EventTarget.prototype.addEventListener = function (type, listener, options) {
10
37
  if (!eventListenersMap.has(this)) {
@@ -25,14 +52,14 @@ EventTarget.prototype.removeEventListener = function (type, listener, options) {
25
52
  if (eventListenersMap.has(this)) {
26
53
  const listeners = eventListenersMap.get(this);
27
54
  if (listeners[type]) {
28
- listeners[type] = listeners[type].filter((l) => l.listener !== listener);
55
+ listeners[type] = listeners[type].filter(l => l.listener !== listener);
29
56
  }
30
57
  }
31
58
  return originalRemoveEventListener.call(this, type, listener, options);
32
59
  };
33
60
 
34
61
  // Function to get XPath of an element
35
- const getXPath = (element) => {
62
+ const getXPath = element => {
36
63
  if (element.id) {
37
64
  return `//*[@id="${element.id}"]`;
38
65
  }
@@ -52,9 +79,84 @@ const getXPath = (element) => {
52
79
  return path;
53
80
  };
54
81
 
55
- // Function to get all event listeners on an element
56
- const getEventListeners = (element) => {
57
- return eventListenersMap.get(element) || {};
82
+ /**
83
+ * Collect inline attribute event handlers from an element
84
+ * @param {Element} element - The element to inspect
85
+ * @returns {Object} Map of event names to listener entry arrays
86
+ */
87
+ const getAttributeHandlers = element => {
88
+ const handlers = {};
89
+ if (!element || !element.attributes) {
90
+ return handlers;
91
+ }
92
+ for (let i = 0; i < element.attributes.length; i++) {
93
+ const attr = element.attributes[i];
94
+ if (attr.name.startsWith('on')) {
95
+ const eventName = attr.name.slice(2);
96
+ handlers[eventName] = [{ listener: attr.value, bindingType: 'attribute' }];
97
+ }
98
+ }
99
+ return handlers;
100
+ };
101
+
102
+ /**
103
+ * Collect property-based event handlers from an element
104
+ * @param {Element} element - The element to inspect
105
+ * @param {Object} attributeHandlers - Already-detected attribute handlers to avoid duplicates
106
+ * @returns {Object} Map of event names to listener entry arrays
107
+ */
108
+ const getPropertyHandlers = (element, attributeHandlers) => {
109
+ const handlers = {};
110
+ if (!element) {
111
+ return handlers;
112
+ }
113
+ for (const prop of KNOWN_EVENT_PROPERTIES) {
114
+ if (typeof element[prop] === 'function') {
115
+ const eventName = prop.slice(2);
116
+ if (!attributeHandlers[eventName]) {
117
+ handlers[eventName] = [{ listener: element[prop], bindingType: 'property' }];
118
+ }
119
+ }
120
+ }
121
+ return handlers;
122
+ };
123
+
124
+ /**
125
+ * Get all event listeners on an element, including addEventListener, attribute, and property handlers
126
+ * @param {Element} element - The element to inspect
127
+ * @returns {Object} Map of event names to listener entry arrays
128
+ */
129
+ const getEventListeners = element => {
130
+ const tracked = eventListenersMap.get(element) || {};
131
+
132
+ // Add bindingType to tracked entries
133
+ const result = {};
134
+ for (const eventName of Object.keys(tracked)) {
135
+ result[eventName] = tracked[eventName].map(entry => ({
136
+ ...entry,
137
+ bindingType: 'addEventListener',
138
+ }));
139
+ }
140
+
141
+ // Merge attribute handlers
142
+ const attrHandlers = getAttributeHandlers(element);
143
+ for (const eventName of Object.keys(attrHandlers)) {
144
+ if (!result[eventName]) {
145
+ result[eventName] = [];
146
+ }
147
+ result[eventName].push(...attrHandlers[eventName]);
148
+ }
149
+
150
+ // Merge property handlers (excluding those already found as attributes)
151
+ const propHandlers = getPropertyHandlers(element, attrHandlers);
152
+ for (const eventName of Object.keys(propHandlers)) {
153
+ if (!result[eventName]) {
154
+ result[eventName] = [];
155
+ }
156
+ result[eventName].push(...propHandlers[eventName]);
157
+ }
158
+
159
+ return result;
58
160
  };
59
161
 
60
162
  /**
@@ -72,11 +174,14 @@ const listEventListeners = (rootElement = document) => {
72
174
  function processElement(el) {
73
175
  const listeners = getEventListeners(el);
74
176
  if (Object.keys(listeners).length > 0) {
75
- Object.keys(listeners).forEach((eventName) => {
76
- eventListeners.push({
77
- element: el.tagName.toLowerCase(),
78
- xpath: getXPath(el),
79
- event: eventName
177
+ Object.keys(listeners).forEach(eventName => {
178
+ listeners[eventName].forEach(entry => {
179
+ eventListeners.push({
180
+ element: el.tagName.toLowerCase(),
181
+ xpath: getXPath(el),
182
+ event: eventName,
183
+ bindingType: entry.bindingType,
184
+ });
80
185
  });
81
186
  });
82
187
  }
@@ -1,3 +1,5 @@
1
+ const { GENERIC_TITLES, GENERIC_LINK_TEXT } = require('./constants.js');
2
+
1
3
  const stringUtils = (function () {
2
4
  /**
3
5
  * Check if a string is empty or only contains whitespace.
@@ -83,45 +85,6 @@ const stringUtils = (function () {
83
85
  return str.pathname;
84
86
  }
85
87
 
86
- /**
87
- * Extracts and concatenates all text content from a given DOM element, including text from text nodes,
88
- * elements with aria-label attributes, and alt attributes of img elements.
89
- *
90
- * @param {Element} el - The DOM element from which to extract text.
91
- * @returns {string} A string containing all concatenated text content from the element.
92
- */
93
- function getAllText(el) {
94
- // Check for form element value (input, textarea, select)
95
- if (el.value !== undefined && el.value !== '') {
96
- return el.value;
97
- }
98
-
99
- const walker = document.createTreeWalker(el, NodeFilter.SHOW_ALL, null, false);
100
- const textNodes = [];
101
- let node;
102
- let text;
103
-
104
- while (walker.nextNode()) {
105
- node = walker.currentNode;
106
- if (node.nodeType === Node.TEXT_NODE) {
107
- text = node.nodeValue.trim();
108
- if (text) {
109
- textNodes.push(text);
110
- } else {
111
- textNodes.push(node.textContent.trim());
112
- }
113
- } else if (node.nodeType === Node.ELEMENT_NODE) {
114
- if (node.hasAttribute('aria-label')) {
115
- textNodes.push(node.getAttribute('aria-label'));
116
- } else if (node.tagName === 'IMG' && node.hasAttribute('alt')) {
117
- textNodes.push(node.getAttribute('alt'));
118
- }
119
- }
120
- }
121
-
122
- return textNodes.join(' ');
123
- }
124
-
125
88
  /**
126
89
  * Checks if the given element contains any text.
127
90
  *
@@ -129,7 +92,16 @@ const stringUtils = (function () {
129
92
  * @returns {boolean} True if the element contains text, false otherwise.
130
93
  */
131
94
  function hasText(element) {
132
- return getAllText(element).trim() !== '';
95
+ if (!element) {
96
+ return false;
97
+ }
98
+ // Check form element value (input, textarea, select)
99
+ if (element.value !== undefined && element.value !== '') {
100
+ return true;
101
+ }
102
+ // Lazy require to avoid circular dependency (getAccessibleText requires stringUtils)
103
+ const { getAccessibleText } = require('./getAccessibleText.js');
104
+ return getAccessibleText(element).trim() !== '';
133
105
  }
134
106
 
135
107
  /**
@@ -156,14 +128,17 @@ const stringUtils = (function () {
156
128
  if (!title) {
157
129
  return false;
158
130
  }
159
- const genericTitles = ['iframe', 'frame', 'untitled', 'title', 'content', 'main', 'page'];
160
131
  const normalized = title.toLowerCase().trim();
161
132
 
162
- if (genericTitles.includes(normalized)) {
133
+ if (GENERIC_TITLES.includes(normalized)) {
163
134
  return true;
164
135
  }
165
136
 
166
- if (/^(frame|iframe|untitled|title)\d*$/i.test(normalized)) {
137
+ if (
138
+ /^(frame|iframe|untitled|title|page|content|section|document|tab|slide|sheet|panel|window|screen|view|module|widget|region|form)\s?\d+$/i.test(
139
+ normalized
140
+ )
141
+ ) {
167
142
  return true;
168
143
  }
169
144
 
@@ -180,32 +155,7 @@ const stringUtils = (function () {
180
155
  if (!text) {
181
156
  return false;
182
157
  }
183
- const defaults = [
184
- 'click here',
185
- 'here',
186
- 'more',
187
- 'read more',
188
- 'learn more',
189
- 'click',
190
- 'link',
191
- 'this',
192
- 'page',
193
- 'article',
194
- 'continue',
195
- 'go',
196
- 'see more',
197
- 'view',
198
- 'download',
199
- 'pdf',
200
- 'document',
201
- 'form',
202
- 'submit',
203
- 'button',
204
- 'press',
205
- 'select',
206
- 'choose',
207
- ];
208
- const list = genericList || defaults;
158
+ const list = genericList || GENERIC_LINK_TEXT;
209
159
  const lowerText = text.toLowerCase().trim();
210
160
  return list.some(function (generic) {
211
161
  return lowerText === generic;
@@ -248,33 +198,6 @@ const stringUtils = (function () {
248
198
  });
249
199
  }
250
200
 
251
- /**
252
- * Extracts DOM text content from an element, including img alt text,
253
- * but excluding ARIA attributes (aria-label, aria-labelledby).
254
- * Useful for detecting actual visible/DOM text that may conflict with ARIA labels.
255
- *
256
- * @param {Element} root - The DOM element from which to extract text.
257
- * @returns {string} Concatenated text from text nodes and img alt attributes.
258
- */
259
- function textIncludingImgAlt(root) {
260
- let out = '';
261
- const walker = document.createTreeWalker(
262
- root,
263
- NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT // eslint-disable-line no-bitwise
264
- );
265
- let n = walker.currentNode;
266
- while (n) {
267
- if (n.nodeType === Node.TEXT_NODE) {
268
- out += n.nodeValue;
269
- }
270
- if (n.nodeType === Node.ELEMENT_NODE && n.tagName === 'IMG') {
271
- out += n.getAttribute('alt') || '';
272
- }
273
- n = walker.nextNode();
274
- }
275
- return out;
276
- }
277
-
278
201
  return {
279
202
  isEmpty,
280
203
  isString,
@@ -283,9 +206,7 @@ const stringUtils = (function () {
283
206
  isUpperCase,
284
207
  isAlphaNumeric,
285
208
  getPathFromUrl,
286
- getAllText,
287
209
  hasText,
288
- textIncludingImgAlt,
289
210
  isEmptyOrWhitespace,
290
211
  isGenericTitle,
291
212
  isGenericLinkText,
package/src/tableUtils.js CHANGED
@@ -3,6 +3,8 @@
3
3
  * @module tableUtils
4
4
  */
5
5
 
6
+ const { GENERIC_SUMMARIES } = require('./constants.js');
7
+
6
8
  const tableUtils = {
7
9
  /**
8
10
  * Check if table has multiple header rows (rows containing th elements).
@@ -137,43 +139,9 @@ const tableUtils = {
137
139
  * @returns {boolean} True if the summary is generic
138
140
  */
139
141
  isGenericSummary(summary, genericList) {
140
- const GENERIC_SUMMARIES = genericList || [
141
- 'table',
142
- 'data',
143
- 'data table',
144
- 'information',
145
- 'content',
146
- 'main content',
147
- 'layout',
148
- 'layout table',
149
- 'for layout',
150
- 'table for layout purposes',
151
- 'structural table',
152
- 'this table is used for page layout',
153
- 'header',
154
- 'footer',
155
- 'navigation',
156
- 'nav',
157
- 'body',
158
- 'combobox',
159
- 'links design table',
160
- 'title and navigation',
161
- 'main heading',
162
- 'spacer',
163
- 'spacer table',
164
- 'menu',
165
- 'n/a',
166
- 'na',
167
- 'none',
168
- 'null',
169
- 'empty',
170
- 'blank',
171
- 'undefined',
172
- 'table summary',
173
- 'summary',
174
- ];
142
+ const list = genericList || GENERIC_SUMMARIES;
175
143
  const normalized = summary.toLowerCase().trim();
176
- return GENERIC_SUMMARIES.includes(normalized);
144
+ return list.includes(normalized);
177
145
  },
178
146
  };
179
147
 
@@ -710,6 +710,72 @@ describe('domUtils', () => {
710
710
  expect(domUtils.hasInteractiveHandler(el)).toBe(false);
711
711
  });
712
712
 
713
+ it('should return true for element with ondblclick', () => {
714
+ const el = document.createElement('div');
715
+ el.setAttribute('ondblclick', 'handle()');
716
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
717
+ });
718
+
719
+ it('should return true for element with oncontextmenu', () => {
720
+ const el = document.createElement('div');
721
+ el.setAttribute('oncontextmenu', 'handle()');
722
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
723
+ });
724
+
725
+ it('should return true for element with onkeypress', () => {
726
+ const el = document.createElement('div');
727
+ el.setAttribute('onkeypress', 'handle()');
728
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
729
+ });
730
+
731
+ it('should return true for element with onfocus', () => {
732
+ const el = document.createElement('div');
733
+ el.setAttribute('onfocus', 'handle()');
734
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
735
+ });
736
+
737
+ it('should return true for element with onblur', () => {
738
+ const el = document.createElement('div');
739
+ el.setAttribute('onblur', 'handle()');
740
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
741
+ });
742
+
743
+ it('should return true for element with oninput', () => {
744
+ const el = document.createElement('div');
745
+ el.setAttribute('oninput', 'handle()');
746
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
747
+ });
748
+
749
+ it('should return true for element with onchange', () => {
750
+ const el = document.createElement('div');
751
+ el.setAttribute('onchange', 'handle()');
752
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
753
+ });
754
+
755
+ it('should return true for element with onsubmit', () => {
756
+ const el = document.createElement('div');
757
+ el.setAttribute('onsubmit', 'handle()');
758
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
759
+ });
760
+
761
+ it('should return true for element with ontouchend', () => {
762
+ const el = document.createElement('div');
763
+ el.setAttribute('ontouchend', 'handle()');
764
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
765
+ });
766
+
767
+ it('should return true for element with onpointerdown', () => {
768
+ const el = document.createElement('div');
769
+ el.setAttribute('onpointerdown', 'handle()');
770
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
771
+ });
772
+
773
+ it('should return true for element with onpointerup', () => {
774
+ const el = document.createElement('div');
775
+ el.setAttribute('onpointerup', 'handle()');
776
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
777
+ });
778
+
713
779
  it('should return false for element with non-interactive event handlers', () => {
714
780
  const el = document.createElement('div');
715
781
  el.setAttribute('onload', 'handle()');
@@ -849,6 +915,96 @@ describe('domUtils', () => {
849
915
  const result = domUtils.getSemanticContainer(document.getElementById('child'));
850
916
  expect(result.tagName).toBe('SECTION');
851
917
  });
918
+
919
+ it('should return main ancestor', () => {
920
+ document.body.innerHTML = '<main><p id="child">Content</p></main>';
921
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
922
+ expect(result.tagName).toBe('MAIN');
923
+ });
924
+
925
+ it('should return nav ancestor', () => {
926
+ document.body.innerHTML = '<nav><p id="child">Content</p></nav>';
927
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
928
+ expect(result.tagName).toBe('NAV');
929
+ });
930
+
931
+ it('should return aside ancestor', () => {
932
+ document.body.innerHTML = '<aside><p id="child">Content</p></aside>';
933
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
934
+ expect(result.tagName).toBe('ASIDE');
935
+ });
936
+
937
+ it('should return header ancestor', () => {
938
+ document.body.innerHTML = '<header><p id="child">Content</p></header>';
939
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
940
+ expect(result.tagName).toBe('HEADER');
941
+ });
942
+
943
+ it('should return footer ancestor', () => {
944
+ document.body.innerHTML = '<footer><p id="child">Content</p></footer>';
945
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
946
+ expect(result.tagName).toBe('FOOTER');
947
+ });
948
+
949
+ it('should return form ancestor', () => {
950
+ document.body.innerHTML = '<form><p id="child">Content</p></form>';
951
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
952
+ expect(result.tagName).toBe('FORM');
953
+ });
954
+
955
+ it('should return fieldset ancestor', () => {
956
+ document.body.innerHTML = '<fieldset><p id="child">Content</p></fieldset>';
957
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
958
+ expect(result.tagName).toBe('FIELDSET');
959
+ });
960
+
961
+ it('should return details ancestor', () => {
962
+ document.body.innerHTML = '<details><p id="child">Content</p></details>';
963
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
964
+ expect(result.tagName).toBe('DETAILS');
965
+ });
966
+
967
+ it('should return dialog ancestor', () => {
968
+ document.body.innerHTML = '<dialog><p id="child">Content</p></dialog>';
969
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
970
+ expect(result.tagName).toBe('DIALOG');
971
+ });
972
+
973
+ it('should return element with role="group"', () => {
974
+ document.body.innerHTML = '<div role="group"><p id="child">Content</p></div>';
975
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
976
+ expect(result.getAttribute('role')).toBe('group');
977
+ });
978
+
979
+ it('should return element with role="tabpanel"', () => {
980
+ document.body.innerHTML = '<div role="tabpanel"><p id="child">Content</p></div>';
981
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
982
+ expect(result.getAttribute('role')).toBe('tabpanel');
983
+ });
984
+
985
+ it('should return element with role="dialog"', () => {
986
+ document.body.innerHTML = '<div role="dialog"><p id="child">Content</p></div>';
987
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
988
+ expect(result.getAttribute('role')).toBe('dialog');
989
+ });
990
+
991
+ it('should return element with role="navigation"', () => {
992
+ document.body.innerHTML = '<div role="navigation"><p id="child">Content</p></div>';
993
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
994
+ expect(result.getAttribute('role')).toBe('navigation');
995
+ });
996
+
997
+ it('should return element with role="complementary"', () => {
998
+ document.body.innerHTML = '<div role="complementary"><p id="child">Content</p></div>';
999
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
1000
+ expect(result.getAttribute('role')).toBe('complementary');
1001
+ });
1002
+
1003
+ it('should return null for non-structural role like presentation', () => {
1004
+ document.body.innerHTML = '<div role="presentation"><p id="child">Content</p></div>';
1005
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
1006
+ expect(result).toBeNull();
1007
+ });
852
1008
  });
853
1009
 
854
1010
  describe('getHeadingLevel', () => {