UNPKG

pptr-testing-library

Version:
277 lines (229 loc) 8.07 kB
import {readFileSync} from 'fs' import * as path from 'path' import {ElementHandle, Frame, JSHandle, Page} from 'puppeteer' import waitForExpect from 'wait-for-expect' import {IConfigureOptions, IQueryUtils, IScopedQueryUtils} from './typedefs' const domLibraryAsString = readFileSync( path.join(__dirname, '../dom-testing-library.js'), 'utf8', ).replace(/process.env/g, '{}') /* istanbul ignore next */ function mapArgument(o: any): any { return convertProxyToRegExp(o, 0) } /* istanbul ignore next */ function convertProxyToRegExp(o: any, depth: number): any { if (typeof o !== 'object' || !o || depth > 2) return o if (!o.__regex || typeof o.__flags !== 'string') { const copy = {...o} for (const key of Object.keys(copy)) { copy[key] = convertProxyToRegExp(copy[key], depth + 1) } return copy } return new RegExp(o.__regex, o.__flags) } function convertRegExpToProxy(o: any, depth: number): any { if (typeof o !== 'object' || !o || depth > 2) return o // Support the cross-environment use case where `instanceof` is not sufficient, see https://github.com/testing-library/pptr-testing-library/pull/64 if (Object.prototype.toString.call(o) !== '[object RegExp]') { const copy = {...o} for (const key of Object.keys(copy)) { copy[key] = convertRegExpToProxy(copy[key], depth + 1) } return copy } return {__regex: o.source, __flags: o.flags} } function getExecutionContextFromHandle( elementHandle: ElementHandle, ): Pick<Frame, 'evaluate' | 'evaluateHandle'> { if (!elementHandle.frame) { // @ts-ignore - Support versions of puppeteer before v17. return elementHandle.executionContext() } return elementHandle.frame } const delegateFnBodyToExecuteInPageInitial = ` ${domLibraryAsString}; ${convertProxyToRegExp.toString()}; const mappedArgs = args.map(${mapArgument.toString()}); const moduleWithFns = fnName in __dom_testing_library__ ? __dom_testing_library__ : __dom_testing_library__.__moduleExports; return moduleWithFns[fnName](container, ...mappedArgs); ` let delegateFnBodyToExecuteInPage = delegateFnBodyToExecuteInPageInitial type DOMReturnType = ElementHandle<Node> | Array<ElementHandle<Node>> | null type ContextFn = (...args: any[]) => ElementHandle async function createElementHandleArray(handle: JSHandle): Promise<Array<ElementHandle<Node>>> { const lengthHandle = await handle.getProperty('length') if (!lengthHandle) throw new Error(`Failed to assess length property`) const length = (await lengthHandle.jsonValue()) as number const elements: Array<ElementHandle<Node>> = [] for (let i = 0; i < length; i++) { const jsElement = await handle.getProperty(i.toString()) if (!jsElement) throw new Error(`Failed to assess ${i.toString()} property`) const element = await createElementHandle(jsElement) if (element) elements.push(element) } return elements } async function createElementHandle(handle: JSHandle): Promise<ElementHandle<Node> | null> { const element = handle.asElement() if (element) return element await handle.dispose() return null // tslint:disable-line } async function covertToElementHandle(handle: JSHandle, asArray: boolean): Promise<DOMReturnType> { return asArray ? createElementHandleArray(handle) : createElementHandle(handle) } function processNodeText(handles: IHandleSet): Promise<string> { return getExecutionContextFromHandle(handles.containerHandle).evaluate( handles.evaluateFn, handles.containerHandle, 'getNodeText', ) } async function processQuery(handles: IHandleSet): Promise<DOMReturnType> { const {containerHandle, evaluateFn, fnName, argsToForward} = handles try { const handle = await getExecutionContextFromHandle(containerHandle).evaluateHandle( evaluateFn, containerHandle, fnName, ...argsToForward, ) return await covertToElementHandle(handle, fnName.includes('All')) } catch (err) { if (typeof err !== 'object' || !err || !(err instanceof Error)) throw err err.message = err.message.replace('[fnName]', `[${fnName}]`) err.stack = (err.stack || '').replace('[fnName]', `[${fnName}]`) throw err } } interface IHandleSet { containerHandle: ElementHandle fnName: string argsToForward: any[] evaluateFn(...params: any[]): any } function createDelegateFor<T = DOMReturnType>( fnName: keyof IQueryUtils, contextFn?: ContextFn, processHandleFn?: (handles: IHandleSet) => Promise<T>, ): (...args: any[]) => Promise<T> { // @ts-ignore processHandleFn = processHandleFn || processQuery return async function(...args: any[]): Promise<T> { // @ts-ignore const containerHandle: ElementHandle = contextFn ? contextFn.apply(this, args) : this // @ts-ignore const evaluateFn: EvaluateFn = new Function( 'container, fnName, ...args', delegateFnBodyToExecuteInPage, ) let argsToForward = args // Remove the container from the argsToForward since it's always the first argument if (containerHandle === args[0]) { argsToForward = argsToForward.slice(1) } // Convert RegExp to a special format since they don't serialize well argsToForward = argsToForward.map(convertRegExpToProxy) return processHandleFn!({fnName, containerHandle, evaluateFn, argsToForward}) } } export async function getDocument(_page?: Page): Promise<ElementHandle<Element>> { // @ts-ignore const page: Page = _page || this const documentHandle = await page.mainFrame().evaluateHandle('document') const document = documentHandle.asElement() if (!document) throw new Error('Could not find document') return document as ElementHandle<Element> } export function wait( callback: () => void, {timeout = 4500, interval = 50}: {timeout?: number; interval?: number} = {}, ): Promise<{}> { return waitForExpect(callback, timeout, interval) } export const waitFor = wait export function configure(options: Partial<IConfigureOptions>): void { if (!options) { return } const {testIdAttribute} = options if (testIdAttribute) { delegateFnBodyToExecuteInPage = delegateFnBodyToExecuteInPageInitial.replace( /testIdAttribute: (['|"])data-testid(['|"])/g, `testIdAttribute: $1${testIdAttribute}$2`, ) } } export function getQueriesForElement<T>( object: T, contextFn?: ContextFn, ): T & IQueryUtils & IScopedQueryUtils { const o = object as any if (!contextFn) contextFn = () => o const functionNames: Array<keyof IQueryUtils> = [ 'queryByPlaceholderText', 'queryAllByPlaceholderText', 'getByPlaceholderText', 'getAllByPlaceholderText', 'findByPlaceholderText', 'findAllByPlaceholderText', 'queryByText', 'queryAllByText', 'getByText', 'getAllByText', 'findByText', 'findAllByText', 'queryByLabelText', 'queryAllByLabelText', 'getByLabelText', 'getAllByLabelText', 'findByLabelText', 'findAllByLabelText', 'queryByAltText', 'queryAllByAltText', 'getByAltText', 'getAllByAltText', 'findByAltText', 'findAllByAltText', 'queryByTestId', 'queryAllByTestId', 'getByTestId', 'getAllByTestId', 'findByTestId', 'findAllByTestId', 'queryByTitle', 'queryAllByTitle', 'getByTitle', 'getAllByTitle', 'findByTitle', 'findAllByTitle', 'queryByRole', 'queryAllByRole', 'getByRole', 'getAllByRole', 'findByRole', 'findAllByRole', 'queryByDisplayValue', 'queryAllByDisplayValue', 'getByDisplayValue', 'getAllByDisplayValue', 'findByDisplayValue', 'findAllByDisplayValue', ] functionNames.forEach(functionName => { o[functionName] = createDelegateFor(functionName, contextFn) }) o.getQueriesForElement = () => getQueriesForElement(o, () => o) o.getNodeText = createDelegateFor<string>('getNodeText', contextFn, processNodeText) return o } export const within = getQueriesForElement // @ts-ignore export const queries: IQueryUtils = {} getQueriesForElement(queries, el => el)