UNPKG

play-ai

Version:

Automate Playwright tests using OpenAI integration

903 lines (891 loc) 25.2 kB
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/ai/config.ts var char_count = process.env.MAX_TASK_CHARS || "2000"; var MAX_TASK_CHARS = parseInt(char_count, 10); // src/ai/completeTask.ts import OpenAI from "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 import { randomUUID } from "crypto"; import { z } from "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 = 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 z.object({ cssSelector: 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 z.object({ elementId: z.string(), pageFunction: 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 z.object({ elementId: z.string(), attributeName: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: z.string(), value: 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 z.object({ elementId: 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 z.object({ elementId: 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 z.object({ elementId: 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 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 z.object({ cssLocator: z.string(), value: 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 z.object({ actual: z.string(), expected: 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 z.object({ actual: z.string(), expected: z.string() }).parse(JSON.parse(args)); }, parameters: { type: "object", properties: { actual: { type: "string" }, expected: { type: "string" } } } }, resultAssertion: { function: (args) => { return args; }, parse: (args) => { return z.object({ assertion: 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 z.object({ query: 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 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 z.object({ errorMessage: 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 OpenAI({ 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 import sanitizeHtml from "sanitize-html"; var sanitizeHtmlString = (subject) => { return sanitizeHtml(subject, { allowedTags: sanitizeHtml.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; }); } export { PlayAIError, play }; //# sourceMappingURL=index.mjs.map