@afixt/test-utils 1.1.8 → 1.2.1

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.
@@ -19,7 +19,11 @@
19
19
  "Bash(npx vitest run:*)",
20
20
  "Bash(1)",
21
21
  "Bash(npm run test:playwright:css:*)",
22
- "Bash(gh run list:*)"
22
+ "Bash(gh run list:*)",
23
+ "Bash(gh pr create:*)",
24
+ "Bash(git checkout:*)",
25
+ "Bash(git pull:*)",
26
+ "Bash(git merge:*)"
23
27
  ]
24
28
  },
25
29
  "enableAllProjectMcpServers": false
@@ -0,0 +1,88 @@
1
+ name: PR Check
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main, develop]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ validate-workflows:
12
+ name: Validate GitHub Actions Workflows
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Checkout code
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Validate workflow syntax
19
+ run: |
20
+ echo "Validating GitHub Actions workflow files..."
21
+ errors=0
22
+
23
+ for f in .github/workflows/*.yml .github/workflows/*.yaml; do
24
+ [ -f "$f" ] || continue
25
+ echo "Checking $f..."
26
+
27
+ # Basic YAML syntax check
28
+ if ! python3 -c "import yaml; yaml.safe_load(open('$f'))" 2>/dev/null; then
29
+ echo "ERROR: Invalid YAML in $f"
30
+ errors=$((errors + 1))
31
+ fi
32
+
33
+ # Check for required 'on' and 'jobs' keys
34
+ if ! grep -q '^on:' "$f" && ! grep -q "^'on':" "$f" && ! grep -q '^"on":' "$f"; then
35
+ echo "WARNING: Missing 'on' trigger in $f"
36
+ fi
37
+
38
+ if ! grep -q '^jobs:' "$f"; then
39
+ echo "WARNING: Missing 'jobs' section in $f"
40
+ fi
41
+ done
42
+
43
+ if [ $errors -gt 0 ]; then
44
+ echo "Found $errors workflow file(s) with errors"
45
+ exit 1
46
+ fi
47
+
48
+ echo "All workflow files are valid"
49
+
50
+ ci-check:
51
+ name: CI Readiness Check
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - name: Checkout code
55
+ uses: actions/checkout@v4
56
+
57
+ - name: Detect project type
58
+ id: detect
59
+ run: |
60
+ if [ -f "package.json" ]; then
61
+ echo "type=node" >> $GITHUB_OUTPUT
62
+ elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
63
+ echo "type=python" >> $GITHUB_OUTPUT
64
+ else
65
+ echo "type=other" >> $GITHUB_OUTPUT
66
+ fi
67
+
68
+ - name: Setup Node.js
69
+ if: steps.detect.outputs.type == 'node'
70
+ uses: actions/setup-node@v4
71
+ with:
72
+ node-version: 'lts/*'
73
+ cache: 'npm'
74
+
75
+ - name: Install dependencies
76
+ if: steps.detect.outputs.type == 'node'
77
+ run: npm ci --ignore-scripts
78
+ continue-on-error: true
79
+
80
+ - name: Run lint check
81
+ if: steps.detect.outputs.type == 'node'
82
+ run: npm run lint --if-present
83
+ continue-on-error: true
84
+
85
+ - name: Run tests
86
+ if: steps.detect.outputs.type == 'node'
87
+ run: npm test --if-present
88
+ continue-on-error: true
@@ -5,9 +5,6 @@ on:
5
5
  branches: [main, develop]
6
6
  pull_request:
7
7
  branches: [main, develop]
8
- schedule:
9
- # Run comprehensive scan every Sunday at midnight
10
- - cron: '0 0 * * 0'
11
8
  workflow_dispatch:
12
9
 
13
10
  permissions:
package/eslint.config.mjs CHANGED
@@ -53,7 +53,7 @@ export default [
53
53
  },
54
54
  {
55
55
  // ESM file overrides
56
- files: ['**/*.mjs'],
56
+ files: ['**/*.mjs', 'vitest.config.js'],
57
57
  languageOptions: {
58
58
  sourceType: 'module',
59
59
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afixt/test-utils",
3
- "version": "1.1.8",
3
+ "version": "1.2.1",
4
4
  "description": "Various utilities for accessibility testing",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -53,6 +53,7 @@
53
53
  "devDependencies": {
54
54
  "@eslint/js": "^9.39.2",
55
55
  "@playwright/test": "^1.57.0",
56
+ "@vitest/coverage-istanbul": "^3.2.4",
56
57
  "@vitest/coverage-v8": "^3.0.9",
57
58
  "clean-jsdoc-theme": "^4.3.0",
58
59
  "eslint": "^9.39.2",
@@ -0,0 +1,231 @@
1
+ /**
2
+ * @file Shared constants for accessibility testing
3
+ * @module constants
4
+ */
5
+
6
+ /**
7
+ * CSS selector string for interactive elements.
8
+ * Matches native HTML interactive elements and ARIA widget roles.
9
+ * @type {string}
10
+ */
11
+ const INTERACTIVE_SELECTOR = [
12
+ 'a[href]',
13
+ 'button',
14
+ 'input:not([type="hidden"])',
15
+ 'select',
16
+ 'textarea',
17
+ '[tabindex]:not([tabindex="-1"])',
18
+ '[role="button"]',
19
+ '[role="link"]',
20
+ '[role="checkbox"]',
21
+ '[role="menuitem"]',
22
+ '[role="menuitemcheckbox"]',
23
+ '[role="menuitemradio"]',
24
+ '[role="tab"]',
25
+ '[role="switch"]',
26
+ '[role="option"]',
27
+ '[role="radio"]',
28
+ '[role="slider"]',
29
+ '[role="spinbutton"]',
30
+ '[role="textbox"]',
31
+ '[role="combobox"]',
32
+ '[role="listbox"]',
33
+ '[role="searchbox"]',
34
+ '[role="gridcell"]',
35
+ '[role="treeitem"]',
36
+ ].join(', ');
37
+
38
+ /**
39
+ * CSS selector string for naturally focusable elements.
40
+ * These elements are focusable by default (unless disabled).
41
+ * @type {string}
42
+ */
43
+ const FOCUSABLE_SELECTOR = [
44
+ 'a[href]',
45
+ 'button:not([disabled])',
46
+ 'input:not([disabled]):not([type="hidden"])',
47
+ 'select:not([disabled])',
48
+ 'textarea:not([disabled])',
49
+ '[tabindex]',
50
+ 'audio[controls]',
51
+ 'video[controls]',
52
+ 'embed',
53
+ 'iframe',
54
+ 'object',
55
+ '[contenteditable]:not([contenteditable="false"])',
56
+ ].join(', ');
57
+
58
+ /**
59
+ * Labellable elements per HTML spec that can be associated with a <label>.
60
+ * @type {string[]}
61
+ */
62
+ const LABELLABLE_ELEMENTS = [
63
+ 'input',
64
+ 'select',
65
+ 'textarea',
66
+ 'button',
67
+ 'meter',
68
+ 'output',
69
+ 'progress',
70
+ ];
71
+
72
+ /**
73
+ * Elements that validly support the alt attribute per HTML spec.
74
+ * @type {string[]}
75
+ */
76
+ const VALID_ALT_ELEMENTS = ['IMG', 'AREA', 'INPUT', 'APPLET'];
77
+
78
+ /**
79
+ * ISO 639-1 codes for right-to-left languages.
80
+ * @type {string[]}
81
+ */
82
+ const RTL_LANGUAGES = [
83
+ 'ar', // Arabic
84
+ 'he', // Hebrew
85
+ 'fa', // Persian/Farsi
86
+ 'ur', // Urdu
87
+ 'sd', // Sindhi
88
+ 'yi', // Yiddish
89
+ 'ps', // Pashto
90
+ 'ku', // Kurdish (some dialects)
91
+ 'ug', // Uyghur
92
+ 'dv', // Divehi/Maldivian
93
+ ];
94
+
95
+ /**
96
+ * Generic, non-descriptive, or meaningless frame/iframe titles.
97
+ * @type {string[]}
98
+ */
99
+ const GENERIC_TITLES = ['iframe', 'frame', 'untitled', 'title', 'content', 'main', 'page'];
100
+
101
+ /**
102
+ * Generic or useless table summaries commonly found in the wild.
103
+ * Merged from multiple detection contexts.
104
+ * @type {string[]}
105
+ */
106
+ const GENERIC_SUMMARIES = [
107
+ 'table',
108
+ 'data',
109
+ 'data table',
110
+ 'information',
111
+ 'content',
112
+ 'main content',
113
+ 'layout',
114
+ 'layout table',
115
+ 'for layout',
116
+ 'table for layout purposes',
117
+ 'structural table',
118
+ 'this table is used for page layout',
119
+ 'header',
120
+ 'footer',
121
+ 'navigation',
122
+ 'nav',
123
+ 'body',
124
+ 'combobox',
125
+ 'links design table',
126
+ 'title and navigation',
127
+ 'main heading',
128
+ 'spacer',
129
+ 'spacer table',
130
+ 'menu',
131
+ 'n/a',
132
+ 'na',
133
+ 'none',
134
+ 'null',
135
+ 'empty',
136
+ 'blank',
137
+ 'undefined',
138
+ 'table summary',
139
+ 'summary',
140
+ ];
141
+
142
+ /**
143
+ * Phrases indicating structure-focused rather than content-focused table summary.
144
+ * @type {string[]}
145
+ */
146
+ const STRUCTURE_PHRASES = [
147
+ 'rows and columns',
148
+ 'grid format',
149
+ 'for display purposes',
150
+ 'arranged in a grid',
151
+ 'the following table shows',
152
+ ];
153
+
154
+ /**
155
+ * Phrases indicating vague/rambling table summary content.
156
+ * @type {string[]}
157
+ */
158
+ const VAGUE_PHRASES = [
159
+ 'might find interesting',
160
+ 'depending on their needs',
161
+ 'users might find',
162
+ 'may be useful',
163
+ ];
164
+
165
+ /**
166
+ * Valid ARIA parent-child interactive role combinations per the WAI-ARIA spec.
167
+ * These composite widget patterns require interactive children by design.
168
+ * Key = parent role, Value = array of valid child roles.
169
+ * @type {Object.<string, string[]>}
170
+ */
171
+ const VALID_ARIA_NESTING = {
172
+ listbox: ['option'],
173
+ menu: ['menuitem', 'menuitemcheckbox', 'menuitemradio', 'menu'],
174
+ menubar: ['menuitem', 'menuitemcheckbox', 'menuitemradio', 'menu'],
175
+ menuitem: ['menu', 'menubar'],
176
+ tablist: ['tab'],
177
+ tree: ['treeitem', 'group'],
178
+ treeitem: ['group', 'tree'],
179
+ grid: ['gridcell', 'row', 'rowgroup'],
180
+ row: ['gridcell', 'columnheader', 'rowheader', 'cell'],
181
+ rowgroup: ['row'],
182
+ radiogroup: ['radio'],
183
+ combobox: ['listbox', 'textbox', 'tree', 'grid', 'dialog'],
184
+ };
185
+
186
+ /**
187
+ * Thresholds for determining table complexity.
188
+ * @type {Object}
189
+ */
190
+ const COMPLEXITY_THRESHOLDS = {
191
+ MAX_COLUMNS: 10,
192
+ MAX_HEADER_COLSPAN: 4,
193
+ MAX_ROWSPAN: 3,
194
+ MIN_HEADER_ROWS_FOR_COMPLEX: 3,
195
+ };
196
+
197
+ /**
198
+ * WCAG 2.5.5 minimum target size in CSS pixels.
199
+ * @type {number}
200
+ */
201
+ const MIN_TARGET_SIZE = 44;
202
+
203
+ /**
204
+ * Phrases that indicate a warning about new window/tab behavior.
205
+ * @type {string[]}
206
+ */
207
+ const NEW_WINDOW_WARNINGS = [
208
+ 'new window',
209
+ 'new tab',
210
+ 'opens in new',
211
+ 'opens in a new',
212
+ 'opens new',
213
+ 'external link',
214
+ 'external site',
215
+ ];
216
+
217
+ module.exports = {
218
+ INTERACTIVE_SELECTOR,
219
+ FOCUSABLE_SELECTOR,
220
+ LABELLABLE_ELEMENTS,
221
+ VALID_ALT_ELEMENTS,
222
+ RTL_LANGUAGES,
223
+ GENERIC_TITLES,
224
+ GENERIC_SUMMARIES,
225
+ STRUCTURE_PHRASES,
226
+ VAGUE_PHRASES,
227
+ VALID_ARIA_NESTING,
228
+ COMPLEXITY_THRESHOLDS,
229
+ MIN_TARGET_SIZE,
230
+ NEW_WINDOW_WARNINGS,
231
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @file CSS styling inspection utilities
3
+ * @module cssUtils
4
+ */
5
+
6
+ const cssUtils = {
7
+ /**
8
+ * Check if an element's background is transparent/none.
9
+ * @param {CSSStyleDeclaration} style - The computed style
10
+ * @returns {boolean} True if background is transparent
11
+ */
12
+ hasTransparentBackground(style) {
13
+ const bg = style.backgroundColor;
14
+ const bgImage = style.backgroundImage;
15
+
16
+ if (bgImage && bgImage !== 'none') {
17
+ return false;
18
+ }
19
+
20
+ return bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent' || bg === '';
21
+ },
22
+
23
+ /**
24
+ * Check if an element has no visible border.
25
+ * @param {CSSStyleDeclaration} style - The computed style
26
+ * @returns {boolean} True if no border
27
+ */
28
+ hasNoBorder(style) {
29
+ return style.borderStyle === 'none' || parseFloat(style.borderWidth) === 0;
30
+ },
31
+
32
+ /**
33
+ * Check if an element looks like regular text (no underline, background, or border).
34
+ * @param {HTMLElement} element - The element to check
35
+ * @returns {boolean} True if element looks like regular text
36
+ */
37
+ looksLikeText(element) {
38
+ const style = window.getComputedStyle(element);
39
+
40
+ const noUnderline = !style.textDecoration.toLowerCase().includes('underline');
41
+ const noBackground = cssUtils.hasTransparentBackground(style);
42
+ const noBorder = cssUtils.hasNoBorder(style);
43
+
44
+ return noUnderline && noBackground && noBorder;
45
+ },
46
+
47
+ /**
48
+ * Check if an element has link-like styling (underline).
49
+ * @param {HTMLElement} element - The element to check
50
+ * @returns {boolean} True if element has link-like styling
51
+ */
52
+ hasLinkStyling(element) {
53
+ const style = window.getComputedStyle(element);
54
+ return style.textDecoration.toLowerCase().includes('underline');
55
+ },
56
+
57
+ /**
58
+ * Check if an element has button-like styling (background/border with padding).
59
+ * @param {HTMLElement} element - The element to check
60
+ * @returns {boolean} True if element has button-like styling
61
+ */
62
+ hasButtonStyling(element) {
63
+ const style = window.getComputedStyle(element);
64
+
65
+ const hasBackground = !cssUtils.hasTransparentBackground(style);
66
+ const hasBorder = !cssUtils.hasNoBorder(style);
67
+ const hasPadding =
68
+ parseFloat(style.paddingTop) > 2 ||
69
+ parseFloat(style.paddingBottom) > 2 ||
70
+ parseFloat(style.paddingLeft) > 4 ||
71
+ parseFloat(style.paddingRight) > 4;
72
+
73
+ return (hasBackground || hasBorder) && hasPadding;
74
+ },
75
+ };
76
+
77
+ module.exports = cssUtils;