UNPKG

play-ai

Version:

Automate Playwright tests using OpenAI integration

940 lines (927 loc) 27.7 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; 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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; // src/index.ts var index_exports = {}; __export(index_exports, { PlayAIError: () => PlayAIError, play: () => play }); module.exports = __toCommonJS(index_exports); // src/ai/config.ts var char_count = process.env.MAX_TASK_CHARS || "2000"; var MAX_TASK_CHARS = parseInt(char_count, 10); // src/ai/completeTask.ts var import_openai = __toESM(require("openai")); // src/ai/prompt.ts var prompt = (message) => { return `This is your task: ${message.task} * When creating CSS selectors, ensure they are unique and specific enough to select only one element, even if there are multiple elements of the same type (like multiple h1 elements). * Avoid using generic tags like 'h1' alone. Instead, combine them with other attributes or structural relationships to form a unique selector. * You must not derive data from the page if you are able to do so by using one of the provided functions, e.g. locator_evaluate. Webpage snapshot: \`\`\` ${message.snapshot.dom} \`\`\` `; }; // src/ai/createActions.ts var import_crypto = require("crypto"); var import_zod = require("zod"); var createActions = (page) => { const locatorMap = /* @__PURE__ */ new Map(); const getLocator = (elementId) => { console.log("locatorMap", locatorMap); const locator = locatorMap.get(elementId); if (!locator) { throw new Error('Unknown elementId "' + elementId + '"'); } return locator; }; const scrollIntoView = (locator) => __async(void 0, null, function* () { let i = 0; while (yield locator.isHidden()) { yield page.mouse.wheel(0, 300); i++; if (yield locator.isVisible()) { return; } else if (i >= 5) { return; } } }); const waitForPageLoad = () => __async(void 0, null, function* () { yield page.waitForLoadState("domcontentloaded", { timeout: 3e4 }); }); return { locateElement: { function: (args) => __async(void 0, null, function* () { const locator = yield page.locator(args.cssSelector); const elementId = (0, import_crypto.randomUUID)(); locatorMap.set(elementId, locator); return { elementId }; }), name: "locateElement", description: "Locates element using a CSS selector and returns elementId. This element ID can be used with other functions to perform actions on the element.", parse: (args) => { return import_zod.z.object({ cssSelector: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { cssSelector: { type: "string" } } } }, locator_evaluate: { function: (args) => __async(void 0, null, function* () { return { result: yield getLocator(args.elementId).evaluate(args.pageFunction) }; }), description: "Execute JavaScript code in the page, taking the matching element as an argument.", name: "locator_evaluate", parameters: { type: "object", properties: { elementId: { type: "string" }, pageFunction: { type: "string", description: "Function to be evaluated in the page context, e.g. node => node.innerText" } } }, parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string(), pageFunction: import_zod.z.string() }).parse(JSON.parse(args)); } }, locator_getAttribute: { function: (args) => __async(void 0, null, function* () { return { attributeValue: yield getLocator(args.elementId).getAttribute( args.attributeName ) }; }), name: "locator_getAttribute", description: "Returns the matching element's attribute value.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string(), attributeName: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { attributeName: { type: "string" }, elementId: { type: "string" } } } }, locator_innerHTML: { function: (args) => __async(void 0, null, function* () { return { innerHTML: yield getLocator(args.elementId).innerHTML() }; }), name: "locator_innerHTML", description: "Returns the element.innerHTML.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_innerText: { function: (args) => __async(void 0, null, function* () { return { innerText: yield getLocator(args.elementId).innerText() }; }), name: "locator_innerText", description: "Returns the element.innerText.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_textContent: { function: (args) => __async(void 0, null, function* () { return { textContent: yield getLocator(args.elementId).textContent() }; }), name: "locator_textContent", description: "Returns the node.textContent.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_inputValue: { function: (args) => __async(void 0, null, function* () { return { inputValue: yield getLocator(args.elementId).inputValue() }; }), name: "locator_inputValue", description: "Returns input.value for the selected <input> or <textarea> or <select> element.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_blur: { function: (args) => __async(void 0, null, function* () { yield getLocator(args.elementId).blur(); return { success: true }; }), name: "locator_blur", description: "Removes keyboard focus from the current element.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_boundingBox: { function: (args) => __async(void 0, null, function* () { return yield getLocator(args.elementId).boundingBox(); }), name: "locator_boundingBox", description: "This method returns the bounding box of the element matching the locator, or null if the element is not visible. The bounding box is calculated relative to the main frame viewport - which is usually the same as the browser window. The returned object has x, y, width, and height properties.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_check: { function: (args) => __async(void 0, null, function* () { yield getLocator(args.elementId).check(); return { success: true }; }), name: "locator_check", description: "Ensure that checkbox or radio element is checked.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_uncheck: { function: (args) => __async(void 0, null, function* () { yield getLocator(args.elementId).uncheck(); return { success: true }; }), name: "locator_uncheck", description: "Ensure that checkbox or radio element is unchecked.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_isChecked: { function: (args) => __async(void 0, null, function* () { return { isChecked: yield getLocator(args.elementId).isChecked() }; }), name: "locator_isChecked", description: "Returns whether the element is checked.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_isEditable: { function: (args) => __async(void 0, null, function* () { return { isEditable: yield getLocator(args.elementId).isEditable() }; }), name: "locator_isEditable", description: "Returns whether the element is editable. Element is considered editable when it is enabled and does not have readonly property set.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_isEnabled: { function: (args) => __async(void 0, null, function* () { return { isEnabled: yield getLocator(args.elementId).isEnabled() }; }), name: "locator_isEnabled", description: "Returns whether the element is enabled. Element is considered enabled unless it is a <button>, <select>, <input> or <textarea> with a disabled property.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_isVisible: { function: (args) => __async(void 0, null, function* () { return { isVisible: yield getLocator(args.elementId).isVisible() }; }), name: "locator_isVisible", description: "Returns whether the element is visible.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_clear: { function: (args) => __async(void 0, null, function* () { yield getLocator(args.elementId).clear(); return { success: true }; }), name: "locator_clear", description: "Clear the input field.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_click: { function: (args) => __async(void 0, null, function* () { yield getLocator(args.elementId).click(); return { success: true }; }), name: "locator_click", description: "Click an element.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_dbl_click: { function: (args) => __async(void 0, null, function* () { yield getLocator(args.elementId).dblclick(); return { success: true }; }), name: "locator_dbl_click", description: "Double Click an element.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_count: { function: (args) => __async(void 0, null, function* () { return { elementCount: yield getLocator(args.elementId).count() }; }), name: "locator_count", description: "Returns the number of elements matching the locator.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_fill: { function: (args) => __async(void 0, null, function* () { yield getLocator(args.elementId).fill(args.value); return { success: true }; }), name: "locator_fill", description: "Set a value to the input field.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string(), value: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { value: { type: "string" }, elementId: { type: "string" } } } }, locator_hover: { function: (args) => __async(void 0, null, function* () { yield getLocator(args.elementId).hover(); return { success: true }; }), name: "locator_hover", description: "Hover on an element.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_scroll_into_view_if_needed: { function: (args) => __async(void 0, null, function* () { yield getLocator(args.elementId).scrollIntoViewIfNeeded(); return { success: true }; }), name: "locator_scroll_into_view_if_needed", description: "Scroll into view an element if needed.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_scroll_into_element_view: { function: (args) => __async(void 0, null, function* () { yield scrollIntoView(getLocator(args.elementId)); return { success: true }; }), name: "locator_scroll_into_element_view", description: "Scroll into view an element.", parse: (args) => { return import_zod.z.object({ elementId: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { elementId: { type: "string" } } } }, locator_wait_for_page_load: { function: () => __async(void 0, null, function* () { yield waitForPageLoad(); return { success: true }; }), name: "locator_wait_for_page_load", description: "Wait until page load completely", parse: (args) => { return import_zod.z.object({}).parse(JSON.parse(args)); }, parameters: { type: "object", properties: {} } }, page_goto: { function: (args) => __async(void 0, null, function* () { return { url: yield page.goto(args.url) }; }), name: "page_goto", description: "Set a value to the input field.", parse: (args) => { return import_zod.z.object({ cssLocator: import_zod.z.string(), value: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { value: { type: "string" }, cssLocator: { type: "string" } } } }, expect_toBe: { function: (args) => { return { actual: args.actual, expected: args.expected, success: args.actual === args.expected }; }, name: "expect_toBe", description: "Asserts that the actual value is equal to the expected value.", parse: (args) => { return import_zod.z.object({ actual: import_zod.z.string(), expected: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { actual: { type: "string" }, expected: { type: "string" } } } }, expect_notToBe: { function: (args) => { return { actual: args.actual, expected: args.expected, success: args.actual !== args.expected }; }, name: "expect_notToBe", description: "Asserts that the actual value is not equal to the expected value.", parse: (args) => { return import_zod.z.object({ actual: import_zod.z.string(), expected: import_zod.z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { actual: { type: "string" }, expected: { type: "string" } } } }, resultAssertion: { function: (args) => { return args; }, parse: (args) => { return import_zod.z.object({ assertion: import_zod.z.boolean() }).parse(JSON.parse(args)); }, description: "This function is called when the initial instructions asked to assert something; then 'assertion' is either true or false (boolean) depending on whether the assertion succeeded.", name: "resultAssertion", parameters: { type: "object", properties: { assertion: { type: "boolean" } } } }, resultQuery: { function: (args) => { return args; }, parse: (args) => { return import_zod.z.object({ query: import_zod.z.string() }).parse(JSON.parse(args)); }, description: "This function is called at the end when the initial instructions asked to extract data; then 'query' property is set to a text value of the extracted data.", name: "resultQuery", parameters: { type: "object", properties: { query: { type: "string" } } } }, resultAction: { function: () => { return null; }, parse: (args) => { return import_zod.z.object({}).parse(JSON.parse(args)); }, description: "This function is called at the end when the initial instructions asked to perform an action.", name: "resultAction", parameters: { type: "object", properties: {} } }, resultError: { function: (args) => { return { errorMessage: args.errorMessage }; }, parse: (args) => { return import_zod.z.object({ errorMessage: import_zod.z.string() }).parse(JSON.parse(args)); }, description: "If user instructions cannot be completed, then this function is used to produce the final response.", name: "resultError", parameters: { type: "object", properties: { errorMessage: { type: "string" } } } } }; }; // src/ai/completeTask.ts var defaultDebug = process.env.PLAY_AI_DEBUG === "true"; var completeTask = (page, task) => __async(void 0, null, function* () { var _a, _b, _c, _d, _e, _f, _g, _h; const openai = new import_openai.default({ apiKey: (_a = task.options) == null ? void 0 : _a.openaiApiKey, baseURL: (_b = task.options) == null ? void 0 : _b.openaiBaseUrl, defaultQuery: (_c = task.options) == null ? void 0 : _c.openaiDefaultQuery, defaultHeaders: (_d = task.options) == null ? void 0 : _d.openaiDefaultHeaders }); let lastFunctionResult = null; const actions = createActions(page); const debug = (_f = (_e = task.options) == null ? void 0 : _e.debug) != null ? _f : defaultDebug; const runner = openai.beta.chat.completions.runTools({ model: (_h = (_g = task.options) == null ? void 0 : _g.model) != null ? _h : "gpt-4o", messages: [ { role: "user", content: prompt(task) } ], tools: Object.values(actions).map((action) => ({ type: "function", function: action })) }).on("message", (message) => { if (debug) { console.log("> Message", message); } if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0 && message.tool_calls[0].function.arguments) { lastFunctionResult = JSON.parse( message.tool_calls[0].function.arguments ); } }); const finalContent = yield runner.finalContent(); if (debug) { console.log("> finalContent", finalContent); } if (!lastFunctionResult) { throw new Error("No function result found."); } if (debug) { console.log("> lastFunctionResult", lastFunctionResult); } return lastFunctionResult; }); // src/ai/sanitizedHtml.ts var import_sanitize_html = __toESM(require("sanitize-html")); var sanitizeHtmlString = (subject) => { return (0, import_sanitize_html.default)(subject, { allowedTags: import_sanitize_html.default.defaults.allowedTags.concat([ "button", "input", "select", "option", "textarea", "form", "img", "label", "span" ]), allowedAttributes: false }); }; // src/ai/getSnapshot.ts var getSnapshot = (page) => __async(void 0, null, function* () { return { dom: sanitizeHtmlString(yield page.content()) }; }); // src/ai/errors.ts var PlayAIError = class extends Error { /** * Creates an instance of PlayAIError. * * @param {string} [message] - The error message. */ constructor(message) { super(message); this.name = new.target.name; } }; var UnimplementedError = class extends PlayAIError { /** * Creates an instance of UnimplementedError. * * @param {string} [message] - The error message. If not provided, a default message "This feature is not yet implemented." is used. */ constructor(message) { super(message || "This feature is not yet implemented."); } }; // src/ai/play.ts var play = (task, config, options) => __async(void 0, null, function* () { if (!config || !config.page) { throw new UnimplementedError( "The play() function is missing the required `{ page }` argument." ); } const { test, page } = config; if (!test) { return yield runTask(task, page, options); } return test.step(`play-ai '${task}'`, () => __async(void 0, null, function* () { const result = yield runTask(task, page, options); if (result.errorMessage) { throw new UnimplementedError(result.errorMessage); } if (result.assertion !== void 0) { return result.assertion; } if (result.query) { return result.query; } return void 0; })); }); function runTask(task, page, options) { return __async(this, null, function* () { var _a, _b; if (task.length > MAX_TASK_CHARS) { throw new Error( `The task is too long. The maximum number of characters is ${MAX_TASK_CHARS}.` ); } const result = yield completeTask(page, { task, snapshot: yield getSnapshot(page), options: options ? { model: (_a = options.model) != null ? _a : "gpt-4o", debug: (_b = options.debug) != null ? _b : false, openaiApiKey: options.openaiApiKey, openaiBaseUrl: options.openaiBaseUrl, openaiDefaultQuery: options.openaiDefaultQuery, openaiDefaultHeaders: options.openaiDefaultHeaders } : void 0 }); return result; }); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { PlayAIError, play }); //# sourceMappingURL=index.js.map