@@ -55,6 +55,9 @@ import {
55
55
import * as msw from 'msw' ;
56
56
import { setupWorker } from 'msw/browser' ;
57
57
58
+ // This should match the unexported constant with the same name in fetch.ts
59
+ const OBSERVER_WAIT_TIME_MS = 300 ;
60
+
58
61
class DummySpanExporter implements tracing . SpanExporter {
59
62
readonly exported : tracing . ReadableSpan [ ] [ ] = [ ] ;
60
63
@@ -91,6 +94,34 @@ const ORIGIN = location.origin;
91
94
// "localhost:9876"
92
95
const ORIGIN_HOST = new URL ( ORIGIN ) . host ;
93
96
97
+ interface Resolvers < T > {
98
+ promise : Promise < T > ;
99
+ resolve : ( value : T ) => void ;
100
+ reject : ( reason : any ) => void ;
101
+ }
102
+
103
+ // Use Promise.withResolvers when we can
104
+ function withResolvers < T > ( ) : Resolvers < T > {
105
+ let resolve : ( value : T ) => void ;
106
+ let reject : ( reason : any ) => void ;
107
+ let promise = new Promise < T > ( ( res , rej ) => {
108
+ resolve = res ;
109
+ reject = rej ;
110
+ } ) ;
111
+
112
+ return {
113
+ promise,
114
+ resolve : resolve ! ,
115
+ reject : reject ! ,
116
+ } ;
117
+ }
118
+
119
+ function waitFor ( timeout : number ) : Promise < void > {
120
+ return new Promise ( resolve => {
121
+ setTimeout ( resolve , timeout ) ;
122
+ } ) ;
123
+ }
124
+
94
125
describe ( 'fetch' , ( ) => {
95
126
let workerStarted = false ;
96
127
@@ -105,13 +136,74 @@ describe('fetch', () => {
105
136
workerStarted = true ;
106
137
} ;
107
138
108
- afterEach ( ( ) => {
109
- if ( workerStarted ) {
110
- worker . stop ( ) ;
111
- workerStarted = false ;
139
+ let pendingObservers = 0 ;
140
+ let waitForPerformanceObservers = async ( ) => { } ;
141
+
142
+ beforeEach ( ( ) => {
143
+ if ( PerformanceObserver ) {
144
+ assert . strictEqual (
145
+ pendingObservers ,
146
+ 0 ,
147
+ 'Did a previous test leak a PerformanceObserver?'
148
+ ) ;
149
+
150
+ let resolvers : Resolvers < void > | undefined ;
151
+
152
+ let _observe = PerformanceObserver . prototype . observe ;
153
+ let _disconnect = PerformanceObserver . prototype . disconnect ;
154
+
155
+ sinon . stub ( PerformanceObserver . prototype , 'observe' ) . callsFake ( function (
156
+ this : PerformanceObserver ,
157
+ ...args
158
+ ) {
159
+ _observe . call ( this , ...args ) ;
160
+ pendingObservers ++ ;
161
+
162
+ if ( ! resolvers ) {
163
+ resolvers = withResolvers ( ) ;
164
+ }
165
+ } ) ;
166
+
167
+ sinon
168
+ . stub ( PerformanceObserver . prototype , 'disconnect' )
169
+ . callsFake ( function ( this : PerformanceObserver , ...args ) {
170
+ _disconnect . call ( this , ...args ) ;
171
+ pendingObservers -- ;
172
+
173
+ if ( pendingObservers === 0 ) {
174
+ resolvers ?. resolve ( ) ;
175
+ resolvers = undefined ;
176
+ }
177
+ } ) ;
178
+
179
+ waitForPerformanceObservers = async ( ) : Promise < void > => {
180
+ while ( resolvers ) {
181
+ await resolvers . promise ;
182
+ }
183
+ } ;
112
184
}
185
+ } ) ;
186
+
187
+ afterEach ( ( ) => {
188
+ try {
189
+ if ( workerStarted ) {
190
+ worker . stop ( ) ;
191
+ workerStarted = false ;
192
+ }
193
+
194
+ const _pendingObservers = pendingObservers ;
113
195
114
- sinon . restore ( ) ;
196
+ pendingObservers = 0 ;
197
+ waitForPerformanceObservers = async ( ) => { } ;
198
+
199
+ assert . strictEqual (
200
+ _pendingObservers ,
201
+ 0 ,
202
+ `Test leaked ${ _pendingObservers } \`PerformanceObserver\`(s)!`
203
+ ) ;
204
+ } finally {
205
+ sinon . restore ( ) ;
206
+ }
115
207
} ) ;
116
208
117
209
describe ( 'enabling/disabling' , ( ) => {
@@ -178,10 +270,7 @@ describe('fetch', () => {
178
270
callback
179
271
) ;
180
272
181
- // FIXME!
182
- await new Promise ( resolve => {
183
- setTimeout ( resolve , 500 ) ;
184
- } ) ;
273
+ await waitForPerformanceObservers ( ) ;
185
274
186
275
if ( expectExport ) {
187
276
// This isn't intended to be an invariant, but in the current setup we
@@ -1594,7 +1683,28 @@ describe('fetch', () => {
1594
1683
1595
1684
getEntriesByTypeSpy = sinon . spy ( performance , 'getEntriesByType' ) ;
1596
1685
1597
- const result = await tracedFetch ( ) ;
1686
+ const result = await tracedFetch ( {
1687
+ callback : async ( ) => {
1688
+ const response = await fetch ( '/api/status.json' ) ;
1689
+
1690
+ // This seemingly random timeout is testing real behavior!
1691
+ //
1692
+ // Currently, the implementation works by waiting a hardcoded
1693
+ // OBSERVER_WAIT_TIME_MS before trying to get the resource
1694
+ // timing entries, and hoping that they are there by then.
1695
+ //
1696
+ // We will match that here plus an additional 50ms. If the
1697
+ // tests still fail despite this timeout, then we may have
1698
+ // found a bug that could occur in the real world, and it's
1699
+ // probably time to revisit the naïve implementation.
1700
+ //
1701
+ // This should be updated as the implementation changes.
1702
+ await waitFor ( OBSERVER_WAIT_TIME_MS + 50 ) ;
1703
+
1704
+ return response ;
1705
+ } ,
1706
+ } ) ;
1707
+
1598
1708
rootSpan = result . rootSpan ;
1599
1709
} ) ;
1600
1710
@@ -1659,7 +1769,29 @@ describe('fetch', () => {
1659
1769
beforeEach ( async ( ) => {
1660
1770
sinon . stub ( performance , 'getEntriesByType' ) . value ( undefined ) ;
1661
1771
1662
- const result = await tracedFetch ( ) ;
1772
+ const result = await tracedFetch ( {
1773
+ callback : async ( ) => {
1774
+ const response = await fetch ( '/api/status.json' ) ;
1775
+
1776
+ // Currently, the implementation works by waiting a hardcoded
1777
+ // OBSERVER_WAIT_TIME_MS before closing out the span.
1778
+ //
1779
+ // This is really meant for the case where we use the
1780
+ // `getEntriesByType` fallback, but the current implementation
1781
+ // waits for that timeout unconditionally. That is a bit silly,
1782
+ // but on the other hand, it's unclear if anyone out there is
1783
+ // actually running this in an environment where `fetch()` is
1784
+ // available but `getEntriesByType` is not.
1785
+ //
1786
+ // For now, we will match that here plus an additional 50ms.
1787
+ // If we refactor the implementation to do without the timeout
1788
+ // in this case, this can be safely deleted.
1789
+ await waitFor ( OBSERVER_WAIT_TIME_MS + 50 ) ;
1790
+
1791
+ return response ;
1792
+ } ,
1793
+ } ) ;
1794
+
1663
1795
rootSpan = result . rootSpan ;
1664
1796
} ) ;
1665
1797
0 commit comments