11import assert from 'node:assert/strict' ;
2- import { after , before , describe , it } from 'node:test' ;
2+ import { type TestContext , after , before , describe , it } from 'node:test' ;
33
44import { type AccessibilityAuditService } from '../../src/index.js' ;
55import * as Services from '../../src/services.js' ;
@@ -21,11 +21,20 @@ import {requireDeviceUdid} from './helpers/device.js';
2121// Generous: the audits alone take ~60s, and the settings tests deliberately
2222// pace their writes because the device commits them asynchronously.
2323describe ( 'AccessibilityAuditService' , { timeout : 300000 } , function ( ) {
24+ /** The selector the setting-write tests depend on. */
25+ const SETTING_WRITE_SELECTOR = 'deviceUpdateAccessibilitySetting:withValue:' ;
26+
2427 let service : AccessibilityAuditService | null = null ;
28+ /** Whether this device's daemon implements setting writes at all. */
29+ let supportsSettingWrites = false ;
2530
2631 before ( async function ( ) {
2732 const udid = requireDeviceUdid ( ) ;
2833 service = await Services . startAccessibilityAuditService ( udid ) ;
34+ // Gate on what the daemon advertises rather than on an OS version: it
35+ // publishes the selectors it implements, which is both more precise and
36+ // stable across releases.
37+ supportsSettingWrites = ( await service . getCapabilities ( ) ) . includes ( SETTING_WRITE_SELECTOR ) ;
2938 } ) ;
3039
3140 after ( function ( ) {
@@ -174,7 +183,59 @@ describe('AccessibilityAuditService', {timeout: 300000}, function () {
174183 * rather than session-scoped, so running it against a configured device
175184 * would discard real settings.
176185 */
186+ /**
187+ * An audit type the device does not implement makes the daemon return neither
188+ * issues nor a completion, and that connection can never run another audit —
189+ * so the name must be refused before it reaches the wire. Matching is exact:
190+ * case, surrounding whitespace and a bare prefix all wedge it.
191+ */
192+ describe ( 'audit type validation' , function ( ) {
193+ const POISON : Array < [ string , string [ ] ] > = [
194+ [ 'an invented name' , [ 'bogusAuditTypeThatDoesNotExist' ] ] ,
195+ [ 'the wrong case' , [ 'TESTTYPECONTRAST' ] ] ,
196+ [ 'an empty string' , [ '' ] ] ,
197+ [ 'a bare prefix' , [ 'testType' ] ] ,
198+ [ 'surrounding whitespace' , [ ' testTypeContrast ' ] ] ,
199+ ] ;
200+
201+ for ( const [ label , auditTypes ] of POISON ) {
202+ it ( `rejects ${ label } without contacting the device` , async function ( ) {
203+ await assert . rejects ( ( ) => service ! . runAudit ( auditTypes , { timeoutMs : 20000 } ) , / U n k n o w n a u d i t t y p e / ) ;
204+ } ) ;
205+ }
206+
207+ it ( 'rejects a valid type mixed with an unknown one' , async function ( ) {
208+ const types = await service ! . getSupportedAuditTypes ( ) ;
209+
210+ await assert . rejects ( ( ) => service ! . runAudit ( [ types [ 0 ] , 'bogusType' ] , { timeoutMs : 20000 } ) , / U n k n o w n a u d i t t y p e / ) ;
211+ } ) ;
212+
213+ it ( 'still accepts an empty list, which the device reads as every type' , async function ( ) {
214+ // Rejecting this would be a regression: the daemon audits everything.
215+ assert . ok ( Array . isArray ( await service ! . runAudit ( [ ] , { timeoutMs : 60000 } ) ) ) ;
216+ } ) ;
217+
218+ it ( 'leaves the connection usable after a rejected audit' , async function ( t ) {
219+ const types = await service ! . getSupportedAuditTypes ( ) ;
220+ await assert . rejects ( ( ) => service ! . runAudit ( [ 'nopeNotAType' ] , { timeoutMs : 20000 } ) , / U n k n o w n a u d i t t y p e / ) ;
221+
222+ // Before the guard this call timed out permanently on this connection.
223+ const issues = await service ! . runAudit ( types , { timeoutMs : 60000 } ) ;
224+ assert . ok ( Array . isArray ( issues ) ) ;
225+ t . diagnostic ( `audit after a rejected one returned ${ issues . length } issue(s)` ) ;
226+ } ) ;
227+ } ) ;
228+
177229 describe ( 'accessibility settings' , function ( ) {
230+ /** Skips when the daemon does not implement setting writes. */
231+ function skipWithoutSettingWrites ( t : TestContext ) : boolean {
232+ if ( ! supportsSettingWrites ) {
233+ t . skip ( `daemon does not implement ${ SETTING_WRITE_SELECTOR } ` ) ;
234+ return true ;
235+ }
236+ return false ;
237+ }
238+
178239 /** Polls until `identifier` reads `expected`, since a write settles asynchronously. */
179240 async function waitForSetting ( identifier : string , expected : unknown , timeoutMs = 8000 ) : Promise < unknown > {
180241 const deadline = performance . now ( ) + timeoutMs ;
@@ -196,6 +257,9 @@ describe('AccessibilityAuditService', {timeout: 300000}, function () {
196257 }
197258
198259 it ( 'quantises a slider value to the device tick marks' , async function ( t ) {
260+ if ( skipWithoutSettingWrites ( t ) ) {
261+ return ;
262+ }
199263 const settings = await service ! . getAccessibilitySettings ( ) ;
200264 const slider = settings . find ( ( setting ) => setting . identifier === 'DYNAMIC_TYPE' ) ;
201265 assert . ok ( slider , 'DYNAMIC_TYPE should be present' ) ;
@@ -217,20 +281,29 @@ describe('AccessibilityAuditService', {timeout: 300000}, function () {
217281 }
218282 } ) ;
219283
220- it ( 'rejects an unknown setting identifier instead of silently doing nothing' , async function ( ) {
284+ it ( 'rejects an unknown setting identifier instead of silently doing nothing' , async function ( t ) {
285+ if ( skipWithoutSettingWrites ( t ) ) {
286+ return ;
287+ }
221288 await assert . rejects (
222289 ( ) => service ! . setAccessibilitySetting ( 'NOT_A_REAL_SETTING' , true ) ,
223290 / U n k n o w n a c c e s s i b i l i t y s e t t i n g / ,
224291 ) ;
225292 } ) ;
226293
227- it ( 'rejects a value of the wrong kind for the setting' , async function ( ) {
294+ it ( 'rejects a value of the wrong kind for the setting' , async function ( t ) {
295+ if ( skipWithoutSettingWrites ( t ) ) {
296+ return ;
297+ }
228298 // DYNAMIC_TYPE is a slider, GRAYSCALE a toggle.
229299 await assert . rejects ( ( ) => service ! . setAccessibilitySetting ( 'DYNAMIC_TYPE' , true ) , / s l i d e r / ) ;
230300 await assert . rejects ( ( ) => service ! . setAccessibilitySetting ( 'GRAYSCALE' , 0.5 as unknown as boolean ) , / t o g g l e / ) ;
231301 } ) ;
232302
233- it ( 'rejects an out-of-range slider value without touching the device' , async function ( ) {
303+ it ( 'rejects an out-of-range slider value without touching the device' , async function ( t ) {
304+ if ( skipWithoutSettingWrites ( t ) ) {
305+ return ;
306+ }
234307 const before = await currentValue ( 'DYNAMIC_TYPE' ) ;
235308
236309 await assert . rejects ( ( ) => service ! . setAccessibilitySetting ( 'DYNAMIC_TYPE' , - 1 ) , / b e t w e e n 0 a n d 1 / ) ;
@@ -269,6 +342,9 @@ describe('AccessibilityAuditService', {timeout: 300000}, function () {
269342
270343 /** Reports why a test is being skipped, or null when it may run. */
271344 async function skipReason ( ) : Promise < string | null > {
345+ if ( ! supportsSettingWrites ) {
346+ return `daemon does not implement ${ SETTING_WRITE_SELECTOR } ` ;
347+ }
272348 if ( ! RESET_ALLOWED ) {
273349 return 'set ALLOW_ACCESSIBILITY_SETTINGS_RESET=1 to run — this permanently resets the accessibility settings on the device' ;
274350 }
0 commit comments