Skip to content

Instantly share code, notes, and snippets.

@JoshuaKGoldberg
Created June 1, 2018 03:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshuaKGoldberg/a0c5fe2812a06318e0eb2ffdc9e17021 to your computer and use it in GitHub Desktop.
Save JoshuaKGoldberg/a0c5fe2812a06318e0eb2ffdc9e17021 to your computer and use it in GitHub Desktop.
Sample of event listening with selenium-webdriver and squee
import { createEventEmitter, IEventReceiver } from "squee";
import * as Selenium from "selenium-webdriver";
// Put whatever you'd like in this...
interface IPostMessageData {
identifier: string;
}
declare const window: {
__MY_IS_EVENT_DATA_VALID__: (data: any) => data is IPostMessageData;
__MY_PENDING_EVENTS__?: IPostMessageData[];
__MY_POST_MESSAGE_LISTENER: (event: { data: any }) => void;
addEventListener(eventType: "message", callback: (event: { data: any }) => void): void;
removeEventListener(eventType: "message", callback: (event: { data: any }) => void): void;
};
export interface ITestEventEmissions {
/**
* Promise for when events are stopped.
*
* @remarks This will never resolve if stopEvents is never called.
*/
continuousResults: Promise<void>;
/**
* Receives re-emitted events from the page.
*/
eventReceiver: IEventReceiver;
/**
* Stops events from being re-emitted.
*
* @returns Promise for removing added event hooks from the page.
*/
stopEvents: () => Promise<void>;
}
export const setupTestEventEmitter = async (browser: Selenium.WebDriver) => {
// This eventEmitter will re-emit transmitted events from the page
const eventEmitter = createEventEmitter();
// Start a postMessage event listener to continuously feed events to the global array
await browser.executeScript((): void => {
// We'll listen to posted messages for our form of emitted event
window.__MY_IS_EVENT_DATA_VALID__ = (data: any): data is IPostMessageData => {
return typeof data === "object" && data.identifier !== undefined;
};
// Keep a global array of pending event data to be sent
// This will be set to undefined if events are stopped
window.__MY_PENDING_EVENTS__ = [];
// Whenever an event is emitted with our data type, we add it to the pending event data list
window.__MY_POST_MESSAGE_LISTENER = ({ data }) => {
if (window.__MY_IS_EVENT_DATA_VALID__(data) && window.__MY_PENDING_EVENTS__ !== undefined) {
window.__MY_PENDING_EVENTS__.push(data);
}
};
window.addEventListener("message", window.__MY_POST_MESSAGE_LISTENER);
});
// If we're told to stop events, we'll remove the added hooks from the page
let enabled: boolean = true;
const stopEvents = async () => {
if (!enabled) {
throw new Error("Cannot stop events twice.");
}
enabled = false;
// This removes the global event listener and pending event data list to prevent memory leaks
await browser.executeScript(() => {
window.removeEventListener("message", window.__MY_POST_MESSAGE_LISTENER);
delete window.__MY_IS_EVENT_DATA_VALID__;
delete window.__MY_PENDING_EVENTS__;
delete window.__MY_POST_MESSAGE_LISTENER;
});
};
/**
* Starts continuously listening to page events.
*
* @returns Promise for when events are stopped.
* @remarks This will never resolve if stopEvents is never called.
*/
const continuouslyRetrievePendingResults = async (): Promise<void> => {
// Stop trying for events if we've been told to stop
if (!enabled) {
return;
}
// Retrieve the next batch of emitted event data
let results: IPostMessageData[];
try {
results = await browser.executeAsyncScript<IPostMessageData[]>(
(onResults: (...results: IPostMessageData[]) => void): void => {
// When we're ready to send new results, we clear the pending event data list and send it
const sendAndClearResults = (pendingEvents: IPostMessageData[]) => {
const results = pendingEvents.slice();
pendingEvents.length = 0;
onResults(...results);
};
// If we've separately been told to stop, the pending event data list will no longer exist
if (window.__MY_PENDING_EVENTS__ === undefined) {
return;
}
// If there are already events waiting to be collected, send those immediately
if (window.__MY_PENDING_EVENTS__.length !== 0) {
sendAndClearResults(window.__MY_PENDING_EVENTS__);
return;
}
// Once we see a new message, if it's our data type, we know to send our pending event(s)
const onMessage = ({ data }: any) => {
if (!window.__MY_IS_EVENT_DATA_VALID__(data)) {
return;
}
window.removeEventListener("message", onMessage);
// If we've separately been told to stop, the pending event data list will no longer exist
if (window.__MY_PENDING_EVENTS__ !== undefined) {
sendAndClearResults(window.__MY_PENDING_EVENTS__);
}
};
window.addEventListener("message", onMessage);
});
} catch (error) {
// The script will error from timing out if no events are posted
return continuouslyRetrievePendingResults();
}
// We might have been told to stop before the script finished
if (!enabled) {
return;
}
// Re-distribute each event data through our own event emitter
for (const { eventType, args } of results) {
eventEmitter.emit(eventType, ...args);
if (!enabled) {
return;
}
}
return continuouslyRetrievePendingResults();
}
return {
continuousResults: continuouslyRetrievePendingResults(),
eventReceiver: eventEmitter,
stopEvents,
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment