From cd2852fdcc58c4459da8b9cc9d5c80747e5cca81 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 8 Apr 2022 12:12:32 -0400 Subject: [PATCH] test(playwright): add event spies (#25080) --- core/src/utils/test/playwright/eventSpy.ts | 100 ++++++++++++++++++ .../{playwright.ts => playwright/fixtures.ts} | 28 ++++- core/src/utils/test/playwright/index.ts | 2 + 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 core/src/utils/test/playwright/eventSpy.ts rename core/src/utils/test/{playwright.ts => playwright/fixtures.ts} (84%) create mode 100644 core/src/utils/test/playwright/index.ts diff --git a/core/src/utils/test/playwright/eventSpy.ts b/core/src/utils/test/playwright/eventSpy.ts new file mode 100644 index 0000000000..6e10808cf1 --- /dev/null +++ b/core/src/utils/test/playwright/eventSpy.ts @@ -0,0 +1,100 @@ +import type { IonicPage } from './fixtures'; + +/** + * The EventSpy class allows + * developers to listen for + * a particular event emission and + * pass/fail the test based on whether + * or not the event was emitted. + * Based off https://github.com/ionic-team/stencil/blob/16b8ea4dabb22024872a38bc58ba1dcf1c7cc25b/src/testing/puppeteer/puppeteer-events.ts#L64 + */ +export class EventSpy { + /** + * Keeping track of a cursor + * ensures that no two spy.next() + * calls point to the same event. + */ + private cursor = 0; + private queuedHandler: (() => void)[] = []; + public events: Event[] = []; + + constructor(public eventName: string) {} + + get length() { + return this.events.length; + } + + public next() { + const { cursor } = this; + this.cursor++; + + const next = this.events[cursor]; + if (next) { + return Promise.resolve(next); + } else { + /** + * If the event has not already been + * emitted, then add it to the queuedHandler. + * When the event is emitted, the push + * method is called which results in the + * Promise below being resolved. + */ + let resolve: () => void; + const promise = new Promise((r) => (resolve = r)); + // @ts-ignore + this.queuedHandler.push(resolve); + + return promise.then(() => this.events[cursor]); + } + } + + public push(ev: Event) { + this.events.push(ev); + const next = this.queuedHandler.shift(); + if (next) { + next(); + } + } +} + +/** + * Initializes information required to + * spy on events. + * The ionicOnEvent function is called in the + * context of the current page. This lets us + * respond to an event listener created within + * the page itself. + */ +export const initPageEvents = async (page: IonicPage) => { + page._e2eEventsIds = 0; + page._e2eEvents = new Map(); + + await page.exposeFunction('ionicOnEvent', (id: number, ev: any) => { + const context = page._e2eEvents.get(id); + if (context) { + context.callback(ev); + } + }); +}; + +/** + * Adds a new event listener in the current + * page context to updates the _e2eEvents map + * when an event is fired. + */ +export const addE2EListener = async (page: IonicPage, eventName: string, callback: (ev: any) => void) => { + const id = page._e2eEventsIds++; + page._e2eEvents.set(id, { + eventName, + callback, + }); + + await page.evaluate( + ([eventName, id]) => { + window.addEventListener(eventName as string, (ev: Event) => { + (window as any).ionicOnEvent(id, ev); + }); + }, + [eventName, id] + ); +}; diff --git a/core/src/utils/test/playwright.ts b/core/src/utils/test/playwright/fixtures.ts similarity index 84% rename from core/src/utils/test/playwright.ts rename to core/src/utils/test/playwright/fixtures.ts index f8bec9e6ac..8af41e90b0 100644 --- a/core/src/utils/test/playwright.ts +++ b/core/src/utils/test/playwright/fixtures.ts @@ -9,10 +9,15 @@ import type { } from '@playwright/test'; import { test as base } from '@playwright/test'; -type IonicPage = Page & { +import { EventSpy, initPageEvents, addE2EListener } from './eventSpy'; + +export type IonicPage = Page & { goto: (url: string) => Promise; setIonViewport: () => Promise; getSnapshotSettings: () => string; + spyOnEvent: (eventName: string) => Promise; + _e2eEventsIds: number; + _e2eEvents: Map; }; type CustomTestArgs = PlaywrightTestArgs & @@ -124,6 +129,27 @@ export const test = base.extend({ }); }; + /** + * Creates a new EventSpy and listens + * on the window for an event. + * The test will timeout if the event + * never fires. + * + * Usage: + * const ionChange = await page.spyOnEvent('ionChange'); + * ... + * await ionChange.next(); + */ + page.spyOnEvent = async (eventName: string): Promise => { + const spy = new EventSpy(eventName); + + await addE2EListener(page, eventName, (ev: Event) => spy.push(ev)); + + return spy; + }; + + initPageEvents(page); + await use(page); }, }); diff --git a/core/src/utils/test/playwright/index.ts b/core/src/utils/test/playwright/index.ts new file mode 100644 index 0000000000..90dcacbd8b --- /dev/null +++ b/core/src/utils/test/playwright/index.ts @@ -0,0 +1,2 @@ +export * from './fixtures'; +export * from './eventSpy';