create-locator
Version:
Creates HTML element locators for tests 📌
262 lines (261 loc) • 8.5 kB
JavaScript
;
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];
};