pomwright
Version:
POMWright is a complementary test framework for Playwright written in TypeScript.
1,284 lines (1,269 loc) • 48.9 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// index.ts
var POMWright_exports = {};
__export(POMWright_exports, {
BaseApi: () => BaseApi,
BasePage: () => BasePage,
GetByMethod: () => GetByMethod,
GetLocatorBase: () => GetLocatorBase,
PlaywrightReportLogger: () => PlaywrightReportLogger,
test: () => test3
});
module.exports = __toCommonJS(POMWright_exports);
// src/basePage.ts
var import_test3 = require("@playwright/test");
// src/helpers/getLocatorBase.ts
var import_test = require("@playwright/test");
// src/helpers/locatorSchema.interface.ts
var GetByMethod = /* @__PURE__ */ ((GetByMethod2) => {
GetByMethod2["role"] = "role";
GetByMethod2["text"] = "text";
GetByMethod2["label"] = "label";
GetByMethod2["placeholder"] = "placeholder";
GetByMethod2["altText"] = "altText";
GetByMethod2["title"] = "title";
GetByMethod2["locator"] = "locator";
GetByMethod2["frameLocator"] = "frameLocator";
GetByMethod2["testId"] = "testId";
GetByMethod2["dataCy"] = "dataCy";
GetByMethod2["id"] = "id";
return GetByMethod2;
})(GetByMethod || {});
var locatorSchemaDummy = {
role: void 0,
roleOptions: {
checked: void 0,
disabled: void 0,
exact: void 0,
expanded: void 0,
includeHidden: void 0,
level: void 0,
name: void 0,
pressed: void 0,
selected: void 0
},
text: void 0,
textOptions: {
exact: void 0
},
label: void 0,
labelOptions: {
exact: void 0
},
placeholder: void 0,
placeholderOptions: {
exact: void 0
},
altText: void 0,
altTextOptions: {
exact: void 0
},
title: void 0,
titleOptions: {
exact: void 0
},
locator: void 0,
locatorOptions: {
has: void 0,
hasNot: void 0,
hasNotText: void 0,
hasText: void 0
},
frameLocator: void 0,
testId: void 0,
dataCy: void 0,
id: void 0,
filter: {
has: void 0,
hasNot: void 0,
hasNotText: void 0,
hasText: void 0
},
locatorMethod: void 0,
locatorSchemaPath: void 0
};
function getLocatorSchemaDummy() {
return locatorSchemaDummy;
}
// src/helpers/getBy.locator.ts
var GetBy = class {
constructor(page, pwrl) {
this.page = page;
this.log = pwrl.getNewChildLogger(this.constructor.name);
this.methodMap = {
["role" /* role */]: this.role,
["text" /* text */]: this.text,
["label" /* label */]: this.label,
["placeholder" /* placeholder */]: this.placeholder,
["altText" /* altText */]: this.altText,
["title" /* title */]: this.title,
["locator" /* locator */]: this.locator,
["frameLocator" /* frameLocator */]: this.frameLocator,
["testId" /* testId */]: this.testId,
["dataCy" /* dataCy */]: this.dataCy,
["id" /* id */]: this.id
};
this.subMethodMap = {
["role" /* role */]: this.page.getByRole,
["text" /* text */]: this.page.getByText,
["label" /* label */]: this.page.getByLabel,
["placeholder" /* placeholder */]: this.page.getByPlaceholder,
["altText" /* altText */]: this.page.getByAltText,
["title" /* title */]: this.page.getByTitle,
["locator" /* locator */]: this.page.locator
};
}
log;
methodMap;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
subMethodMap;
/**
* Retrieves a Locator based on the details provided in a LocatorSchema.
* The method identifies the appropriate locator creation function from methodMap and invokes it.
* Throws an error if the locator method is unsupported.
*/
getLocator = (locatorSchema) => {
const methodName = locatorSchema.locatorMethod;
const method = this.methodMap[methodName];
if (method) {
return method(locatorSchema);
}
throw new Error(`Unsupported locator method: ${methodName}`);
};
/**
* Internal method to retrieve a Locator using a specified GetByMethodSubset and LocatorSchema.
* It identifies the appropriate locator creation function from subMethodMap and invokes it.
* Throws an error if the caller is unknown or if the initial locator is not found.
*/
getBy = (caller, locator) => {
const method = this.subMethodMap[caller];
if (!method) {
const errorText = "Error: unknown caller of method getBy(caller, locator) in getBy.locators.ts";
this.log.error(errorText);
throw new Error(errorText);
}
const initialPWLocator = locator[caller] ? method.call(this.page, locator[caller], locator?.[`${caller}Options`]) : null;
if (!initialPWLocator) {
const errorText = `Locator "${locator.locatorSchemaPath}" .${caller} is undefined.`;
this.log.warn(errorText);
throw new Error(errorText);
}
return initialPWLocator;
};
/**
* Creates a method for generating a Locator using a specific GetByMethodSubset.
* Returns a function that takes a LocatorSchema and returns a Locator.
* The returned function is a locator creation function corresponding to the specified methodName.
*/
createByMethod = (methodName) => {
return (locator) => {
return this.getBy(methodName, locator);
};
};
// Methods for creating locators using different locator methods.
// These methods are generated using createByMethod and provide a unified way to create locators based on LocatorSchema.
// Each method is responsible for creating a Locator based on a specific attribute (role, text, label, etc.) provided in LocatorSchema.
// These methods return a Locator and throw an error if the necessary attribute is not defined in the LocatorSchema.
role = this.createByMethod("role" /* role */);
text = this.createByMethod("text" /* text */);
label = this.createByMethod("label" /* label */);
placeholder = this.createByMethod("placeholder" /* placeholder */);
altText = this.createByMethod("altText" /* altText */);
title = this.createByMethod("title" /* title */);
locator = this.createByMethod("locator" /* locator */);
/**
* Returns a FrameLocator using the 'frameLocator' selector from a LocatorSchema.
* Throws an error if the frameLocator is not defined.
*/
frameLocator = (locatorSchema) => {
const initialFrameLocator = locatorSchema.frameLocator ? this.page.frameLocator(locatorSchema.frameLocator) : null;
if (!initialFrameLocator) {
const errorText = `Locator "${locatorSchema.locatorSchemaPath}" .frameLocator is not defined.`;
this.log.warn(errorText);
throw new Error(errorText);
}
return initialFrameLocator;
};
/**
* Returns a Locator using the 'testId' selector from a LocatorSchema.
* Throws an error if the testId is not defined.
*/
testId = (locator) => {
const initialPWLocator = locator.testId ? this.page.getByTestId(locator.testId) : null;
if (!initialPWLocator) {
const errorText = `Locator "${locator.locatorSchemaPath}" .testId is not defined.`;
this.log.warn(`Locator "${locator.locatorSchemaPath}" .testId is not defined.`);
throw new Error(errorText);
}
return initialPWLocator;
};
/**
* Returns a Locator using the 'dataCy' selector from a LocatorSchema.
* Throws an error if the dataCy is undefined.
*/
dataCy = (locator) => {
let initialPWLocator = null;
if (locator.dataCy) {
initialPWLocator = locator.dataCy.startsWith("data-cy=") ? this.page.locator(locator.dataCy) : this.page.locator(`data-cy=${locator.dataCy}`);
} else {
const errorText = `Locator "${locator.locatorSchemaPath}" .dataCy is undefined.`;
this.log.warn(errorText);
throw new Error(errorText);
}
return initialPWLocator;
};
/**
* Returns a Locator using the 'id' selector from a LocatorSchema.
* Throws an error if the id is not defined or the id type is unsupported.
*/
id = (locator) => {
let initialPWLocator = null;
let selector;
let regexPattern;
if (!locator.id) {
const errorText = `Locator "${locator.locatorSchemaPath}" .id is not defined.`;
this.log.warn(errorText);
throw new Error(errorText);
}
if (typeof locator.id === "string") {
if (locator.id.startsWith("#")) {
selector = locator.id;
} else if (locator.id.startsWith("id=")) {
selector = `#${locator.id.slice("id=".length)}`;
} else {
selector = `#${locator.id}`;
}
} else if (locator.id instanceof RegExp) {
regexPattern = locator.id.source;
selector = `*[id^="${regexPattern}"]`;
} else {
const errorText = `Unsupported id type: ${typeof locator.id}`;
this.log.error(errorText);
throw new Error(errorText);
}
initialPWLocator = this.page.locator(selector);
return initialPWLocator;
};
};
// src/helpers/getLocatorBase.ts
var REQUIRED_PROPERTIES_FOR_LOCATOR_SCHEMA_WITH_METHODS = [
"update",
"updates",
"addFilter",
"getNestedLocator",
"getLocator",
"locatorSchemaPath",
"locatorMethod",
"schemasMap",
"filterMap"
];
var safeStringifyOfNestedLocatorResults = (obj) => {
const seen = /* @__PURE__ */ new WeakSet();
return JSON.stringify(
obj,
(key, value) => {
if (value instanceof Map) {
return Array.from(value.entries());
}
if (value instanceof RegExp) {
return { type: "RegExp", source: value.source, flags: value.flags };
}
if (value && typeof value === "object" && value.constructor && value.constructor.name === "Locator") {
return { type: "Locator", note: "Custom placeholder - Locators are complex." };
}
if (typeof value === "object" && value !== null) {
if (seen.has(value)) return "[Circular]";
seen.add(value);
}
return value;
},
2
);
};
var GetLocatorBase = class {
constructor(pageObjectClass, log, locatorSubstring) {
this.pageObjectClass = pageObjectClass;
this.log = log;
this.locatorSubstring = locatorSubstring;
this.locatorSchemas = /* @__PURE__ */ new Map();
this.getBy = new GetBy(this.pageObjectClass.page, this.log.getNewChildLogger("GetBy"));
}
getBy;
locatorSchemas;
/**
* getLocatorSchema:
* Given a path P, we:
* 1. Collect deep copies of the schemas involved.
* 2. Create a WithMethodsClass instance with LocatorSubstring = P.
* 3. Return a locator schema copy enriched with chainable methods.
*/
getLocatorSchema(locatorSchemaPath) {
const pathIndexPairs = this.extractPathsFromSchema(locatorSchemaPath);
const schemasMap = this.collectDeepCopies(locatorSchemaPath, pathIndexPairs);
const locatorSchemaCopy = schemasMap.get(locatorSchemaPath);
locatorSchemaCopy.schemasMap = schemasMap;
locatorSchemaCopy.filterMap = /* @__PURE__ */ new Map();
const wrapper = new WithMethodsClass(
this.pageObjectClass,
this.log,
locatorSchemaPath,
schemasMap
);
return wrapper.init(locatorSchemaPath, locatorSchemaCopy);
}
/**
* collectDeepCopies:
* Clones and stores all schemas related to the chosen path and its sub-paths.
* Ensures updates and filters don't affect original schema definitions.
*/
collectDeepCopies(locatorSchemaPath, pathIndexPairs) {
const schemasMap = /* @__PURE__ */ new Map();
const fullSchemaFunc = this.safeGetLocatorSchema(locatorSchemaPath);
if (!fullSchemaFunc) {
const errorMessage = `LocatorSchema not found for path: '${locatorSchemaPath}'`;
this.log.error(errorMessage);
throw new Error(`[${this.pageObjectClass.pocName}] ${errorMessage}`);
}
schemasMap.set(locatorSchemaPath, structuredClone(fullSchemaFunc()));
for (const { path } of pathIndexPairs) {
if (path !== locatorSchemaPath) {
const schemaFunc = this.safeGetLocatorSchema(path);
if (schemaFunc) {
schemasMap.set(path, structuredClone(schemaFunc()));
}
}
}
return schemasMap;
}
isLocatorSchemaWithMethods(schema) {
return REQUIRED_PROPERTIES_FOR_LOCATOR_SCHEMA_WITH_METHODS.every((p) => p in schema);
}
/**
* applyUpdateToSubPath:
* Applies updates to a specific sub-path schema within schemasMap.
* Similar to applyUpdate, but we locate the sub-path schema directly by its path.
*/
applyUpdateToSubPath(schemasMap, subPath, updates) {
const schema = schemasMap.get(subPath);
if (!schema) {
throw new Error(`No schema found for sub-path: '${subPath}'`);
}
const updatedSchema = this.deepMerge(schema, updates);
if (this.isLocatorSchemaWithMethods(schema)) {
Object.assign(schema, updatedSchema);
} else {
schemasMap.set(subPath, updatedSchema);
}
}
/**
* applyUpdate:
* Applies updates to a single schema within the schemasMap.
*/
applyUpdate(schemasMap, locatorSchemaPath, updateData) {
const schema = schemasMap.get(locatorSchemaPath);
if (schema) {
const updatedSchema = this.deepMerge(schema, updateData);
if (this.isLocatorSchemaWithMethods(schema)) {
Object.assign(schema, updatedSchema);
} else {
throw new Error("Invalid LocatorSchema object provided for update method.");
}
}
}
/**
* applyUpdates:
* Applies multiple updates to multiple schemas in the chain, identified by their path indexes.
*/
applyUpdates(schemasMap, pathIndexPairs, updatesData) {
for (const [index, updateAtIndex] of Object.entries(updatesData)) {
const path = pathIndexPairs[Number.parseInt(index)]?.path;
if (path && updateAtIndex) {
const schema = schemasMap.get(path);
if (schema) {
const updatedSchema = this.deepMerge(schema, updateAtIndex);
if (this.isLocatorSchemaWithMethods(schema)) {
Object.assign(schema, updatedSchema);
} else {
schemasMap.set(path, updatedSchema);
}
}
}
}
}
/**
* createLocatorSchema:
* Creates a fresh LocatorSchema object by merging provided schemaDetails with a required locatorSchemaPath.
*/
createLocatorSchema(schemaDetails, locatorSchemaPath) {
const schema = { ...schemaDetails, locatorSchemaPath };
return schema;
}
/**
* addSchema:
* Registers a new LocatorSchema under the given locatorSchemaPath.
* Throws an error if a schema already exists at that path.
*/
addSchema(locatorSchemaPath, schemaDetails) {
const newLocatorSchema = this.createLocatorSchema(schemaDetails, locatorSchemaPath);
const existingSchemaFunc = this.safeGetLocatorSchema(locatorSchemaPath);
if (existingSchemaFunc) {
const existingLocatorSchema = existingSchemaFunc();
throw new Error(
`[${this.pageObjectClass.pocName}] A LocatorSchema with the path '${locatorSchemaPath}' already exists.
Existing Schema: ${JSON.stringify(existingLocatorSchema, null, 2)}
Attempted to Add Schema: ${JSON.stringify(newLocatorSchema, null, 2)}`
);
}
this.locatorSchemas.set(locatorSchemaPath, () => newLocatorSchema);
}
/**
* safeGetLocatorSchema:
* Safely retrieves a schema function if available for the given path.
*/
safeGetLocatorSchema(path) {
return this.locatorSchemas.get(path);
}
/**
* extractPathsFromSchema:
* Splits a path into incremental sub-paths and associates them with optional indices.
* Used by updates and getNestedLocator methods.
*/
extractPathsFromSchema = (paths, indices = {}) => {
const schemaParts = paths.split(".");
let cumulativePath = "";
return schemaParts.map((part, index) => {
cumulativePath = cumulativePath ? `${cumulativePath}.${part}` : part;
return {
path: cumulativePath,
index: indices[index] ?? void 0
};
});
};
/**
* logError:
* Logs detailed error information and re-throws the error to ensure tests fail as expected.
*/
logError = (error, locatorSchemaPath, currentLocator, currentPath, pathIndexPairs, nestedLocatorResults) => {
const errorDetails = {
error: error.message,
locatorSchemaPath,
currentPath,
pathIndexPairs: JSON.stringify(pathIndexPairs, null, 2),
currentLocatorDetails: currentLocator ? {
locatorString: currentLocator,
isNotNull: true
} : { isNotNull: false },
nestedLocatorResults: safeStringifyOfNestedLocatorResults(nestedLocatorResults)
};
this.log.error(
"An error occurred during nested locator construction.\n",
"Error details:\n",
JSON.stringify(errorDetails, null, 2)
);
throw error;
};
/**
* deepMerge:
* Recursively merges source properties into target, validating them against LocatorSchema to ensure no invalid keys.
* Ensures immutability by creating a new object rather than modifying in place.
*/
deepMerge(target, source, schema = getLocatorSchemaDummy()) {
const merged = { ...target };
for (const key of Object.keys(source)) {
if (key === "locatorSchemaPath") {
throw new Error(
`[${this.pageObjectClass.pocName}] Invalid property: 'locatorSchemaPath' cannot be updated. Attempted to update LocatorSchemaPath from '${target[key]}' to '${source[key]}'.`
);
}
if (!(key in schema)) {
throw new Error(`Invalid property: '${key}' is not a valid property of LocatorSchema`);
}
const sourceValue = source[key];
const targetValue = target[key];
if (typeof sourceValue === "object" && sourceValue !== null && schema[key] && typeof schema[key] === "object") {
if (targetValue && typeof targetValue === "object" && !Array.isArray(targetValue)) {
merged[key] = this.deepMerge(
targetValue,
// Updated type here
sourceValue,
schema[key]
);
} else {
merged[key] = this.deepMerge(
{},
sourceValue,
schema[key]
);
}
} else {
if (Array.isArray(sourceValue)) {
merged[key] = Array.isArray(targetValue) ? targetValue.concat(sourceValue) : [...sourceValue];
} else if (typeof sourceValue === "object" && sourceValue !== null && Object.prototype.toString.call(sourceValue) === "[object RegExp]") {
merged[key] = new RegExp(
sourceValue.source,
sourceValue.flags
);
} else {
merged[key] = sourceValue;
}
}
}
return merged;
}
/**
* buildNestedLocator:
* Constructs a nested locator by iterating through each sub-path of locatorSchemaPath and chaining locators.
* Applies filters, indexing (nth), and logs details for debugging during test retries.
*/
buildNestedLocator = async (locatorSchemaPath, schemasMap, filterMap, indices = {}) => {
return await import_test.test.step(`${this.pageObjectClass.pocName}: Build Nested Locator`, async () => {
const pathIndexPairs = this.extractPathsFromSchema(locatorSchemaPath, indices);
let currentLocator = null;
let currentIFrame = null;
const nestedLocatorResults = {
LocatorSchema: null,
NestingSteps: []
};
for (const { path, index } of pathIndexPairs) {
const currentSchema = schemasMap.get(path);
if (!currentSchema) continue;
try {
const nextLocator = this.getBy.getLocator(currentSchema);
currentLocator = currentLocator ? currentLocator.locator(nextLocator) : nextLocator;
if (currentSchema.locatorMethod !== "frameLocator" /* frameLocator */ && currentSchema.filter) {
currentLocator = currentLocator.filter({
has: currentSchema.filter.has,
hasNot: currentSchema.filter.hasNot,
hasNotText: currentSchema.filter.hasNotText,
hasText: currentSchema.filter.hasText
});
}
const filterEntries = filterMap.get(path);
if (filterEntries) {
for (const filterData of filterEntries) {
currentLocator = currentLocator.filter({
has: filterData.has,
hasNot: filterData.hasNot,
hasNotText: filterData.hasNotText,
hasText: filterData.hasText
});
}
}
if (index != null) {
currentLocator = currentLocator.nth(index);
}
if (this.log.isLogLevelEnabled("debug")) {
if (!nestedLocatorResults.LocatorSchema) {
const schemaFromMap = schemasMap.get(locatorSchemaPath);
if (schemaFromMap) {
nestedLocatorResults.LocatorSchema = schemaFromMap;
}
}
if (currentSchema.locatorMethod === "frameLocator" /* frameLocator */) {
if (!currentIFrame) {
currentIFrame = currentSchema.frameLocator;
}
if (currentIFrame && currentSchema.frameLocator && currentIFrame.endsWith(currentSchema.frameLocator)) {
currentIFrame += ` -> ${currentSchema.frameLocator}`;
}
}
if (currentIFrame !== void 0) {
await this.evaluateCurrentLocator(currentLocator, nestedLocatorResults.NestingSteps, currentIFrame);
} else {
await this.evaluateCurrentLocator(currentLocator, nestedLocatorResults.NestingSteps, null);
}
}
} catch (error) {
this.logError(error, locatorSchemaPath, currentLocator, path, pathIndexPairs, nestedLocatorResults);
break;
}
}
if (!currentLocator) {
this.logError(
new Error(`Failed to build nested locator for path: ${locatorSchemaPath}`),
locatorSchemaPath,
currentLocator,
locatorSchemaPath,
pathIndexPairs
);
}
if (this.log.isLogLevelEnabled("debug")) {
this.log.debug("Nested locator evaluation results:", safeStringifyOfNestedLocatorResults(nestedLocatorResults));
}
if (currentLocator != null) {
return currentLocator;
}
});
};
/**
* evaluateCurrentLocator:
* Gathers debug information about the current locator's resolved elements.
* Helps with logging and debugging complex locator chains.
*/
evaluateCurrentLocator = async (currentLocator, resultsArray, currentIFrame) => {
if (currentIFrame) {
resultsArray.push({
currentLocatorString: currentLocator,
currentIFrame,
Note: "iFrame locators evaluation not implemented"
});
} else {
const elementsData = await this.evaluateAndGetAttributes(currentLocator);
resultsArray.push({
currentLocatorString: `${currentLocator}`,
resolved: elementsData.length > 0,
elementCount: elementsData.length,
elementsResolvedTo: elementsData
});
}
};
/**
* evaluateAndGetAttributes:
* Extracts tagName and attributes from all elements matched by the locator for debugging purposes.
*/
evaluateAndGetAttributes = async (pwLocator) => {
return await pwLocator.evaluateAll(
(objects) => objects.map((el) => {
const elementAttributes = el.hasAttributes() ? Object.fromEntries(Array.from(el.attributes).map(({ name, value }) => [name, value])) : {};
return { tagName: el.tagName, attributes: elementAttributes };
})
);
};
};
var WithMethodsClass = class extends GetLocatorBase {
constructor(pageObjectClass, log, locatorSubstring, schemasMap) {
super(pageObjectClass, log, locatorSubstring);
this.pageObjectClass = pageObjectClass;
this.log = log;
this.schemasMap = schemasMap;
}
locatorSchemaPath;
/**
* init:
* Assigns the locatorSchemaPath and binds methods (update, updates, addFilter, getNestedLocator, getLocator)
* directly on the locatorSchemaCopy. Returns the modified copy, now fully chainable and type-safe.
*/
init(locatorSchemaPath, locatorSchemaCopy) {
this.locatorSchemaPath = locatorSchemaPath;
const self = this;
locatorSchemaCopy.update = function(a, b) {
const fullPath = this.locatorSchemaPath;
if (b === void 0) {
const updates = a;
self.applyUpdate(self.schemasMap, self.locatorSchemaPath, updates);
} else {
const subPath = a;
const updates = b;
if (!(subPath === fullPath || fullPath.startsWith(`${subPath}.`))) {
throw new Error(`Invalid sub-path: '${subPath}' is not a valid sub-path of '${fullPath}'.`);
}
self.applyUpdateToSubPath(self.schemasMap, subPath, updates);
}
return this;
};
locatorSchemaCopy.updates = function(indexedUpdates) {
const pathIndexPairs = self.extractPathsFromSchema(self.locatorSchemaPath);
self.applyUpdates(self.schemasMap, pathIndexPairs, indexedUpdates);
return this;
};
locatorSchemaCopy.addFilter = function(subPath, filterData) {
const fullPath = this.locatorSchemaPath;
if (!self.schemasMap.has(subPath)) {
const allowedPaths = self.extractPathsFromSchema(fullPath).map((p) => p.path).filter((path) => self.schemasMap.has(path));
throw new Error(
`Invalid sub-path '${subPath}' in addFilter. Allowed sub-paths are:
${allowedPaths.join(",\n")}`
);
}
if (!this.filterMap) {
this.filterMap = /* @__PURE__ */ new Map();
}
const existingFilters = this.filterMap.get(subPath) || [];
existingFilters.push(filterData);
this.filterMap.set(subPath, existingFilters);
return this;
};
locatorSchemaCopy.getNestedLocator = async function(arg) {
if (arg !== void 0 && arg !== null && typeof arg !== "object") {
throw new Error("Invalid argument passed to getNestedLocator: Expected an object or null.");
}
if (!arg || Object.keys(arg).length === 0) {
return await self.buildNestedLocator(
self.locatorSchemaPath,
self.schemasMap,
this.filterMap,
{}
);
}
const keys = Object.keys(arg);
const isNumberKey = keys.every((key) => /^\d+$/.test(key));
const numericIndices = {};
if (isNumberKey) {
for (const [key, value] of Object.entries(arg)) {
const index = Number(key);
if (typeof value === "number" && value >= 0) {
numericIndices[index] = value;
} else if (value !== null) {
throw new Error(`Invalid index value at key '${key}': Expected a positive number or null.`);
}
}
} else {
const pathIndexPairs = self.extractPathsFromSchema(self.locatorSchemaPath);
const pathToIndexMap = new Map(pathIndexPairs.map((pair, idx) => [pair.path, idx]));
for (const [subPath, value] of Object.entries(arg)) {
if (!self.schemasMap.has(subPath)) {
const validPaths = Array.from(self.schemasMap.keys());
throw new Error(
`Invalid sub-path '${subPath}' in getNestedLocator. Allowed sub-paths are:
${validPaths.join(",\n")}`
);
}
if (!pathToIndexMap.has(subPath)) {
const validPaths = pathIndexPairs.map((p) => p.path).filter((path) => self.schemasMap.has(path));
throw new Error(
`Invalid sub-path '${subPath}' in getNestedLocator. Allowed sub-paths are:
${validPaths.join(",\n")}`
);
}
const numericIndex = pathToIndexMap.get(subPath);
if (numericIndex === void 0) {
throw new Error(`Sub-path '${subPath}' not found in pathToIndexMap.`);
}
if (value !== null && (typeof value !== "number" || value < 0)) {
throw new Error(`Invalid index for sub-path '${subPath}': Expected a positive number or null.`);
}
if (value !== null) {
numericIndices[numericIndex] = value;
}
}
}
return await self.buildNestedLocator(
self.locatorSchemaPath,
self.schemasMap,
this.filterMap,
numericIndices
);
};
locatorSchemaCopy.getLocator = async () => {
return self.getBy.getLocator(locatorSchemaCopy);
};
return locatorSchemaCopy;
}
};
// src/helpers/sessionStorage.actions.ts
var import_test2 = require("@playwright/test");
var SessionStorage = class {
// Initializes the class with a Playwright Page object and a name for the Page Object Class.
constructor(page, pocName) {
this.page = page;
this.pocName = pocName;
}
// Defines an object to hold states to be set in session storage, allowing any value type.
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
queuedStates = {};
// Indicates if the session storage manipulation has been initiated.
isInitiated = false;
/** Writes states to session storage. Accepts an object with key-value pairs representing the states. */
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
async writeToSessionStorage(states) {
await this.page.evaluate((storage) => {
for (const [key, value] of Object.entries(storage)) {
window.sessionStorage.setItem(key, JSON.stringify(value));
}
}, states);
}
/** Reads all states from session storage and returns them as an object. */
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
async readFromSessionStorage() {
return await this.page.evaluate(() => {
const storage = {};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key !== null) {
const item = sessionStorage.getItem(key);
try {
storage[key] = item ? JSON.parse(item) : null;
} catch (e) {
storage[key] = item;
}
}
}
return storage;
});
}
/**
* Sets the specified states in session storage.
* Optionally reloads the page after setting the data to ensure the new session storage state is active.
*
* Parameters:
* states: Object representing the states to set in session storage.
* reload: Boolean indicating whether to reload the page after setting the session storage data.
*/
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
async set(states, reload) {
await import_test2.test.step(`${this.pocName}: setSessionStorage`, async () => {
await this.writeToSessionStorage(states);
if (reload) {
await this.page.reload();
}
});
}
/**
* Queues states to be set in the sessionStorage before the next navigation occurs.
* Handles different scenarios based on whether the context exists or multiple calls are made.
*
* 1. No Context, Single Call: Queues and sets states upon the next navigation.
* 2. No Context, Multiple Calls: Merges states from multiple calls and sets them upon the next navigation.
* 3. With Context: Directly sets states in sessionStorage if the context already exists.
*
* Parameters:
* states: Object representing the states to queue for setting in session storage.
*/
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
async setOnNextNavigation(states) {
this.queuedStates = { ...this.queuedStates, ...states };
const populateStorage = async () => {
await import_test2.test.step(`${this.pocName}: setSessionStorageBeforeNavigation`, async () => {
await this.writeToSessionStorage(this.queuedStates);
});
this.queuedStates = {};
};
let contextExists = false;
try {
contextExists = await this.page.evaluate(() => {
return typeof window !== "undefined" && window.sessionStorage !== void 0;
});
} catch (e) {
contextExists = false;
}
if (contextExists) {
await populateStorage();
return;
}
if (!this.isInitiated) {
this.isInitiated = true;
this.page.once("framenavigated", async () => {
await populateStorage();
});
}
}
/**
* Fetches states from session storage.
* If specific keys are provided, fetches only those states; otherwise, fetches all states.
*
* Parameters:
* keys: Optional array of keys to specify which states to fetch from session storage.
*
* Returns:
* Object containing the fetched states.
*/
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
async get(keys) {
let result = {};
await import_test2.test.step(`${this.pocName}: getSessionStorage`, async () => {
const allData = await this.readFromSessionStorage();
if (keys && keys.length > 0) {
for (const key of keys) {
if (Object.prototype.hasOwnProperty.call(allData, key)) {
result[key] = allData[key];
}
}
} else {
result = allData;
}
});
return result;
}
/**
* Clears all states in sessionStorage.
*/
async clear() {
await import_test2.test.step(`${this.pocName}: clear SessionStorage`, async () => {
await this.page.evaluate(() => sessionStorage.clear());
});
}
};
// src/utils/selectorEngines.ts
function createCypressIdEngine() {
return {
/**
* Uses the document's querySelector method to find the first element with a specific 'data-cy' attribute.
* Constructs a selector string for the 'data-cy' attribute and searches the DOM for the first match.
*
* Parameters:
* - document: An object that mimics the global document, having a querySelector method.
* - selector: A string representing the value of the 'data-cy' attribute to search for.
*
* Returns the first HTML element matching the 'data-cy' attribute, or null if no match is found.
*/
query(document, selector) {
const attr = `[data-cy="${selector}"]`;
const el = document.querySelector(attr);
return el;
},
/**
* Uses the document's querySelectorAll method to find all elements with a specific 'data-cy' attribute.
* Constructs a selector string for the 'data-cy' attribute and retrieves all matching elements in the DOM.
* Converts the NodeList from querySelectorAll into an array for easier handling and manipulation.
*
* Parameters:
* - document: An object that mimics the global document, having a querySelectorAll method.
* - selector: A string representing the value of the 'data-cy' attribute to search for.
*
* Returns an array of HTML elements matching the 'data-cy' attribute. Returns an empty array if no matches are found.
*/
queryAll(document, selector) {
const attr = `[data-cy="${selector}"]`;
const els = Array.from(document.querySelectorAll(attr));
return els;
}
};
}
// src/basePage.ts
var selectorRegistered = false;
var BasePage = class {
/** Provides Playwright page methods */
page;
/** Playwright TestInfo contains information about currently running test, available to any test function */
testInfo;
/** Selectors can be used to install custom selector engines.*/
selector;
/** The base URL of the Page Object Class */
baseUrl;
/** The URL path of the Page Object Class */
urlPath;
/** The full URL of the Page Object Class */
fullUrl;
/** The name of the Page Object Class */
pocName;
/** The Page Object Class' PlaywrightReportLogger instance, prefixed with its name. Log levels: debug, info, warn, and error. */
log;
/** The SessionStorage class provides methods for setting and getting session storage data in Playwright.*/
sessionStorage;
/**
* locators:
* An instance of GetLocatorBase that handles schema management and provides getLocatorSchema calls.
* Initially, LocatorSubstring is undefined. Once getLocatorSchema(path) is called,
* we get a chainable object typed with LocatorSubstring = P.
*/
locators;
constructor(page, testInfo, baseUrl, urlPath, pocName, pwrl, locatorSubstring) {
this.page = page;
this.testInfo = testInfo;
this.selector = import_test3.selectors;
this.baseUrl = baseUrl;
this.urlPath = urlPath;
this.fullUrl = this.constructFullUrl(baseUrl, urlPath);
this.pocName = pocName;
this.log = pwrl.getNewChildLogger(pocName);
this.locators = new GetLocatorBase(
this,
this.log.getNewChildLogger("GetLocator"),
locatorSubstring
);
this.initLocatorSchemas();
this.sessionStorage = new SessionStorage(this.page, this.pocName);
if (!selectorRegistered) {
import_test3.selectors.register("data-cy", createCypressIdEngine);
selectorRegistered = true;
}
}
/**
* constructFullUrl:
* Combines baseUrl and urlPath, handling both strings and RegExps.
* Ensures a flexible approach to URL matching (string or regex-based).
*/
constructFullUrl(baseUrl, urlPath) {
const escapeStringForRegExp = (str) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
if (typeof baseUrl === "string" && typeof urlPath === "string") {
return `${baseUrl}${urlPath}`;
}
if (typeof baseUrl === "string" && urlPath instanceof RegExp) {
return new RegExp(`^${escapeStringForRegExp(baseUrl)}${urlPath.source}`);
}
if (baseUrl instanceof RegExp && typeof urlPath === "string") {
return new RegExp(`${baseUrl.source}${escapeStringForRegExp(urlPath)}$`);
}
if (baseUrl instanceof RegExp && urlPath instanceof RegExp) {
return new RegExp(`${baseUrl.source}${urlPath.source}`);
}
throw new Error("Invalid baseUrl or urlPath types. Expected string or RegExp.");
}
/**
* Implementation of getNestedLocator.
*/
async getNestedLocator(locatorSchemaPath, subPathIndices) {
const withValidation = new WithSubPathValidation(
this,
this.log.getNewChildLogger("SubPathValidation"),
locatorSchemaPath
);
return await withValidation.getNestedLocator(subPathIndices);
}
/**
* Short-hand wrapper method for calling .getLocatorSchema(LocatorSchemaPath).getLocator()
*
* This method does not perform nesting,and will return the locator for which the full LocatorSchemaPath resolves to,
* provided by getLocatorSchema("...")
*
* Note: This short-hand wrapper method is useful for quickly getting a locator without having to call
* getLocatorSchema("...") first. On the other hand, it can't be used to update or add filters to the LocatorSchema.
*
* @example
* // Usage:
* const submitButton = await poc.getLocator("main.form.button@submit");
* await expect(submitButton, "should only exist one submit button").toHaveCount(1);
*/
getLocator = async (locatorSchemaPath) => {
return await this.getLocatorSchema(locatorSchemaPath).getLocator();
};
/**
* getLocatorSchema:
* Delegates to this.locators.getLocatorSchema.
* Returns a chainable schema object for the given path.
* Once called with a specific path P, the update and addFilter methods are restricted to sub-paths of P.
*
* The "getLocatorSchema" method is used to retrieve an updatable deep copy of a LocatorSchema defined in the
* GetLocatorBase class. It enriches the returned schema with additional methods to handle updates and retrieval of
* deep copy locators.
*
* getLocatorSchema adds the following chainable methods to the returned LocatorSchemaWithMethods object:
*
* update
* - Allows updating any schema in the chain by specifying the subPath directly.
* - Gives compile-time suggestions for valid sub-paths of the LocatorSchemaPath provided to .getLocatorSchema().
* - If you want to update multiple schemas, chain multiple .update() calls.
*
* addFilter(subPath: SubPaths<LocatorSchemaPathType, LocatorSubstring>, filterData: FilterEntry)
* - The equivalent of the Playwright locator.filter() method
* - This method is used for filtering the specified locator based on the provided filterData.
* - Can be chained multiple times to add multiple filters to the same or different LocatorSchema.
*
* getNestedLocator
* - Asynchronously builds a nested locator based on the LocatorSchemaPath provided by getLocatorSchema("...")
* - Can be chained once after the update and addFilter methods or directly on the .getLocatorSchema method.
* - getNestedLocator will end the method chain and return a nested Playwright Locator.
* - Optionally parameter takes a list of key(subPath)-value(index) pairs, the locator constructed from the LocatorSchema
* with the specified subPath will resolve to the .nth(n) occurrence of the element, within the chain.
*
* getLocator()
* - Asynchronously retrieves a locator based on the current LocatorSchemaPath.
* - This method does not perform nesting and will return the locator for which the full LocatorSchemaPath resolves to, provided by getLocatorSchema("...")
* - Can be chained once after the update and addFilter methods or directly on the .getLocatorSchema method.
* - getLocator will end the method chain and return a Playwright Locator.
*
* Note: Calling getLocator() and getNestedLocator() on the same LocatorSchemaPath will return a Locator for the same
* element, but the Locator returned by getNestedLocator() will be a locator resolving to said same element through
* a chain of locators. While the Locator returned by getLocator() will be a single locator which resolves directly
* to said element. Thus getLocator() is rarely used, while getNestedLocator() is used extensively.
*
* That said, for certain use cases, getLocator() can be useful, and you could use it to manually chain locators
* yourself if some edge case required it. Though, it would be likely be more prudent to expand your LocatorSchemaPath
* type and initLocatorSchemas() method to include the additional locators you need for the given POC, and then use
* getNestedLocator() instead, or by implementing a helper method on your Page Object Class.
*/
getLocatorSchema(path) {
return this.locators.getLocatorSchema(path);
}
};
var WithSubPathValidation = class extends GetLocatorBase {
constructor(pageObjectClass, log, locatorSchemaPath) {
super(pageObjectClass, log, locatorSchemaPath);
this.locatorSchemaPath = locatorSchemaPath;
}
async getNestedLocator(arg) {
return await this.pageObjectClass.getLocatorSchema(this.locatorSchemaPath).getNestedLocator(arg);
}
};
// src/fixture/base.fixtures.ts
var import_test4 = require("@playwright/test");
// src/helpers/playwrightReportLogger.ts
var PlaywrightReportLogger = class _PlaywrightReportLogger {
// Initializes the logger with shared log level, log entries, and a context name.
constructor(sharedLogLevel, sharedLogEntry, contextName) {
this.sharedLogLevel = sharedLogLevel;
this.sharedLogEntry = sharedLogEntry;
this.contextName = contextName;
}
contextName;
logLevels = ["debug", "info", "warn", "error"];
/**
* Creates a child logger with a new contextual name, sharing the same log level and log entries with the parent logger.
*
* The root loggers log "level" is referenced by all child loggers and their child loggers and so on...
* Changing the log "level" of one, will change it for all.
*/
getNewChildLogger(prefix) {
return new _PlaywrightReportLogger(this.sharedLogLevel, this.sharedLogEntry, `${this.contextName} -> ${prefix}`);
}
/**
* Logs a message with the specified log level, prefix, and additional arguments if the current log level permits.
*/
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
log(level, message, ...args) {
const logLevelIndex = this.logLevels.indexOf(level);
if (logLevelIndex < this.getCurrentLogLevelIndex()) {
return;
}
this.sharedLogEntry.push({
timestamp: /* @__PURE__ */ new Date(),
logLevel: level,
prefix: this.contextName,
message: `${message}
${args.join("\n\n")}`
});
}
/**
* Logs a debug-level message with the specified message and arguments.
*/
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
debug(message, ...args) {
this.log("debug", message, ...args);
}
/**
* Logs a info-level message with the specified message and arguments.
*/
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
info(message, ...args) {
this.log("info", message, ...args);
}
/**
* Logs a warn-level message with the specified message and arguments.
*/
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
warn(message, ...args) {
this.log("warn", message, ...args);
}
/**
* Logs a error-level message with the specified message and arguments.
*/
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
error(message, ...args) {
this.log("error", message, ...args);
}
/**
* Sets the current log level to the specified level during runTime.
*/
setLogLevel(level) {
this.sharedLogLevel.current = level;
}
/**
* Retrieves the current log level during runtime.
*/
getCurrentLogLevel() {
return this.sharedLogLevel.current;
}
/**
* Retrieves the index of the current log level in the logLevels array during runtime.
*/
getCurrentLogLevelIndex() {
return this.logLevels.indexOf(this.sharedLogLevel.current);
}
/**
* Resets the current log level to the initial level during runtime.
*/
resetLogLevel() {
this.sharedLogLevel.current = this.sharedLogLevel.initial;
}
/**
* Checks if the input log level is equal to the current log level of the PlaywrightReportLogger instance.
*/
isCurrentLogLevel(level) {
return this.sharedLogLevel.current === level;
}
/**
* Returns 'true' if the "level" parameter provided has an equal or greater index than the current logLevel.
*/
isLogLevelEnabled(level) {
const logLevelIndex = this.logLevels.indexOf(level);
if (logLevelIndex < this.getCurrentLogLevelIndex()) {
return false;
}
return true;
}
/**
* Attaches the recorded log entries to the Playwright HTML report in a sorted and formatted manner.
*/
attachLogsToTest(testInfo) {
this.sharedLogEntry.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
for (const log of this.sharedLogEntry) {
const printTime = log.timestamp.toLocaleTimeString("nb-NO", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
const printDate = log.timestamp.toLocaleDateString("nb-NO", {
day: "2-digit",
month: "2-digit",
year: "numeric"
});
const printLogLevel = `${log.logLevel.toUpperCase()}`;
const printPrefix = log.prefix ? `: [${log.prefix}]` : "";
let messageBody = "";
let messageContentType = "";
try {
const parsedMessage = JSON.parse(log.message);
messageContentType = "application/json";
messageBody = JSON.stringify(parsedMessage, null, 2);
} catch (error) {
messageContentType = "text/plain";
messageBody = log.message;
}
testInfo.attach(`${printTime} ${printDate} - ${printLogLevel} ${printPrefix}`, {
contentType: messageContentType,
body: Buffer.from(messageBody)
});
}
}
};
// src/fixture/base.fixtures.ts
var test3 = import_test4.test.extend({
// biome-ignore lint/correctness/noEmptyPattern: <Playwright does not support the use of _, thus we must provide an empty object {}>
log: async ({}, use, testInfo) => {
const contextName = "TestCase";
const sharedLogEntry = [];
const sharedLogLevel = testInfo.retry === 0 ? { current: "warn", initial: "warn" } : { current: "debug", initial: "debug" };
const log = new PlaywrightReportLogger(sharedLogLevel, sharedLogEntry, contextName);
await use(log);
log.attachLogsToTest(testInfo);
}
});
// src/api/baseApi.ts
var BaseApi = class {
baseUrl;
apiName;
log;
request;
constructor(baseUrl, apiName, context, pwrl) {
this.baseUrl = baseUrl;
this.apiName = apiName;
this.log = pwrl.getNewChildLogger(apiName);
this.request = context;
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BaseApi,
BasePage,
GetByMethod,
GetLocatorBase,
PlaywrightReportLogger,
test
});