UNPKG

pomwright

Version:

POMWright is a complementary test framework for Playwright written in TypeScript.

1,284 lines (1,269 loc) 48.9 kB
"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 });