UNPKG

playwright-json-runner

Version:

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

533 lines (519 loc) 16.5 kB
'use strict'; var zod = require('zod'); var playwright = require('playwright'); var console$1 = require('console'); var cosmiconfig = require('cosmiconfig'); var test = require('playwright/test'); var jsdom = require('jsdom'); // src/schemas/test-base.ts 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 var testRunSchema = 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); test.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); test.expect(actual).toContain(value); }, "assertElementExists": async (locator) => { await test.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 test.expect(locator)[playwrightFunction] !== "function") { throw new Error(`Invalid 'playwrightFunction': '${playwrightFunction}' is not a valid Playwright assertion.`); } await test.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 _a; return page.getByRole(strategy.value.role, (_a = strategy.value.options) != null ? _a : {}); }, 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` }; function extendConfig(extensions) { var _a, _b, _c, _d, _e; return { ...baseConfig, ...extensions, // Merge top-level properties locatorStrategies: { ...baseConfig.locatorStrategies, ...(_a = extensions.locatorStrategies) != null ? _a : {} // Merge objects }, actionTypeHandlers: { ...baseConfig.actionTypeHandlers, ...(_b = extensions.actionTypeHandlers) != null ? _b : {} // Merge objects }, rules: { ...baseConfig.rules, ...(_c = extensions.rules) != null ? _c : {} // Merge objects }, getterStrategies: { ...baseConfig.getterStrategies, ...(_d = extensions.getterStrategies) != null ? _d : {} // Merge objects }, setterStrategies: { ...baseConfig.setterStrategies, ...(_e = extensions.setterStrategies) != null ? _e : {} // Merge objects } }; } 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 runTests(testRun) { const config2 = getConfiguration(); const browserType = { chrome: playwright.chromium, firefox: playwright.firefox, webkit: playwright.webkit }[testRun.browser] || playwright.chromium; const browser = await browserType.launch(); console.log(`\u{1F680} Running tests on ${testRun.host} using ${testRun.browser}`); await ExecuteTestRun(config2, browser, testRun); await browser.close(); } async function ExecuteTestRun(config2, browser, testRun) { for (const scenario of testRun.scenarios) { const context = await browser.newContext({ baseURL: testRun.host, recordVideo: { dir: "./videos" } }); const page = await context.newPage(); try { executeScenario(config2, page, scenario); } finally { await context.close(); } } } async function executeScenario(config2, page, scenario) { var _a; console.log(`\u{1F4CC} Executing scenario: ${(_a = scenario.label) != null ? _a : scenario.name}`); await page.goto("/"); for (const step of scenario.steps) { await executeStep(config2, page, step); } } async function executeStep(config2, page, step) { var _a; console.log(` \u{1F6E0} Step: ${(_a = step.label) != null ? _a : step.description}`); for (const action of step.actions) { await executeAction(config2, page, action); } } async function executeAction(config2, page, action) { var _a, _b; console.log(` - \u{1F539} Performing action: ` + ((_a = action.label) != null ? _a : `${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"); } } exports.baseConfig = baseConfig; exports.executeAction = executeAction; exports.extendConfig = extendConfig; exports.getConfiguration = getConfiguration; exports.getLocatorValue = getLocatorValue; exports.loadConfiguration = loadConfiguration; exports.resolveLocator = resolveLocator; exports.runTests = runTests; exports.setLocatorValue = setLocatorValue; exports.testRunSchema = testRunSchema; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map