Skip to content

Commit f7ab8ac

Browse files
committed
Fix issues that could cause unexpected onDocumentReady firing
Fix various issues that could cause the callback to `onDocumentReady` to fire after the subscription had been canceled. - `pollOnUnload` was called twice for the initial document, once at the top level of the function and once by the first call to `checkForDocumentChange`. This resulted into "unload" listeners being added for the same window. - The initial async `checkForDocumentChange` call would still fire if the subscription was immediately canceled - If the current window's "unload" event fired after the subscription was canceled, polling would be restarted, effectively re-subscribing to document changes.
1 parent 5fff6ee commit f7ab8ac

File tree

2 files changed

+50
-4
lines changed

2 files changed

+50
-4
lines changed

src/annotator/frame-observer.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,12 @@ export function onDocumentReady(frame, callback, { pollInterval = 10 } = {}) {
215215
pollOnUnload();
216216
};
217217

218+
let canceled = false;
218219
pollForDocumentChange = () => {
219220
cancelPoll();
220-
pollTimer = setInterval(checkForDocumentChange, pollInterval);
221+
if (!canceled) {
222+
pollTimer = setInterval(checkForDocumentChange, pollInterval);
223+
}
221224
};
222225

223226
// Set up observers for signals that the document either has changed or will
@@ -226,16 +229,20 @@ export function onDocumentReady(frame, callback, { pollInterval = 10 } = {}) {
226229
// - Polling after the current document is about to be unloaded. This allows
227230
// us to detect the new document quickly, but may not fire in some
228231
// situations (exact circumstances unclear, but eg. MDN warns about this).
232+
//
233+
// This is set up in the first call to `checkForDocumentChange`.
234+
//
229235
// - The iframe's "load" event. This is guaranteed to fire but only after the
230236
// new document is fully loaded.
231-
pollOnUnload();
232237
frame.addEventListener('load', checkForDocumentChange);
233238

234239
// Notify caller about the current document. This fires asynchronously so that
235240
// the caller will have received the unsubscribe callback first.
236-
setTimeout(() => checkForDocumentChange(), 0);
241+
const initialCheckTimer = setTimeout(checkForDocumentChange, 0);
237242

238243
return () => {
244+
canceled = true;
245+
clearTimeout(initialCheckTimer);
239246
cancelPoll();
240247
frame.removeEventListener('load', checkForDocumentChange);
241248
};

src/annotator/test/frame-observer-test.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ describe('annotator/frame-observer', () => {
228228
assert.equal(error.message, 'Frame is cross-origin');
229229
});
230230

231-
it('returns a callback that stops polling', async () => {
231+
it('stops polling when subscription is canceled', async () => {
232232
const callback = sinon.stub();
233233
const frame = createFrame(sameOriginURL);
234234
await waitForEvent(frame, 'load');
@@ -242,6 +242,45 @@ describe('annotator/frame-observer', () => {
242242

243243
assert.calledOnce(callback);
244244
});
245+
246+
it('does not start polling if "unload" event is received after subscription is canceled', async () => {
247+
const clock = sinon.useFakeTimers();
248+
try {
249+
const callback = sinon.stub();
250+
const frame = createFrame(sameOriginURL);
251+
await waitForEvent(frame, 'load');
252+
253+
const unsubscribe = onDocumentReady(frame, callback);
254+
clock.tick();
255+
assert.calledOnce(callback);
256+
257+
const contentWindow = frame.contentWindow;
258+
unsubscribe();
259+
contentWindow.dispatchEvent(new Event('unload'));
260+
261+
frame.src = sameOriginURL + '?v2';
262+
await waitForEvent(frame, 'load');
263+
clock.tick(50); // Wait for any active polling to trigger
264+
265+
assert.calledOnce(callback);
266+
} finally {
267+
clock.restore();
268+
}
269+
});
270+
271+
it('does not invoke callback if subscription is immediately canceled', async () => {
272+
const callback = sinon.stub();
273+
const frame = createFrame(sameOriginURL);
274+
await waitForEvent(frame, 'load');
275+
276+
const unsubscribe = onDocumentReady(frame, callback);
277+
unsubscribe();
278+
279+
frame.src = sameOriginURL + '?v2';
280+
await waitForEvent(frame, 'load');
281+
282+
assert.notCalled(callback);
283+
});
245284
});
246285

247286
describe('onNextDocumentReady', () => {

0 commit comments

Comments
 (0)