UNPKG

playwright-json-runner

Version:

Extends Playwright to run tests using JSON-based test definitions.

525 lines (509 loc) 16.6 kB
'use strict'; var zod = require('zod'); require('playwright'); var console$1 = require('console'); var cosmiconfig = require('cosmiconfig'); var default2 = require('playwright/test'); var jsdom = require('jsdom'); var fs = require('fs'); var glob = require('glob'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var default2__namespace = /*#__PURE__*/_interopNamespace(default2); 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 __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget); var withLabelSchema = zod.z.object({ label: zod.z.string().optional() }); var withDescriptionSchema = zod.z.object({ description: zod.z.string().optional() }); var testObjectSchema = withLabelSchema.merge(withDescriptionSchema); var PlaywrightRoleOptionsSchema = zod.z.object({ checked: zod.z.boolean().optional(), disabled: zod.z.boolean().optional(), exact: zod.z.boolean().optional(), expanded: zod.z.boolean().optional(), includeHidden: zod.z.boolean().optional(), level: zod.z.number().optional(), // For name, accept string or RegExp. // If you strictly need to parse only real RegExp objects at runtime, keep it like this. // If you want to accept a "string that might be a pattern," consider a string-based approach. name: zod.z.union([zod.z.string(), zod.z.instanceof(RegExp)]).optional(), pressed: zod.z.boolean().optional(), selected: zod.z.boolean().optional() }).optional(); var PlaywrightRoleSchema = zod.z.enum([ "alert", "alertdialog", "application", "article", "banner", "blockquote", "button", "caption", "cell", "checkbox", "code", "columnheader", "combobox", "complementary", "contentinfo", "definition", "deletion", "dialog", "directory", "document", "emphasis", "feed", "figure", "form", "generic", "grid", "gridcell", "group", "heading", "img", "insertion", "link", "list", "listbox", "listitem", "log", "main", "marquee", "math", "meter", "menu", "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "navigation", "none", "note", "option", "paragraph", "presentation", "progressbar", "radio", "radiogroup", "region", "row", "rowgroup", "rowheader", "scrollbar", "search", "searchbox", "separator", "slider", "spinbutton", "status", "strong", "subscript", "superscript", "switch", "tab", "table", "tablist", "tabpanel", "term", "textbox", "time", "timer", "toolbar", "tooltip", "tree", "treegrid", "treeitem" ]); // src/schemas/locators/locator-parameters.ts var selectorStrategyParamsSchema = zod.z.object({ type: zod.z.literal("selector"), value: zod.z.string() }); var roleStrategyParamsSchema = zod.z.object({ type: zod.z.literal("role"), value: zod.z.object({ role: PlaywrightRoleSchema, options: PlaywrightRoleOptionsSchema }).describe("the values for role are role name and then optiosn object e.g. {value: {role: 'link', options: {name: 'sign on'}}}") }); var testIdStrategyParamsSchema = zod.z.object({ type: zod.z.literal("testId"), value: zod.z.string() }); var textStrategyParamsSchema = zod.z.object({ type: zod.z.literal("text"), value: zod.z.string() }); var locatorParamsSchema; var nestedStrategyParamsSchema = zod.z.lazy( () => zod.z.object({ type: zod.z.literal("nested"), parent: locatorParamsSchema, child: locatorParamsSchema }) ); locatorParamsSchema = zod.z.union([ selectorStrategyParamsSchema, roleStrategyParamsSchema, testIdStrategyParamsSchema, textStrategyParamsSchema, nestedStrategyParamsSchema ]); var actionTypeSchema = zod.z.enum([ "setfieldvalue", "click", "navigate", "expect", "assertFieldValueEquals", "assertFieldValueContains", "assertElementExists", "sleep" ]).describe("The type of action to perform"); var testActionSchema = testObjectSchema.extend({ type: actionTypeSchema, value: zod.z.string().optional(), playwrightFunction: zod.z.string().optional().describe("on verify steps, the expect function to use (e.g. toBe is the playwright equivalent to: expect(locator).toBe(value)"), locator: locatorParamsSchema.optional().describe("Locator to use for the action"), selector: zod.z.string().optional().describe("Selector to use for the action (replaces locator)") }); var testStepSchema = testObjectSchema.extend({ description: zod.z.string(), actions: zod.z.array(testActionSchema) }); // src/schemas/test-scenario.ts var testScenarioSchema = testObjectSchema.extend({ name: zod.z.string(), steps: zod.z.array(testStepSchema) }); // src/schemas/test-run.ts testObjectSchema.extend({ browser: zod.z.enum(["chrome", "firefox", "webkit"]), host: zod.z.string(), scenarios: zod.z.array(testScenarioSchema) }); async function setLocatorValue(locator, value) { const html = await locator.evaluate((el) => el.outerHTML); const config2 = getConfiguration(); const ruleMatch = getRuleMatch(html, config2); if (ruleMatch && config2.setterStrategies[ruleMatch.id]) { const targetLocator = ruleMatch.matchedChild && ruleMatch.xpath ? locator.locator(ruleMatch.xpath) : locator; return await config2.setterStrategies[ruleMatch.id]({ locator: targetLocator, ruleMatch, value }); } throw new Error(`\u274C Couldn't find a rule match for on element ${locator} \b html: ${html}`); } async function getLocatorValue(locator) { const html = await locator.evaluate((el) => el.outerHTML); const config2 = getConfiguration(); const ruleMatch = getRuleMatch(html, config2); if (ruleMatch && config2.getterStrategies[ruleMatch.id]) { const targetLocator = ruleMatch.matchedChild && ruleMatch.xpath ? locator.locator(ruleMatch.xpath) : locator; return await config2.getterStrategies[ruleMatch.id]({ locator: targetLocator, ruleMatch }); } throw new Error(`\u274C Couldn't find a rule match for on element ${locator} \b html: ${html}`); } function getRuleMatch(html, config2) { const { document } = new jsdom.JSDOM(html).window; const element = document.body; if (!element) return null; let xpathMatch = void 0; let matchedChild = void 0; const xpath = (xpath2) => { const result = document.evaluate(xpath2, document, null, 9, null); const matchedElement = result.singleNodeValue; if (matchedElement) { xpathMatch = xpath2; matchedChild = element.firstElementChild !== matchedElement; return true; } return false; }; for (const [id, condition] of Object.entries(config2.rules)) { try { if (condition({ document, element, xpathEval: xpath })) { return { id, xpath: xpathMatch, matchedChild: matchedChild != null ? matchedChild : false }; } } catch (err) { console.error("error executing condition: ", id); throw err; } } return null; } // src/defaults/action-type-handlers.ts var actionTypeHandlers = { "sleep": async (_, { value }) => { }, "navigate": async (_, { value }) => { if (value) { console.log(`Navigating to ${value}`); } else { throw new Error("The 'navigate' action requires a 'url' property."); } }, "setfieldvalue": async (locator, { value }) => { if (!locator) { throw new Error("The 'setFieldValue' action requires a 'locator' property."); } if (value === void 0 || value === null) { throw new Error("The 'setFieldValue' action requires a non-null 'value' property."); } await setLocatorValue(locator, value); }, "click": async (locator) => { if (locator) { await locator.click(); } else { throw new Error("The 'click' action requires a 'locator' or 'selector' property."); } }, "assertFieldValueEquals": async (locator, { value }) => { if (!locator) { throw new Error("The 'setFieldValue' action requires a 'locator' property."); } if (value === void 0 || value === null) { throw new Error("The 'setFieldValue' action requires a non-null 'value' property."); } const actual = await getLocatorValue(locator); default2.expect(actual).toBe(value); }, "assertFieldValueContains": async (locator, { value }) => { if (!locator) { throw new Error("The 'setFieldValue' action requires a 'locator' property."); } if (value === void 0 || value === null) { throw new Error("The 'setFieldValue' action requires a non-null 'value' property."); } const actual = await getLocatorValue(locator); default2.expect(actual).toContain(value); }, "assertElementExists": async (locator) => { await default2.expect(locator).toBeAttached(); }, "expect": async (locator, { value, playwrightFunction }) => { if (!locator) { throw new Error("The 'expect' action requires a 'locator' property."); } if (!value) { throw new Error("The 'expect' action requires a non-null 'value' property."); } if (!playwrightFunction) { throw new Error("The 'expect' action requires an 'playwrightFunction' property."); } if (typeof default2.expect(locator)[playwrightFunction] !== "function") { throw new Error(`Invalid 'playwrightFunction': '${playwrightFunction}' is not a valid Playwright assertion.`); } await default2.expect(locator)[playwrightFunction](value); } }; var action_type_handlers_default = actionTypeHandlers; // src/defaults/getter-setter-rules.ts var getterSetterRules = { "input.datepicker": ({ element }) => element.matches(".custom-datepicker"), "select": ({ xpathEval }) => xpathEval("//select"), "text": ({ xpathEval }) => xpathEval("//input | //textarea") }; var getter_setter_rules_default = getterSetterRules; // src/defaults/getter-strategies.ts var getterStrategies = { "input.datepicker": async ({ locator }) => { return await locator.inputValue(); }, "select": async ({ locator }) => { const selectedOption = locator.locator("option:checked"); return selectedOption ? await selectedOption.innerText() : ""; }, "text": async ({ locator }) => { return await locator.inputValue(); } }; var getter_strategies_default = getterStrategies; // src/defaults/setter-strategies.ts var setterStrategies = { "select": async ({ locator, value }) => { await locator.selectOption({ label: value, value }); }, "text": async ({ locator, value }) => { await locator.fill(value != null ? value : ""); }, "input.datepicker": async ({ locator, value }) => { await locator.click(); await locator.page().locator(`//button[text()='${value}']`).click(); } }; var setter_strategies_default = setterStrategies; // src/locator-resolver.ts async function resolveLocator(locatorStrategies2, page, strategy) { const handler = locatorStrategies2[strategy.type]; if (!handler) { throw new Error(`Invalid locator strategy: ${JSON.stringify(strategy)}`); } return handler(page, strategy); } // src/defaults/locator-strategies.ts var locatorStrategies = { selector: async (page, strategy) => page.locator(strategy.value), role: async (page, strategy) => { var _a2; return page.getByRole(strategy.value.role, (_a2 = strategy.value.options) != null ? _a2 : {}); }, testId: async (page, strategy) => page.getByTestId(strategy.value), text: async (page, strategy) => page.getByText(strategy.value), nested: async (page, strategy) => { const parentLocator = await resolveLocator(locatorStrategies, page, strategy.parent); const childLocator = await resolveLocator(locatorStrategies, page, strategy.child); return parentLocator.locator(childLocator); } }; var locator_strategies_default = locatorStrategies; // src/config.ts var baseConfig = { actionTypeHandlers: action_type_handlers_default, rules: getter_setter_rules_default, setterStrategies: setter_strategies_default, getterStrategies: getter_strategies_default, locatorStrategies: locator_strategies_default, jsonTestDir: "json-tests", jsonTestMatch: `**/*.playwright.json` }; var config; function getConfiguration() { if (config) { return config; } config = loadConfiguration(); return config; } var explorer = cosmiconfig.cosmiconfigSync("playwright-json", { searchPlaces: [ "playwright-json.config.ts", "playwright-json.config.js" ] }); function loadConfiguration() { try { if (explorer) { const result = explorer.search(); if (result && result.config) { return result.config || result.config.default; } else { return baseConfig; } } else { return baseConfig; } } catch (error2) { console.error("\u274C Failed to load user config:", error2); return baseConfig; } } // src/runner.ts async function executeAction(config2, page, action) { var _a2, _b; console.log(` - \u{1F539} Performing action: ` + ((_a2 = action.label) != null ? _a2 : `${action.type}`)); if (action.type === "navigate") { await HandleActionTypeNavigate(action, page); return; } if (action.type === "sleep") { if (!action.value) { throw console$1.error("Action type: sleep must have 'value' prop in MS"); } await page.waitForTimeout(Number.parseInt(action.value)); return; } if (action.selector) { action.locator = { type: "selector", value: action.selector }; } if (!action.locator) { throw new Error(`Action must have a valid locator: ${JSON.stringify(action)}`); } const locator = await resolveLocator(config2.locatorStrategies, page, action.locator); const handler = (_b = Object.entries(config2.actionTypeHandlers).find( ([key]) => key.toLowerCase() === action.type.toLowerCase() )) == null ? void 0 : _b[1]; if (!handler) { throw new Error(`No handler found for action type: ${action.type}`); } await handler(locator, action); } async function HandleActionTypeNavigate(action, page) { if (action.value) { console.log("navigating to: ", action.value); await page.goto(action.value); } else { throw console$1.error("navigate action requires the url to be provided as value"); } } // node_modules/@playwright/test/index.mjs var test_exports = {}; __export(test_exports, { default: () => default2__namespace.default }); __reExport(test_exports, default2__namespace); function loadTestFiles() { const config2 = getConfiguration(); const jsonTestDir = config2.jsonTestDir; const jsonTestPattern = config2.jsonTestMatch; const finalGlobPattern = `**/${jsonTestDir}/${jsonTestPattern}`; console.log("Glob pattern to find test files: ", finalGlobPattern); const jsonFiles2 = glob.globSync(finalGlobPattern); console.log("Found ", jsonFiles2 == null ? void 0 : jsonFiles2.length, " Files."); return jsonFiles2; } var jsonFiles = loadTestFiles(); var _a; for (const testFilePath of jsonFiles) { const testRun = JSON.parse(fs.readFileSync(testFilePath, "utf-8")); const config2 = getConfiguration(); for (const scenario of testRun.scenarios) { (0, test_exports.test)((_a = scenario.label) != null ? _a : scenario.name, async ({ page }) => { var _a2, _b; await page.goto(testRun.host); console.log(`\u{1F4CC} Executing scenario: ${(_a2 = scenario.label) != null ? _a2 : scenario.name}`); for (const step of scenario.steps) { console.log(` \u{1F6E0} Step: ${(_b = step.label) != null ? _b : step.description}`); for (const action of step.actions) { await executeAction(config2, page, action); } } }); } } //# sourceMappingURL=runner-playwright.js.map //# sourceMappingURL=runner-playwright.js.map