Created
June 1, 2018 03:10
-
-
Save JoshuaKGoldberg/a0c5fe2812a06318e0eb2ffdc9e17021 to your computer and use it in GitHub Desktop.
Sample of event listening with selenium-webdriver and squee
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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