Skip to content

Commit

Permalink
FI-494: add selectors with custom methods
Browse files Browse the repository at this point in the history
  • Loading branch information
nnn3d committed Nov 15, 2023
1 parent 34034da commit d9b3975
Show file tree
Hide file tree
Showing 20 changed files with 478 additions and 150 deletions.
12 changes: 12 additions & 0 deletions autotests/pageObjects/pages/E2edReportExample.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {setPageCookies} from 'autotests/context';
import {E2edReportExample as E2edReportExampleRoute} from 'autotests/routes/pageRoutes';
import {Page} from 'e2ed';
import {locatorIdSelector} from 'e2ed/selectors';
import {setReadonlyProperty} from 'e2ed/utils';

import type {Cookie} from 'e2ed/types';
Expand Down Expand Up @@ -28,6 +29,17 @@ export class E2edReportExample extends Page<CustomPageParams> {
return new E2edReportExampleRoute();
}

readonly navigationRetries = locatorIdSelector('app-navigation-retries');

readonly navigationRetriesButton = this.navigationRetries.findByTestId(
'app-navigation-retries-button',
);

readonly navigationRetriesButtonSelected = this.navigationRetriesButton.filterByTestProp(
'selected',
'true',
);

/**
* Set page cookies to context before navigate.
*/
Expand Down
34 changes: 34 additions & 0 deletions autotests/tests/e2edReportExample/customSelectorMethods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {it} from 'autotests';
import {E2edReportExample} from 'autotests/pageObjects/pages';
import {expect} from 'e2ed';
import {navigateToPage} from 'e2ed/actions';

it('custom selector methods', {meta: {testId: '6'}, testIdleTimeout: 35_000}, async () => {
const reportPage = await navigateToPage(E2edReportExample);

await expect(reportPage.navigationRetries.exists, 'navigation retries exists').ok();

await expect(reportPage.navigationRetriesButton.exists, ' exists').ok();

await expect(
reportPage.navigationRetriesButtonSelected.exists,
'selected navigation retries button exists',
).ok();

const buttonsCount = await reportPage.navigationRetriesButton.count;

await expect(
reportPage.navigationRetriesButtonSelected.getTestProp('retry'),
'last navigation retries button selected',
).eql(String(buttonsCount));

await expect(
reportPage.navigationRetriesButtonSelected.hasTestProp('disabled'),
'selected navigation retries button has "disabled" test prop',
).ok();

await expect(
reportPage.navigationRetriesButtonSelected.getDescription(),
'selector has apropriate description',
).eql('');
});
63 changes: 63 additions & 0 deletions autotests/tests/internalTypeTests/selectors.skip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
createSelector,
createSelectorByCss,
htmlElementSelector,
locatorIdSelector,
} from 'e2ed/selectors';

// @ts-expect-error: wrong number of arguments
htmlElementSelector.findByTestId();
// @ts-expect-error: wrong type of arguments
htmlElementSelector.findByTestId(0);
// ok
htmlElementSelector.findByTestId('id');
// ok
htmlElementSelector.findByTestId('id').findByTestId('id2');
// ok
htmlElementSelector.findByTestId('id').find('.test-children');
// ok
htmlElementSelector.find('body').findByTestId('id');

// ok
createSelector('id').findByTestId('id').find('body').findByTestId('id');
// ok
createSelectorByCss('id').findByTestId('id').find('body').findByTestId('id');
// ok
locatorIdSelector('id').findByTestId('id').find('body').findByTestId('id');

// ok
htmlElementSelector.filterByTestId('id');
// ok
htmlElementSelector.parentByTestId('id');
// ok
htmlElementSelector.childByTestId('id');
// ok
htmlElementSelector.siblingByTestId('id');
// ok
htmlElementSelector.nextSiblingByTestId('id');
// ok
htmlElementSelector.prevSiblingByTestId('id');

// ok
htmlElementSelector.filterByTestProp('prop', 'value');
// ok
htmlElementSelector.findByTestProp('prop', 'value');
// ok
htmlElementSelector.parentByTestProp('prop', 'value');
// ok
htmlElementSelector.childByTestProp('prop', 'value');
// ok
htmlElementSelector.siblingByTestProp('prop', 'value');
// ok
htmlElementSelector.nextSiblingByTestProp('prop', 'value');
// ok
htmlElementSelector.prevSiblingByTestProp('prop', 'value');

// ok
void htmlElementSelector.getTestProp('prop');
// ok
void htmlElementSelector.hasTestProp('prop');

// ok
declare function testStringOrUndefined(val: string | undefined): void;
testStringOrUndefined(htmlElementSelector.getDescription());
70 changes: 0 additions & 70 deletions src/createSelector.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
* Base public modules.
*/
export {ApiRoute} from './ApiRoute';
export {createSelector} from './createSelector';
export {Page} from './Page';
export {PageRoute} from './PageRoute';
export {Route} from './Route';
export {createSelector} from './selectors';
export {testController} from './testController';
export {useContext} from './useContext';

Expand Down
91 changes: 91 additions & 0 deletions src/selectors/createSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {Selector} from 'testcafe-without-typecheck';

import {DESCRIPTION_KEY} from '../constants/internal';

import type {Fn, RawSelector, SelectorCustomMethods, Values} from '../types/internal';

/**
* Proxy handler for wrapping all selector properties.
*/
const createGet = <CustomMethods extends SelectorCustomMethods = {}>(
customMethods?: CustomMethods,
): Required<ProxyHandler<RawSelector<CustomMethods>>>['get'] => {
const get: Required<ProxyHandler<RawSelector<CustomMethods>>>['get'] = (
target,
property,
receiver,
) => {
const customMethod =
customMethods && typeof property === 'string' ? customMethods[property] : undefined;

let result = (
customMethod ? customMethod.bind(target) : Reflect.get(target, property, receiver)
) as Values<RawSelector<CustomMethods>> & {
[DESCRIPTION_KEY]?: string;
};

if (typeof property === 'symbol') {
return result;
}

if ((typeof result !== 'object' || result === null) && typeof result !== 'function') {
return result;
}

if (typeof result === 'function') {
const originalFunction = result as Fn;

result = // eslint-disable-next-line no-restricted-syntax
function selectorMethodWrapper(this: RawSelector, ...args: never[]) {
const callResult = originalFunction.apply(this, args);

if (
(typeof callResult !== 'object' || callResult === null) &&
typeof callResult !== 'function'
) {
return callResult;
}

const callLocator = `${result[DESCRIPTION_KEY]}(${args.join(', ')})`;

(callResult as typeof result)[DESCRIPTION_KEY] = callLocator;

// callResult is Selector
if (Object.prototype.hasOwnProperty.call(callResult, 'getBoundingClientRectProperty')) {
return new Proxy(callResult, {get});
}

return callResult;
} as typeof result;
}

const locator = target[DESCRIPTION_KEY] || '';

result[DESCRIPTION_KEY] = `${locator}.${property}`;

return result;
};

return get;
};

export type CreateSelector<CustomMethods extends SelectorCustomMethods = {}> = (
...args: Parameters<typeof Selector>
) => RawSelector<CustomMethods>;

export const createSelectorCreator = <CustomMethods extends SelectorCustomMethods = {}>(
customMethods?: CustomMethods,
): CreateSelector<CustomMethods> => {
const createSelector: CreateSelector<CustomMethods> = (...args) => {
const locator = args[0];
const selector = Selector(...args) as RawSelector<CustomMethods>;

if (typeof locator === 'string') {
selector[DESCRIPTION_KEY] = locator;
}

return new Proxy(selector, {get: createGet(customMethods)});
};

return createSelector;
};
20 changes: 13 additions & 7 deletions src/selectors/createSelectorByCss.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import {createSelector} from '../createSelector';
import type {RawSelector, SelectorCustomMethods} from '../types/internal';

import type {Selector} from '../types/internal';
import type {CreateSelector} from './createSelector';

/**
* Creates selector of page elements by CSS selector.
*/
export const createSelectorByCss = (cssSelectorString: string): Selector =>
createSelector(cssSelectorString);
export const createSelectorByCssCreator = <CustomMethods extends SelectorCustomMethods = {}>(
createSelector: CreateSelector<CustomMethods>,
): typeof createSelectorByCss => {
/**
* Creates selector of page elements by CSS selector.
*/
const createSelectorByCss = (cssSelectorString: string): RawSelector<CustomMethods> =>
createSelector(cssSelectorString);

return createSelectorByCss;
};
44 changes: 44 additions & 0 deletions src/selectors/createSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {createSelectorCreator} from './createSelector';
import {createSelectorByCssCreator} from './createSelectorByCss';
import {createDefaultCustomMethods} from './defaultCustomMethods';
import {htmlElementSelectorCreator} from './htmlElementSelector';
import {locatorIdSelectorCreator} from './locatorIdSelector';

import type {
CreateSelectorsOptions,
GetTestAttrNameFn,
SelectorCustomMethods,
} from '../types/internal';

const createSelectorsWithCustomMethods = <CustomMethods extends SelectorCustomMethods = {}>(
getTestAttrName: GetTestAttrNameFn,
// force `this` to be Selector
customMethods?: CustomMethods,
): typeof selectorsWithCustomMethods => {
const createSelector = createSelectorCreator(customMethods);

const selectorsWithCustomMethods = {
/**
* Creates selector by locator and optional parameters.
*/
createSelector,
/**
* Creates selector of page elements by CSS selector.
*/
createSelectorByCss: createSelectorByCssCreator(createSelector),
/**
* Selector of page HTML element ("documentElement").
*/
htmlElementSelector: htmlElementSelectorCreator(createSelector),
/**
* Selector of locator elements by locator id.
*/
locatorIdSelector: locatorIdSelectorCreator(createSelector, getTestAttrName),
};

return selectorsWithCustomMethods;
};

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const createSelectors = ({getTestAttrName}: CreateSelectorsOptions) =>
createSelectorsWithCustomMethods(getTestAttrName, createDefaultCustomMethods(getTestAttrName));
Loading

0 comments on commit d9b3975

Please sign in to comment.