@@ -6,7 +6,58 @@ import {Octokit} from '@octokit/core'
66import { throttling } from '@octokit/plugin-throttling'
77const OctokitWithThrottling = Octokit . plugin ( throttling )
88
9- describe . skip ( 'site-with-errors' , ( ) => {
9+ const WAIT_FOR_PULL_REQUESTS = ! ! process . env . WAIT_FOR_PULL_REQUESTS
10+ const POLL_INTERVAL_MS = 30_000 // 30 seconds
11+ const POLL_TIMEOUT_MS = 15 * 60 * 1000 // 15 minutes
12+
13+ /**
14+ * Repeatedly calls `fn` until `predicate(result)` returns `true`, or until the timeout is exceeded.
15+ * Errors thrown by `fn` (e.g. HTTP 404) are swallowed while polling continues.
16+ */
17+ async function pollUntil < T > (
18+ fn : ( ) => Promise < T > ,
19+ predicate : ( result : T ) => boolean ,
20+ { intervalMs, timeoutMs} : { intervalMs : number ; timeoutMs : number } ,
21+ ) : Promise < T > {
22+ const deadline = Date . now ( ) + timeoutMs
23+ let lastError : unknown
24+ while ( true ) {
25+ try {
26+ const result = await fn ( )
27+ if ( predicate ( result ) ) return result
28+ } catch ( error ) {
29+ lastError = error
30+ }
31+ if ( Date . now ( ) >= deadline ) {
32+ throw lastError ?? new Error ( `Timed out after ${ timeoutMs } ms waiting for condition` )
33+ }
34+ await new Promise ( resolve => setTimeout ( resolve , intervalMs ) )
35+ }
36+ }
37+
38+ function createOctokit ( ) : InstanceType < typeof OctokitWithThrottling > {
39+ return new OctokitWithThrottling ( {
40+ auth : process . env . GITHUB_TOKEN ,
41+ throttle : {
42+ onRateLimit : ( retryAfter , options , octokit , retryCount ) => {
43+ octokit . log . warn ( `Request quota exhausted for request ${ options . method } ${ options . url } ` )
44+ if ( retryCount < 3 ) {
45+ octokit . log . info ( `Retrying after ${ retryAfter } seconds!` )
46+ return true
47+ }
48+ } ,
49+ onSecondaryRateLimit : ( retryAfter , options , octokit , retryCount ) => {
50+ octokit . log . warn ( `Secondary rate limit hit for request ${ options . method } ${ options . url } ` )
51+ if ( retryCount < 3 ) {
52+ octokit . log . info ( `Retrying after ${ retryAfter } seconds!` )
53+ return true
54+ }
55+ } ,
56+ } ,
57+ } )
58+ }
59+
60+ describe ( 'site-with-errors' , ( ) => {
1061 let results : Result [ ]
1162
1263 beforeAll ( ( ) => {
@@ -16,11 +67,13 @@ describe.skip('site-with-errors', () => {
1667 } )
1768
1869 it ( 'cache has expected results' , ( ) => {
19- const actual = results . map ( ( { issue : { url : issueUrl } , pullRequest : { url : pullRequestUrl } , findings} ) => {
70+ const actual = results . map ( ( { issue : { url : issueUrl } , pullRequest, findings} ) => {
2071 const { problemUrl, solutionLong, screenshotId, ...finding } = findings [ 0 ]
2172 // Check volatile fields for existence only
2273 expect ( issueUrl ) . toBeDefined ( )
23- expect ( pullRequestUrl ) . toBeDefined ( )
74+ if ( WAIT_FOR_PULL_REQUESTS ) {
75+ expect ( pullRequest ?. url ) . toBeDefined ( )
76+ }
2477 expect ( problemUrl ) . toBeDefined ( )
2578 expect ( solutionLong ) . toBeDefined ( )
2679 // Check `problemUrl`, ignoring axe version
@@ -100,32 +153,11 @@ describe.skip('site-with-errors', () => {
100153 expect ( process . env . GITHUB_TOKEN ) . toBeDefined ( )
101154 } )
102155
103- describe . runIf ( ! ! process . env . GITHUB_TOKEN ) ( '—' , ( ) => {
104- let octokit : Octokit
156+ describe . runIf ( ! ! process . env . GITHUB_TOKEN ) ( 'issues' , ( ) => {
105157 let issues : Endpoints [ 'GET /repos/{owner}/{repo}/issues/{issue_number}' ] [ 'response' ] [ 'data' ] [ ]
106- let pullRequests : Endpoints [ 'GET /repos/{owner}/{repo}/pulls/{pull_number}' ] [ 'response' ] [ 'data' ] [ ]
107158
108159 beforeAll ( async ( ) => {
109- octokit = new OctokitWithThrottling ( {
110- auth : process . env . GITHUB_TOKEN ,
111- throttle : {
112- onRateLimit : ( retryAfter , options , octokit , retryCount ) => {
113- octokit . log . warn ( `Request quota exhausted for request ${ options . method } ${ options . url } ` )
114- if ( retryCount < 3 ) {
115- octokit . log . info ( `Retrying after ${ retryAfter } seconds!` )
116- return true
117- }
118- } ,
119- onSecondaryRateLimit : ( retryAfter , options , octokit , retryCount ) => {
120- octokit . log . warn ( `Secondary rate limit hit for request ${ options . method } ${ options . url } ` )
121- if ( retryCount < 3 ) {
122- octokit . log . info ( `Retrying after ${ retryAfter } seconds!` )
123- return true
124- }
125- } ,
126- } ,
127- } )
128- // Fetch issues referenced in the cache file
160+ const octokit = createOctokit ( )
129161 issues = await Promise . all (
130162 results . map ( async ( { issue : { url : issueUrl } } ) => {
131163 expect ( issueUrl ) . toBeDefined ( )
@@ -142,23 +174,6 @@ describe.skip('site-with-errors', () => {
142174 return issue
143175 } ) ,
144176 )
145- // Fetch pull requests referenced in the findings file
146- pullRequests = await Promise . all (
147- results . map ( async ( { pullRequest : { url : pullRequestUrl } } ) => {
148- expect ( pullRequestUrl ) . toBeDefined ( )
149- const { owner, repo, pullNumber} =
150- / h t t p s : \/ \/ g i t h u b \. c o m \/ (?< owner > [ ^ / ] + ) \/ (?< repo > [ ^ / ] + ) \/ p u l l \/ (?< pullNumber > \d + ) / . exec (
151- pullRequestUrl ! ,
152- ) ! . groups !
153- const { data : pullRequest } = await octokit . request ( 'GET /repos/{owner}/{repo}/pulls/{pull_number}' , {
154- owner,
155- repo,
156- pull_number : parseInt ( pullNumber , 10 ) ,
157- } )
158- expect ( pullRequest ) . toBeDefined ( )
159- return pullRequest
160- } ) ,
161- )
162177 } )
163178
164179 it ( 'issues exist and have expected title, state, and assignee' , async ( ) => {
@@ -179,6 +194,35 @@ describe.skip('site-with-errors', () => {
179194 expect ( issue . assignees ! . some ( a => a . login === 'Copilot' ) ) . toBe ( true )
180195 }
181196 } )
197+ } )
198+
199+ describe . runIf ( ! ! process . env . GITHUB_TOKEN && WAIT_FOR_PULL_REQUESTS ) ( 'pull requests' , ( ) => {
200+ let pullRequests : Endpoints [ 'GET /repos/{owner}/{repo}/pulls/{pull_number}' ] [ 'response' ] [ 'data' ] [ ]
201+
202+ beforeAll ( async ( ) => {
203+ const octokit = createOctokit ( )
204+ pullRequests = await Promise . all (
205+ results . map ( async ( { pullRequest : { url : pullRequestUrl } } ) => {
206+ expect ( pullRequestUrl ) . toBeDefined ( )
207+ const { owner, repo, pullNumber} =
208+ / h t t p s : \/ \/ g i t h u b \. c o m \/ (?< owner > [ ^ / ] + ) \/ (?< repo > [ ^ / ] + ) \/ p u l l \/ (?< pullNumber > \d + ) / . exec (
209+ pullRequestUrl ! ,
210+ ) ! . groups !
211+ return pollUntil (
212+ async ( ) => {
213+ const { data} = await octokit . request ( 'GET /repos/{owner}/{repo}/pulls/{pull_number}' , {
214+ owner,
215+ repo,
216+ pull_number : parseInt ( pullNumber , 10 ) ,
217+ } )
218+ return data
219+ } ,
220+ pr => pr . state === 'open' ,
221+ { intervalMs : POLL_INTERVAL_MS , timeoutMs : POLL_TIMEOUT_MS } ,
222+ )
223+ } ) ,
224+ )
225+ } , POLL_TIMEOUT_MS + 60_000 )
182226
183227 it ( 'pull requests exist and have expected author, state, and assignee' , async ( ) => {
184228 for ( const pullRequest of pullRequests ) {
0 commit comments