playwright-json-runner
Version:
Extends Playwright to run tests using JSON-based test definitions.
525 lines (509 loc) • 16.6 kB
JavaScript
;
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