Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/lib/util/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,21 @@ export function onDocumentLoaded(listener: () => void): void {
}
}

export function onWindowLoaded(listener: () => void, windowInstance: Window = window): void {
if (windowInstance.document.readyState === 'complete') {
export function onWindowLoaded(listener: () => void): void {
if (window.document.readyState === 'complete') {
listener();
} else {
windowInstance.addEventListener('load', listener);
window.addEventListener('load', listener);
}
}

export function onAddEntityDialogLoaded(dialog: HTMLIFrameElement, listener: () => void): void {
// iframe could already have finished loading. We can detect this as the
// absence of the loading div.
if (qsMaybe('.content-loading', dialog.parentElement!) === null) {
listener();
} else {
dialog.addEventListener('load', () => { listener(); });
}
}

Expand Down
15 changes: 5 additions & 10 deletions src/mb_multi_external_links/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { LOGGER } from '@lib/logging/logger';
import { assertDefined } from '@lib/util/assert';
import { logFailure, retryTimes } from '@lib/util/async';
import { createPersistentCheckbox } from '@lib/util/checkboxes';
import { onWindowLoaded, qsa, qsMaybe, setInputValue } from '@lib/util/dom';
import { onAddEntityDialogLoaded, qsa, qsMaybe, setInputValue } from '@lib/util/dom';

import DEBUG_MODE from 'consts:debug-mode';
import USERSCRIPT_ID from 'consts:userscript-id';
Expand Down Expand Up @@ -160,15 +160,10 @@ async function run(windowInstance: Window): Promise<void> {

function onIframeAdded(iframe: HTMLIFrameElement): void {
LOGGER.debug(`Initialising on iframe ${iframe.src}`);
const iframeWindow = iframe.contentWindow;
if (!iframeWindow) return;

// Cannot use onDocumentLoaded even if we make it accept a custom document
// since iframe contentDocument doesn't fire the DOMContentLoaded event in
// Firefox.
onWindowLoaded(() => {
logFailure(run(iframeWindow));
}, iframeWindow);

onAddEntityDialogLoaded(iframe, () => {
logFailure(run(iframe.contentWindow!));
});
}

// Observe for additions of embedded entity creation dialogs and run the link
Expand Down
81 changes: 26 additions & 55 deletions tests/unit/lib/util/dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,61 +131,32 @@ describe('callback on window loaded', () => {
cb.mockReset();
});

function runTests(windowInstance?: Window): void {
it('does not fire if the window is not loaded', () => {
jest.spyOn(windowInstance?.document ?? document, 'readyState', 'get').mockReturnValue('interactive');
const cb = jest.fn();
onWindowLoaded(cb, windowInstance);

expect(cb).not.toHaveBeenCalled();
});

it('fires if the window was already loaded', () => {
jest.spyOn(windowInstance?.document ?? document, 'readyState', 'get').mockReturnValue('complete');
const cb = jest.fn();
onWindowLoaded(cb, windowInstance);

expect(cb).toHaveBeenCalledOnce();
});

it('fires after the window was loaded', () => {
jest.spyOn(windowInstance?.document ?? document, 'readyState', 'get').mockReturnValue('interactive');
const cb = jest.fn();
onWindowLoaded(cb, windowInstance);

expect(cb).not.toHaveBeenCalled();

(windowInstance ?? window).dispatchEvent(new Event('load'));

expect(cb).toHaveBeenCalledOnce();
});
}

describe('with main window', () => {
// eslint-disable-next-line jest/require-hook
runTests();
});

describe('with custom window', () => {
const customWindow = {
eventListener: null as (() => void) | null,
document: {
get readyState(): string {
return 'uninitialized';
},
},
addEventListener(evt: string, listener: () => void): void {
if (evt === 'load') this.eventListener = listener;
},
dispatchEvent(evt: Event): void {
if (evt.type === 'load') {
this.eventListener?.();
}
},
};

// eslint-disable-next-line jest/require-hook
runTests(customWindow as unknown as Window);
it('does not fire if the window is not loaded', () => {
jest.spyOn(document, 'readyState', 'get').mockReturnValue('interactive');
const cb = jest.fn();
onWindowLoaded(cb);

expect(cb).not.toHaveBeenCalled();
});

it('fires if the window was already loaded', () => {
jest.spyOn(document, 'readyState', 'get').mockReturnValue('complete');
const cb = jest.fn();
onWindowLoaded(cb);

expect(cb).toHaveBeenCalledOnce();
});

it('fires after the window was loaded', () => {
jest.spyOn(document, 'readyState', 'get').mockReturnValue('interactive');
const cb = jest.fn();
onWindowLoaded(cb);

expect(cb).not.toHaveBeenCalled();

window.dispatchEvent(new Event('load'));

expect(cb).toHaveBeenCalledOnce();
});
});

Expand Down