@v4fire/client
Version:
V4Fire client core library
433 lines (374 loc) • 11.1 kB
text/typescript
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
import type { ElementHandle, JSHandle, Page } from 'playwright';
import { expandedStringify } from 'core/prelude/test-env/components/json';
import type iBlock from 'super/i-block/i-block';
import BOM, { WaitForIdleOptions } from 'tests/helpers/bom';
import type bDummy from 'dummies/b-dummy/b-dummy';
import type { ComponentInDummy } from 'tests/helpers/component/interface';
// Temporary import until v4v4 migration
import type { VNodeDescriptor } from 'base/b-virtual-scroll-new/interface';
/**
* Class provides API to work with components on a page
*/
export default class Component {
/**
* Creates components by the passed name and scheme and mounts them into the DOM tree
*
* @param page
* @param componentName
* @param scheme
* @param [opts]
*/
static async createComponents(
page: Page,
componentName: string,
scheme: RenderParams[],
opts?: RenderOptions
): Promise<void> {
const schemeAsString = expandedStringify(scheme);
await page.evaluate(([{componentName, schemeAsString, opts}]) => {
globalThis.renderComponents(componentName, schemeAsString, opts);
}, [{componentName, schemeAsString, opts}]);
}
/**
* Creates a component by the specified name and parameters
*
* @param page
* @param componentName
* @param [scheme]
* @param [opts]
*/
static async createComponent<T extends iBlock>(
page: Page,
componentName: string,
scheme?: Partial<RenderParams>,
opts?: RenderOptions
): Promise<JSHandle<T>>;
/**
* Creates a component by the specified name and parameters
*
* @param page
* @param componentName
* @param [scheme]
* @param [opts]
*/
static async createComponent<T extends iBlock>(
page: Page,
componentName: string,
scheme: RenderParams[],
opts?: RenderOptions
): Promise<undefined>;
/**
* @param page
* @param componentName
* @param [scheme]
* @param [opts]
*/
static async createComponent<T extends iBlock>(
page: Page,
componentName: string,
scheme: Partial<RenderParams> | RenderParams[] = {},
opts?: RenderOptions
): Promise<CanUndef<JSHandle<T>>> {
if (Array.isArray(scheme)) {
await this.createComponents(page, componentName, scheme, opts);
return;
}
const
renderId = String(Math.random());
const schemeAsString = expandedStringify([
{
...scheme,
attrs: {
...scheme.attrs,
'data-render-id': renderId
}
}
]);
const normalizedOptions = {
...opts,
rootSelector: '#root-component'
};
await page.evaluate(([{componentName, schemeAsString, normalizedOptions}]) => {
globalThis.renderComponents(componentName, schemeAsString, normalizedOptions);
}, [{componentName, schemeAsString, normalizedOptions}]);
return <Promise<JSHandle<T>>>this.getComponentByQuery(page, `[data-render-id="${renderId}"]`);
}
/**
* Removes all dynamically created components
* @param page
*/
static removeCreatedComponents(page: Page): Promise<void> {
return page.evaluate(() => globalThis.removeCreatedComponents());
}
/**
* Returns a component by the specified query
*
* @param ctx
* @param selector
*/
static async getComponentByQuery<T extends iBlock>(
ctx: Page | ElementHandle,
selector: string
): Promise<CanUndef<JSHandle<T>>> {
return (await ctx.$(selector))?.getProperty('component');
}
/**
* Returns a promise that will be resolved with a component by the specified query
*
* @param ctx
* @param selector
*/
static async waitForComponentByQuery<T extends iBlock>(
ctx: Page | ElementHandle,
selector: string
): Promise<JSHandle<T>> {
return (await ctx.waitForSelector(selector, {state: 'attached'})).getProperty('component');
}
/**
* Returns a component by the specified selector
*
* @param ctx
* @param selector
*/
static async getComponents(ctx: Page | ElementHandle, selector: string): Promise<JSHandle[]> {
const
els = await ctx.$$(selector),
components = <JSHandle[]>[];
for (let i = 0; i < els.length; i++) {
components[i] = await els[i].getProperty('component');
}
return components;
}
/**
* Sets the passed props to a component by the specified selector and waits for `nextTick` after that
*
* @param page
* @param componentSelector
* @param props
* @param [opts]
*/
static async setPropsToComponent<T extends iBlock>(
page: Page,
componentSelector: string,
props: Dictionary,
opts?: WaitForIdleOptions
): Promise<JSHandle<T>> {
const ctx = await this.waitForComponentByQuery(page, componentSelector);
await ctx.evaluate(async (ctx, props) => {
for (let keys = Object.keys(props), i = 0; i < keys.length; i++) {
const
prop = keys[i],
val = props[prop];
ctx.field.set(prop, val);
}
await ctx.nextTick();
}, props);
await BOM.waitForIdleCallback(page, opts);
return this.waitForComponentByQuery(page, componentSelector);
}
/**
* Returns the root component
*
* @typeparam T - type of the root
* @param ctx
* @param [selector]
*/
static waitForRoot<T>(ctx: Page | ElementHandle, selector: string = '#root-component'): Promise<JSHandle<T>> {
const res = this.waitForComponentByQuery(ctx, selector);
return <any>res;
}
/**
* Waits until the component has the specified status and returns the component
*
* @param ctx
* @param componentSelector
* @param status
*/
static async waitForComponentStatus<T extends iBlock>(
ctx: Page | ElementHandle,
componentSelector: string,
status: string
): Promise<CanUndef<JSHandle<T>>> {
const
component = await this.waitForComponentByQuery<T>(ctx, componentSelector);
await component.evaluate((ctx, status) => new Promise<void>((res) => {
if (ctx.componentStatus === status) {
res();
}
ctx.on(`status${status.camelize(true)}`, res);
}), status);
return component;
}
/**
* Waits until a component by the passed selector switches to the specified status, then returns it
*
* @param ctx
* @param componentSelector
* @param [options]
* @deprecated
* @see [[Component.waitForComponentByQuery]]
*/
async waitForComponent<T extends iBlock>(
ctx: Page | ElementHandle,
componentSelector: string
): Promise<JSHandle<T>> {
await ctx.waitForSelector(componentSelector, {state: 'attached'});
const
component = await this.getComponentByQuery<T>(ctx, componentSelector);
if (!component) {
throw new Error('There is no component by the passed selector');
}
return component;
}
/**
* @param page
* @param componentName
* @param scheme
* @param opts
* @deprecated
* @see [[Component.createComponent]]
*/
async createComponent<T extends iBlock>(
page: Page,
componentName: string,
scheme: Partial<RenderParams> = {},
opts?: RenderOptions
): Promise<JSHandle<T>> {
return Component.createComponent(page, componentName, scheme, opts);
}
/**
* Creates a component inside the `b-dummy` component and uses the `field-like` property of `b-dummy`
* to pass props to the inner component.
*
* This function can be useful when you need to test changes to component props.
* Since component props are readonly properties, you cannot change them directly;
* changes are only available through the parent component. This is why the `b-dummy` wrapper is created,
* and the props for the component you want to render are passed as references to the property of `b-dummy`.
*
* The function returns a `handle` to the created component (not to `b-dummy`)
* and adds a method and property for convenience:
*
* - `update` - a method that allows you to modify the component's props.
*
* - `dummy` - the `handle` of the `b-dummy` component.
*
* @param page
* @param componentName
* @param params
*/
static async createComponentInDummy<T extends iBlock>(
page: Page,
componentName: string,
params: RenderComponentsVnodeParams
): Promise<ComponentInDummy<T>> {
const dummy = await this.createComponent<bDummy>(page, 'b-dummy');
const update = async (props, mixInitialProps = false) => {
await dummy.evaluate((ctx, [name, props, mixInitialProps]) => {
const parsed: RenderComponentsVnodeParams = globalThis.expandedParse(props);
ctx.testComponentAttrs = mixInitialProps ?
Object.assign(ctx.testComponentAttrs, parsed.attrs) :
parsed.attrs ?? {};
if (parsed.children) {
ctx.testComponentSlots = compileChild();
}
ctx.testComponent = name;
function compileChild() {
const
slots = Object.entries(parsed.children ?? {}),
// @ts-expect-error (misstype)
vnodes = slots.map(([slotName, child]) => ctx.unsafe.$createElement('template', {attrs: {slot: slotName}}, child ?? []));
return vnodes;
}
}, <const>[componentName, expandedStringify(props), mixInitialProps]);
};
await update(params);
const component = await dummy.evaluateHandle((ctx) => ctx.unsafe.$refs.testComponent);
Object.assign(component, {
update,
dummy
});
return <ComponentInDummy<T>>component;
}
/**
* @deprecated
* @see [[Component.waitForRoot]]
*
* @param ctx
* @param [selector]
*/
getRoot<T extends iBlock>(ctx: Page | ElementHandle, selector: string = '#root-component'): Promise<CanUndef<JSHandle<T>>> {
return Component.waitForRoot(ctx, selector);
}
/**
* @deprecated
* @see [[Component.getComponentByQuery]]
*/
async getComponentById<T extends iBlock>(
page: Page | ElementHandle,
id: string
): Promise<CanUndef<JSHandle<T>>> {
return (await page.$(`#${id}`))?.getProperty('component');
}
/**
* Returns a component by the specified query
*
* @param ctx
* @param selector
* @deprecated
* @see [[Component.getComponentByQuery]]
*/
async getComponentByQuery<T extends iBlock>(
ctx: Page | ElementHandle,
selector: string
): Promise<CanUndef<JSHandle<T>>> {
return Component.getComponentByQuery(ctx, selector);
}
/**
* @deprecated
* @see [[Component.getComponents]]
*
* @param ctx
* @param selector
*/
getComponents(ctx: Page | ElementHandle, selector: string): Promise<JSHandle[]> {
return Component.getComponents(ctx, selector);
}
/**
* @deprecated
* @see [[Component.setPropsToComponent]]
*
* @param page
* @param componentSelector
* @param props
* @param opts
*/
async setPropsToComponent<T extends iBlock>(
page: Page,
componentSelector: string,
props: Dictionary,
opts?: WaitForIdleOptions
): Promise<CanUndef<JSHandle<T>>> {
return Component.setPropsToComponent(page, componentSelector, props, opts);
}
/**
* @param ctx
* @param componentSelector
* @param status
* @deprecated
* @see [[Component.waitForComponentStatus]]
*/
async waitForComponentStatus<T extends iBlock>(
ctx: Page | ElementHandle,
componentSelector: string,
status: string
): Promise<CanUndef<JSHandle<T>>> {
return Component.waitForComponentStatus(ctx, componentSelector, status);
}
}