diff --git a/index.d.ts b/index.d.ts index 4b0fe9dc0f..fd364d137a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -125,6 +125,11 @@ interface RenderParams { /** @see [[RenderContent]] */ content?: Dictionary; + + /** + * List of component events to listen to + */ + events?: string[]; } /** diff --git a/src/core/prelude/test-env/components/index.ts b/src/core/prelude/test-env/components/index.ts index e32389e02c..1abc169265 100644 --- a/src/core/prelude/test-env/components/index.ts +++ b/src/core/prelude/test-env/components/index.ts @@ -10,6 +10,7 @@ import type iStaticPage from 'super/i-static-page/i-static-page'; import type { ComponentElement } from 'super/i-static-page/i-static-page'; import { expandedParse } from 'core/prelude/test-env/components/json'; +import EventStore from 'core/prelude/test-env/event-store'; globalThis.renderComponents = ( componentName: string, @@ -73,10 +74,19 @@ globalThis.renderComponents = ( const ids = scheme.map(() => Math.random()); - const vNodes = scheme.map(({attrs, content}, i) => ctx.$createElement(componentName, { + const vNodes = scheme.map(({attrs, content, events}, i) => ctx.$createElement(componentName, { attrs: { 'v-attrs': { ...attrs, + + ...events?.reduce((acc, name) => ({ + ...acc, + [name.startsWith('@') ? name : `@${name}`]: (el, ...args) => { + el.component.tmp.eventStore ??= new EventStore(); + el.component.tmp.eventStore.push({name, args}); + } + }), {}), + [idAttrs]: ids[i] } }, diff --git a/src/core/prelude/test-env/event-store/index.ts b/src/core/prelude/test-env/event-store/index.ts new file mode 100644 index 0000000000..8ee9827a63 --- /dev/null +++ b/src/core/prelude/test-env/event-store/index.ts @@ -0,0 +1,52 @@ +import type { EventStoreEntry } from 'core/prelude/test-env/event-store/interface'; + +export * from 'core/prelude/test-env/event-store/interface'; + +export default class EventStore { + events: EventStoreEntry[] = []; + updateListeners: Set = new Set(); + + push(event: EventStoreEntry): void { + this.events.push(event); + this.updateListeners.forEach((listener) => listener.call(this, event)); + } + + waitEvent(targetEvent: EventStoreEntry, timeout?: number): Promise { + const + compareEvents = (event1: EventStoreEntry, event2: EventStoreEntry): boolean => Object.fastCompare(event1, event2); + + const + hasEvent: boolean = this.events.some((event) => compareEvents(targetEvent, event)); + + if (hasEvent || timeout === 0) { + return Promise.resolve(hasEvent); + } + + return new Promise((resolve) => { + const + clearFns: Function[] = []; + + const resolveWith = (val: boolean) => { + clearFns.forEach((fn) => fn.call(this)); + resolve(val); + }; + + const listener = (event: EventStoreEntry) => { + if (compareEvents(targetEvent, event)) { + return resolveWith(true); + } + }; + + this.updateListeners.add(listener); + clearFns.push(() => this.updateListeners.delete(listener)); + + if (timeout != null) { + const timerId = setTimeout(() => { + resolveWith(false); + }, timeout); + + clearFns.push(() => clearTimeout(timerId)); + } + }); + } +} diff --git a/src/core/prelude/test-env/event-store/interface.ts b/src/core/prelude/test-env/event-store/interface.ts new file mode 100644 index 0000000000..65486342d6 --- /dev/null +++ b/src/core/prelude/test-env/event-store/interface.ts @@ -0,0 +1,7 @@ +/** + * Component event + */ +export interface EventStoreEntry { + name: string; + args: any[]; +} diff --git a/tests/helpers/component/index.ts b/tests/helpers/component/index.ts index 25533767af..e8150b3ed8 100644 --- a/tests/helpers/component/index.ts +++ b/tests/helpers/component/index.ts @@ -14,6 +14,9 @@ import type iBlock from 'super/i-block/i-block'; import BOM, { WaitForIdleOptions } from 'tests/helpers/bom'; +import type { EventStoreEntry } from 'core/prelude/test-env/event-store'; +import type EventStore from 'core/prelude/test-env/event-store'; + /** * Class provides API to work with components on a page */ @@ -237,6 +240,68 @@ export default class Component { return component; } + /** + * Returns all events emitted by the component that were listened to + * + * @param ctx + * @param componentSelector + */ + static async getComponentEmittedEvents(ctx: Page | ElementHandle, componentSelector: string): Promise { + const + component = await this.waitForComponentByQuery(ctx, componentSelector); + + return component.evaluate( + (ctx) => ctx.unsafe.tmp.eventStore.events + ); + } + + /** + * Waits until the component emits specified event + * + * @param ctx + * @param componentSelector + * @param eventName + * @param [eventArgs] + */ + static async waitForComponentEvent( + ctx: Page | ElementHandle, + componentSelector: string, + eventName: string, + ...eventArgs: any[] + ): Promise; + + /** + * Waits until the component emits specified event + * + * @param ctx + * @param componentSelector + * @param event + * @param [opts] + */ + static async waitForComponentEvent( + ctx: Page | ElementHandle, + componentSelector: string, + event: EventStoreEntry, + opts?: {timeout?: number} + ): Promise; + + static async waitForComponentEvent( + ctx: Page | ElementHandle, + componentSelector: string, + ...args: any[] + ): Promise { + const + [event, opts] = Object.isString(args[0]) ? [{name: args[0], args: args.slice(1)}, {}] : args, + component = await this.waitForComponentByQuery(ctx, componentSelector); + + return component.evaluate( + (ctx, {event, opts}) => Boolean( + (<{eventStore: EventStore}>ctx.unsafe.tmp).eventStore?.waitEvent(event, opts?.timeout) + ), + {event, opts} + ); + } + /** * Waits until a component by the passed selector switches to the specified status, then returns it *