11import type { Protocol } from "devtools-protocol" ;
2- import { beforeEach , describe , expect , it , vi } from "vitest" ;
2+ import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
33import type { CDPSessionLike } from "../../cdp.js" ;
4+ import { Progress } from "../../progress.js" ;
5+ import { executionContexts } from "../../executionContextRegistry.js" ;
46import { a11yForFrame } from "./a11yTree.js" ;
57import { resolveObjectIdForCss , resolveObjectIdForXPath } from "./focusSelectors.js" ;
68
7- vi . mock ( "./focusSelectors.js" , ( ) => ( {
9+ vi . mock ( "./focusSelectors.js" , async ( importOriginal ) => ( {
10+ ...( await importOriginal < typeof import ( "./focusSelectors.js" ) > ( ) ) ,
811 resolveObjectIdForCss : vi . fn ( ) ,
912 resolveObjectIdForXPath : vi . fn ( ) ,
1013} ) ) ;
@@ -14,6 +17,92 @@ describe("a11yForFrame focused locators", () => {
1417 vi . resetAllMocks ( ) ;
1518 } ) ;
1619
20+ afterEach ( ( ) => {
21+ vi . useRealTimers ( ) ;
22+ vi . restoreAllMocks ( ) ;
23+ } ) ;
24+
25+ it . each ( [ "Accessibility.getFullAXTree" , "DOM.describeNode" ] ) (
26+ "does not fall back after %s exceeds the deadline" ,
27+ async ( command ) => {
28+ vi . useFakeTimers ( ) ;
29+ const progress = new Progress ( "act()" , 10 ) ;
30+ vi . mocked ( resolveObjectIdForCss ) . mockResolvedValue ( "object-second" ) ;
31+ const session = fakeSession ( 20 , ( method ) => {
32+ if ( method === command ) {
33+ vi . spyOn ( performance , "now" ) . mockReturnValue ( 10 ) ;
34+ throw new Error ( "Frame with the given id is not found" ) ;
35+ }
36+ } ) ;
37+ const send = vi . spyOn ( session , "send" ) ;
38+ try {
39+ await expect (
40+ a11yForFrame (
41+ session ,
42+ "frame" ,
43+ {
44+ focusLocator : { selector : ".card" } ,
45+ tagNameMap : { } ,
46+ scrollableMap : { } ,
47+ encode : String ,
48+ } ,
49+ progress ,
50+ ) ,
51+ ) . rejects . toThrow ( / a c t \( \) t i m e d o u t / ) ;
52+ expect ( send . mock . calls . filter ( ( [ method ] ) => method === command ) ) . toHaveLength ( 1 ) ;
53+ if ( command === "DOM.describeNode" ) {
54+ expect ( send ) . toHaveBeenCalledWith ( "Runtime.releaseObject" , {
55+ objectId : "object-second" ,
56+ } ) ;
57+ }
58+ } finally {
59+ progress . dispose ( ) ;
60+ }
61+ } ,
62+ ) ;
63+
64+ it . each ( [ ".card" , "xpath=//article" ] ) (
65+ "bounds a stalled %s focus lookup by the caller's deadline" ,
66+ async ( selector ) => {
67+ vi . useFakeTimers ( ) ;
68+ const progress = new Progress ( "snapshot" , 10 ) ;
69+ const actual =
70+ await vi . importActual < typeof import ( "./focusSelectors.js" ) > ( "./focusSelectors.js" ) ;
71+ vi . mocked ( resolveObjectIdForCss ) . mockImplementation ( actual . resolveObjectIdForCss ) ;
72+ vi . mocked ( resolveObjectIdForXPath ) . mockImplementation ( actual . resolveObjectIdForXPath ) ;
73+ const session = fakeSession ( 20 , async ( method ) => {
74+ if ( method === "Runtime.evaluate" ) await new Promise ( ( resolve ) => setTimeout ( resolve , 25 ) ) ;
75+ } ) ;
76+ executionContexts . registerExtensionWorld ( session , "frame" , 1 ) ;
77+ const send = vi . spyOn ( session , "send" ) ;
78+ let failure : unknown ;
79+ const settled = a11yForFrame (
80+ session ,
81+ "frame" ,
82+ {
83+ focusLocator : { selector } ,
84+ tagNameMap : { } ,
85+ scrollableMap : { } ,
86+ encode : String ,
87+ } ,
88+ progress ,
89+ ) . catch ( ( error : unknown ) => {
90+ failure = error ;
91+ } ) ;
92+ try {
93+ await vi . advanceTimersByTimeAsync ( 10 ) ;
94+ const failureAtDeadline = failure ;
95+ await vi . advanceTimersByTimeAsync ( 15 ) ;
96+ await settled ;
97+ expect ( failureAtDeadline ) . toBe ( progress . signal . reason ) ;
98+ expect ( send ) . toHaveBeenCalledWith ( "Runtime.releaseObject" , { objectId : "object-second" } ) ;
99+ expect ( send ) . not . toHaveBeenCalledWith ( "DOM.describeNode" , expect . anything ( ) ) ;
100+ } finally {
101+ progress . dispose ( ) ;
102+ }
103+ } ,
104+ ) ;
105+
17106 it . each ( [
18107 {
19108 name : "CSS" ,
@@ -30,6 +119,7 @@ describe("a11yForFrame focused locators", () => {
30119 async ( { selector, resolver } ) => {
31120 vi . mocked ( resolver ) . mockResolvedValue ( "object-second" ) ;
32121 const session = fakeSession ( 20 ) ;
122+ const send = vi . spyOn ( session , "send" ) ;
33123
34124 const result = await a11yForFrame ( session , "root-frame" , {
35125 focusLocator : { selector, nth : 1 } ,
@@ -43,24 +133,35 @@ describe("a11yForFrame focused locators", () => {
43133 expect ( result . outline ) . toContain ( "Second detail" ) ;
44134 expect ( result . outline ) . not . toContain ( "First match" ) ;
45135 expect ( result . outline ) . not . toContain ( "First detail" ) ;
46- expect ( resolver ) . toHaveBeenCalledWith ( session , selector , "root-frame" , 1 ) ;
136+ expect ( resolver ) . toHaveBeenCalledWith ( session , selector , "root-frame" , 1 , undefined ) ;
137+ expect ( send ) . toHaveBeenCalledWith ( "Runtime.releaseObject" , {
138+ objectId : "object-second" ,
139+ } ) ;
47140 } ,
48141 ) ;
49142} ) ;
50143
51- function fakeSession ( focusedBackendNodeId : number ) : CDPSessionLike {
144+ function fakeSession (
145+ focusedBackendNodeId : number ,
146+ beforeSend ?: ( method : string ) => void | Promise < void > ,
147+ ) : CDPSessionLike {
52148 return {
53149 send : vi . fn ( async ( method : string , params ?: Record < string , unknown > ) => {
150+ await beforeSend ?.( method ) ;
54151 if (
55152 method === "Accessibility.enable" ||
56153 method === "Runtime.enable" ||
57- method === "DOM.enable"
154+ method === "DOM.enable" ||
155+ method === "Runtime.releaseObject"
58156 ) {
59157 return { } ;
60158 }
61159 if ( method === "Accessibility.getFullAXTree" ) {
62160 return { nodes : repeatedSubtreeNodes ( ) } ;
63161 }
162+ if ( method === "Runtime.evaluate" ) {
163+ return { result : { type : "object" , objectId : "object-second" } } ;
164+ }
64165 if ( method === "DOM.describeNode" && params ?. objectId === "object-second" ) {
65166 return { node : { backendNodeId : focusedBackendNodeId } } ;
66167 }
0 commit comments