UNPKG

create-locator

Version:

Creates HTML element locators for tests 📌

262 lines (261 loc) • 8.5 kB
'use strict'; exports.anyLocator = undefined; exports.createLocator = undefined; exports.createRootLocator = undefined; exports.getLocatorParameters = undefined; exports.removeMarkFromProperties = undefined; exports.setGlobalProductionMode = undefined; const { anyLocator: productionAnyLocator } = require('./production.cjs'); /** * Proxy object that represents the locator in production mode (and, for example, in unit tests). */ const anyLocator = exports.anyLocator = productionAnyLocator; /** * Creates component locator (by component properties). */ const createLocator = exports.createLocator = ((properties) => { if (isGlobalProductionMode) { return anyLocator; } const pathAttributeValue = getPathAttributeValueFromProperties(properties); if (!pathAttributeValue) { return anyLocator; } return pathAttributeValue[LOCATOR]; }); /** * Creates root locator (by prefix and options). */ const createRootLocator = exports.createRootLocator = ((prefix, maybeOptions) => { if (isGlobalProductionMode) { return anyLocator; } const options = Object.assign({}, DEFAULT_OPTIONS, maybeOptions); if (options.isProduction) { return anyLocator; } return createLocatorProxy(options, prefix); }); /** * Get component parameters of locator from parent component (by component properties). */ const getLocatorParameters = exports.getLocatorParameters = ((properties) => { if (isGlobalProductionMode) { return anyLocator; } const pathAttributeValue = getPathAttributeValueFromProperties(properties); if (!pathAttributeValue) { return anyLocator; } if (pathAttributeValue.parameters === undefined) { return undefined; } return pathAttributeValue.parameters || anyLocator; }); /** * Removes locator mark from properties (or rest properties) object. * Returns properties without locator mark. */ const removeMarkFromProperties = exports.removeMarkFromProperties = ((properties) => { if (isGlobalProductionMode) { return properties; } const pathAttributeValue = getPathAttributeValueFromProperties(properties); if (!pathAttributeValue) { return properties; } const { [LOCATOR]: locator, parameters } = pathAttributeValue; const { parameterAttributePrefix, pathAttribute } = locator[OPTIONS]; const attributes = { __proto__: null, [pathAttribute]: '' }; const propertiesWithoutLocator = { __proto__: Object.getPrototypeOf(properties) }; setAttributesFromParameters(attributes, parameterAttributePrefix, parameters); for (const key of Reflect.ownKeys(properties)) { if (!(key in attributes)) { Object.defineProperty(propertiesWithoutLocator, key, Object.getOwnPropertyDescriptor(properties, key)); } } return propertiesWithoutLocator; }); /** * Set production mode for all locators at all. */ const setGlobalProductionMode = exports.setGlobalProductionMode = () => { isGlobalProductionMode = true; }; /** * If `true`, then all locators work in production mode. */ var isGlobalProductionMode = false; /** * Adds before the string its length. */ const addLength = (value) => { const str = String(value); return `${str.length}:${str}`; }; /** * create-locator 📌 package url on npm website. */ const packageUrl = 'https://www.npmjs.com/package/create-locator'; /** * Symbol key for cache of locator attributes. */ const ATTRIBUTES = Symbol.for(`${packageUrl}#attributes`); /** * Symbol key for cache of locator call results. */ const CACHE = Symbol.for(`${packageUrl}#cache`); /** * Creates a proxy object that represents the locator at runtime, by root options and path. */ const createLocatorProxy = (options, path, parent, attributes) => { const cache = { __proto__: null }; const target = Object.setPrototypeOf(() => { }, null); delete target['length']; delete target['name']; Object.assign(target, { [CACHE]: cache, [OPTIONS]: options, [PATH]: path, [Symbol.toPrimitive]: toJSON, toJSON, }); const locatorProxy = new Proxy(target, handler); target[LOCATOR] = locatorProxy; if (parent || attributes) { target[ATTRIBUTES] = attributes; target[PARENT] = parent; } return locatorProxy; }; /** * Default options of root locator (without mapping attributes function). */ const DEFAULT_OPTIONS = { isProduction: false, parameterAttributePrefix: 'data-test-', pathAttribute: 'data-testid', pathSeparator: '-', }; /** * Get string key for any value for its caching. */ const getKey = (value) => { const type = typeof value; const parts = [type, addLength(value)]; if (value && (type === 'object' || type === 'function')) { for (const key of Reflect.ownKeys(value)) { parts.push(addLength(key), addLength(value[key])); } } return parts.join(''); }; /** * Get a chain of parent attributes for `mapAttributesChain` function. */ const getParentAttributesChain = (target) => { const attributesChain = [ target[ATTRIBUTES] || { [target[OPTIONS].pathAttribute]: target[PATH] }, ]; let currentTarget = target; while ((currentTarget = currentTarget[PARENT])) { if (currentTarget[ATTRIBUTES]) { attributesChain.unshift(currentTarget[ATTRIBUTES]); } } return attributesChain; }; /** * Get path attribute value from component properties that marked with locator. */ const getPathAttributeValueFromProperties = (properties) => { for (const key of Object.keys(properties || true)) { const value = properties[key]; if (value !== null && typeof value === 'object' && LOCATOR in value) { return value; } } return; }; /** * Proxy handler for locator proxy. */ const handler = { apply(target, _thisArg, args) { const cache = target[CACHE]; const [parameters] = args; const key = args.length ? getKey(parameters) : 'noArgs'; if (key in cache) { return cache[key]; } const { mapAttributesChain, parameterAttributePrefix, pathAttribute } = target[OPTIONS]; const attributes = { [pathAttribute]: target[PATH] }; setAttributesFromParameters(attributes, parameterAttributePrefix, parameters); if (mapAttributesChain) { const mappingResult = args.length ? createLocatorProxy(target[OPTIONS], target[PATH], target[PARENT], attributes) : mapAttributesChain(getParentAttributesChain(target)); cache[key] = mappingResult; return mappingResult; } const pathAttributeValue = { [LOCATOR]: target[LOCATOR], parameters, toJSON: toString, toString, }; attributes[pathAttribute] = pathAttributeValue; cache[key] = attributes; return attributes; }, defineProperty: () => false, deleteProperty: () => false, get(target, property) { if (typeof property !== 'string' || property in target) { return target[property]; } const options = target[OPTIONS]; const newPath = `${target[PATH]}${options.pathSeparator}${property}`; target[property] = createLocatorProxy(options, newPath, options.mapAttributesChain ? target : undefined); return target[property]; }, preventExtensions: () => false, }; /** * Symbol key for locator in path attribute value. */ const LOCATOR = Symbol.for(`${packageUrl}#locator`); /** * Symbol key for options of root locator. */ const OPTIONS = Symbol.for(`${packageUrl}#options`); /** * Symbol key for parent locator. */ const PARENT = Symbol.for(`${packageUrl}#parent`); /** * Symbol key for path of locator. */ const PATH = Symbol.for(`${packageUrl}#path`); /** * Set attributes from parameters to attributes object. */ const setAttributesFromParameters = (attributes, parameterAttributePrefix, parameters) => { if (parameters) { for (const key of Object.keys(parameters)) { attributes[`${parameterAttributePrefix}${key}`] = String(parameters[key]); } } }; /** * Method `toJSON` (and, in fact, `toString`) for locator proxy. */ const toJSON = function () { return this[PATH]; }; /** * Method `toString` for path attribute value. */ const toString = function () { return this[LOCATOR][PATH]; };