From a8306b0f4a029c837eee7072f4e85b17d8eb015e Mon Sep 17 00:00:00 2001 From: kobenguyent Date: Wed, 19 Jun 2024 10:05:44 +0200 Subject: [PATCH] synchronized with docs --- docs/ai.md | 107 +++++++++++++++++++++++++++++++++++++++++-- docs/build/AI.js | 91 +++++++++++++++++++++++++++++++++--- docs/build/REST.js | 35 ++++++++++++-- docs/changelog.md | 34 ++++++++++++++ docs/helpers/AI.md | 34 +++++++++++++- docs/helpers/REST.md | 11 +++-- docs/pageobjects.md | 2 + 7 files changed, 292 insertions(+), 22 deletions(-) diff --git a/docs/ai.md b/docs/ai.md index 3f51f2f..a06c3b2 100644 --- a/docs/ai.md +++ b/docs/ai.md @@ -23,6 +23,7 @@ So, instead of asking "write me a test" it can ask "write a test for **this** pa CodeceptJS AI can do the following: * 🏋ī¸â€â™€ī¸ **assist writing tests** in `pause()` or interactive shell mode +* 📃 **generate page objects** in `pause()` or interactive shell mode * 🚑 **self-heal failing tests** (can be used on CI) * đŸ’Ŧ send arbitrary prompts to AI provider from any tested page attaching its HTML contents @@ -260,15 +261,29 @@ By evaluating this information you will be able to check how effective AI can be ### Arbitrary GPT Prompts -What if you want to take ChatGPT on the journey of test automation and ask it questions while browsing pages? +What if you want to take AI on the journey of test automation and ask it questions while browsing pages? -This is possible with the new `AI` helper. Enable it in your config and it will automatically attach to Playwright, WebDriver, or another web helper you use. It includes the following methods: +This is possible with the new `AI` helper. Enable it in your config file in `helpers` section: + +```js +// inside codecept.conf +helpers: { + // Playwright, Puppeteer, or WebDrver helper should be enabled too + Playwright: { + }, + + AI: {} +} +``` + +AI helper will be automatically attached to Playwright, WebDriver, or another web helper you use. It includes the following methods: * `askGptOnPage` - sends GPT prompt attaching the HTML of the page. Large pages will be split into chunks, according to `chunkSize` config. You will receive responses for all chunks. * `askGptOnPageFragment` - sends GPT prompt attaching the HTML of the specific element. This method is recommended over `askGptOnPage` as you can reduce the amount of data to be processed. * `askGptGeneralPrompt` - sends GPT prompt without HTML. +* `askForPageObject` - creates PageObject for you, explained in next section. -OpenAI helper won't remove non-interactive elements, so it is recommended to manually control the size of the sent HTML. +`askGpt` methods won't remove non-interactive elements, so it is recommended to manually control the size of the sent HTML. Here are some good use cases for this helper: @@ -282,7 +297,84 @@ Here are some good use cases for this helper: const pageDoc = await I.askGptOnPageFragment('Act as technical writer, describe what is this page for', '#container'); ``` -As of now, those use cases do not apply to test automation but maybe you can apply them to your testing setup. +As of now, those use cases do not apply to test automation but maybe you can apply them to your testing setup. + +## Generate PageObjects + +Last but not the least. AI helper can be used to quickly prototype PageObjects on pages browsed within interactive session. + +![](/img/ai_page_object.png) + +Enable AI helper as explained in previous section and launch shell: + +``` +npx codeceptjs shell --ai +``` + +Also this is availble from `pause()` if AI helper is enabled, + +Ensure that browser is started in window mode, then browse the web pages on your site. +On a page you want to create PageObject execute `askForPageObject()` command. The only required parameter is the name of a page: + +```js +I.askForPageObject('login') +``` + +This command sends request to AI provider should create valid CodeceptJS PageObject. +Run it few times or switch AI provider if response is not satisfactory to you. + +> You can change the style of PageObject and locator preferences by adjusting prompt in a config file + +When completed successfully, page object is saved to **output** directory and loaded into the shell as `page` variable so locators and methods can be checked on the fly. + +If page object has `signInButton` locator you can quickly check it by typing: + +```js +I.click(page.signInButton) +``` + +If page object has `clickForgotPassword` method you can execute it as: + +```js +=> page.clickForgotPassword() +``` + +```shell +Page object for login is saved to .../output/loginPage-1718579784751.js +Page object registered for this session as `page` variable +Use `=>page.methodName()` in shell to run methods of page object +Use `click(page.locatorName)` to check locators of page object + + I.=>page.clickSignUp() + I.click(page.signUpLink) + I.=> page.enterPassword('asdasd') + I.=> page.clickSignIn() +``` + +You can improve prompt by passing custom request as a second parameter: + +```js +I.askForPageObject('login', 'implement signIn(username, password) method') +``` + +To generate page object for the part of a page, pass in root locator as third parameter. + +```js +I.askForPageObject('login', '', '#auth') +``` + +In this case, all generated locators, will use `#auth` as their root element. + +Don't aim for perfect PageObjects but find a good enough one, which you can use for writing your tests. +All created page objects are considered temporary, that's why saved to `output` directory. + +Rename created PageObject to remove timestamp and move it from `output` to `pages` folder and include it into codecept.conf file: + +```js + include: { + loginPage: "./pages/loginPage.js", + // ... +``` ## Advanced Configuration @@ -315,6 +407,7 @@ ai: { prompts: { writeStep: (html, input) => [{ role: 'user', content: 'As a test engineer...' }] healStep: (html, { step, error, prevSteps }) => [{ role: 'user', content: 'As a test engineer...' }] + generatePageObject: (html, extraPrompt = '', rootLocator = null) => [{ role: 'user', content: 'As a test engineer...' }] } } ``` @@ -392,3 +485,9 @@ To debug AI features run tests with `DEBUG="codeceptjs:ai"` flag. This will prin ``` DEBUG="codeceptjs:ai" npx codeceptjs run --ai ``` + +or if you run it in shell mode: + +``` +DEBUG="codeceptjs:ai" npx codeceptjs shell --ai +``` \ No newline at end of file diff --git a/docs/build/AI.js b/docs/build/AI.js index 0eaa8e3..b00d33e 100644 --- a/docs/build/AI.js +++ b/docs/build/AI.js @@ -1,8 +1,14 @@ const Helper = require('@codeceptjs/helper'); +const ora = require('ora-classic'); +const fs = require('fs'); +const path = require('path'); const ai = require('../ai'); const standardActingHelpers = require('../plugin/standardActingHelpers'); const Container = require('../container'); const { splitByChunks, minifyHtml } = require('../html'); +const { beautify } = require('../utils'); +const output = require('../output'); +const { registerVariable } = require('../pause'); /** * AI Helper for CodeceptJS. @@ -10,6 +16,8 @@ const { splitByChunks, minifyHtml } = require('../html'); * This helper class provides integration with the AI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts. * This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDrvier to ensure the HTML context is available. * + * Use it only in development mode. It is recommended to run it only inside pause() mode. + * * ## Configuration * * This helper should be configured in codecept.json or codecept.conf.js @@ -66,9 +74,9 @@ class AI extends Helper { if (htmlChunks.length > 1) messages.push({ role: 'user', content: 'If action is not possible on this page, do not propose anything, I will send another HTML fragment' }); - const response = await this.aiAssistant.createCompletion(messages); + const response = await this._processAIRequest(messages); - console.log(response); + output.print(response); responses.push(response); } @@ -96,15 +104,15 @@ class AI extends Helper { { role: 'user', content: `Within this HTML: ${minifyHtml(html)}` }, ]; - const response = await this.aiAssistant.createCompletion(messages); + const response = await this._processAIRequest(messages); - console.log(response); + output.print(response); return response; } /** - * Send a general request to ChatGPT and return response. + * Send a general request to AI and return response. * @param {string} prompt * @returns {Promise} - A Promise that resolves to the generated response from the GPT model. */ @@ -113,10 +121,79 @@ class AI extends Helper { { role: 'user', content: prompt }, ]; - const response = await this.aiAssistant.createCompletion(messages); + const response = await this._processAIRequest(messages); + + output.print(response); + + return response; + } + + /** + * Generates PageObject for current page using AI. + * + * It saves the PageObject to the output directory. You can review the page object and adjust it as needed and move to pages directory. + * Prompt can be customized in a global config file. + * + * ```js + * // create page object for whole page + * I.askForPageObject('home'); + * + * // create page object with extra prompt + * I.askForPageObject('home', 'implement signIn(username, password) method'); + * + * // create page object for a specific element + * I.askForPageObject('home', null, '.detail'); + * ``` + * + * Asks for a page object based on the provided page name, locator, and extra prompt. + * + * @async + * @param {string} pageName - The name of the page to retrieve the object for. + * @param {string|null} [extraPrompt=null] - An optional extra prompt for additional context or information. + * @param {string|null} [locator=null] - An optional locator to find a specific element on the page. + * @returns {Promise} A promise that resolves to the requested page object. + */ + async askForPageObject(pageName, extraPrompt = null, locator = null) { + const html = locator ? await this.helper.grabHTMLFrom(locator) : await this.helper.grabSource(); + + const spinner = ora(' Processing AI request...').start(); + await this.aiAssistant.setHtmlContext(html); + const response = await this.aiAssistant.generatePageObject(extraPrompt, locator); + spinner.stop(); + + if (!response[0]) { + output.error('No response from AI'); + return ''; + } + + const code = beautify(response[0]); - console.log(response); + output.print('----- Generated PageObject ----'); + output.print(code); + output.print('-------------------------------'); + const fileName = path.join(output_dir, `${pageName}Page-${Date.now()}.js`); + + output.print(output.styles.bold(`Page object for ${pageName} is saved to ${output.styles.bold(fileName)}`)); + fs.writeFileSync(fileName, code); + + try { + registerVariable('page', require(fileName)); + output.success('Page object registered for this session as `page` variable'); + output.print('Use `=>page.methodName()` in shell to run methods of page object'); + output.print('Use `click(page.locatorName)` to check locators of page object'); + } catch (err) { + output.error('Error while registering page object'); + output.error(err.message); + } + + return code; + } + + async _processAIRequest(messages) { + const spinner = ora(' Processing AI request...').start(); + const response = await this.aiAssistant.createCompletion(messages); + spinner.stop(); return response; } } diff --git a/docs/build/REST.js b/docs/build/REST.js index 0af3334..7e4e3b3 100644 --- a/docs/build/REST.js +++ b/docs/build/REST.js @@ -11,12 +11,13 @@ const { beautify } = require('../utils'); * @typedef RESTConfig * @type {object} * @prop {string} [endpoint] - API base URL - * @prop {boolean} [prettyPrintJson=false] - pretty print json for response/request on console logs - * @prop {number} [timeout=1000] - timeout for requests in milliseconds. 10000ms by default - * @prop {object} [defaultHeaders] - a list of default headers + * @prop {boolean} [prettyPrintJson=false] - pretty print json for response/request on console logs. + * @prop {boolean} [printCurl=false] - print cURL request on console logs. False by default. + * @prop {number} [timeout=1000] - timeout for requests in milliseconds. 10000ms by default. + * @prop {object} [defaultHeaders] - a list of default headers. * @prop {object} [httpAgent] - create an agent with SSL certificate - * @prop {function} [onRequest] - a async function which can update request object. - * @prop {function} [onResponse] - a async function which can update response object. + * @prop {function} [onRequest] - an async function which can update request object. + * @prop {function} [onResponse] - an async function which can update response object. * @prop {number} [maxUploadFileSize] - set the max content file size in MB when performing api calls. */ const config = {}; @@ -42,6 +43,7 @@ const config = {}; * } *} * ``` + * * With httpAgent * * ```js @@ -192,6 +194,9 @@ class REST extends Helper { } this.options.prettyPrintJson ? this.debugSection('Request', beautify(JSON.stringify(_debugRequest))) : this.debugSection('Request', JSON.stringify(_debugRequest)); + if (this.options.printCurl) { + this.debugSection('CURL Request', curlize(request)); + } let response; try { @@ -372,3 +377,23 @@ class REST extends Helper { } } module.exports = REST; + +function curlize(request) { + if (request.data?.constructor.name.toLowerCase() === 'formdata') return 'cURL is not printed as the request body is not a JSON'; + let curl = `curl --location --request ${request.method ? request.method.toUpperCase() : 'GET'} ${request.baseURL} `.replace("'", ''); + + if (request.headers) { + Object.entries(request.headers).forEach(([key, value]) => { + curl += `-H "${key}: ${value}" `; + }); + } + + if (!curl.toLowerCase().includes('content-type: application/json')) { + curl += '-H "Content-Type: application/json" '; + } + + if (request.data) { + curl += `-d '${JSON.stringify(request.data)}'`; + } + return curl; +} diff --git a/docs/changelog.md b/docs/changelog.md index a2ecb9c..0857319 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,40 @@ layout: Section # Releases +## 3.6.4 + +❤ī¸ Thanks all to those who contributed to make this release! ❤ī¸ + +🛩ī¸ *Features* +* feat(rest): print curl ([#4396](https://github.com/codeceptjs/CodeceptJS/issues/4396)) - by **[kobenguyent](https://github.com/kobenguyent)** + +``` +Config: + +... +REST: { + ... + printCurl: true, + ... +} +... + +â€ē [CURL Request] curl --location --request POST https://httpbin.org/post -H ... +``` + +* feat(AI): Generate PageObject, added types, shell improvement ([#4319](https://github.com/codeceptjs/CodeceptJS/issues/4319)) - by **[davert](https://github.com/davert)** + * added `askForPageObject` method to generate PageObjects on the fly + * improved AI types + * interactive shell improved to restore history + +![Screenshot from 2024-06-17 02-47-37](https://github.com/codeceptjs/CodeceptJS/assets/220264/12acd2c7-18d1-4105-a24b-84070ec4d393) + +🐛 *Bug Fixes* +* fix(heal): wrong priority ([#4394](https://github.com/codeceptjs/CodeceptJS/issues/4394)) - by **[kobenguyent](https://github.com/kobenguyent)** + +📖 *Documentation* +* AI docs improvements + ## 3.6.3 ❤ī¸ Thanks all to those who contributed to make this release! ❤ī¸ diff --git a/docs/helpers/AI.md b/docs/helpers/AI.md index b892486..2ee6c65 100644 --- a/docs/helpers/AI.md +++ b/docs/helpers/AI.md @@ -16,6 +16,8 @@ AI Helper for CodeceptJS. This helper class provides integration with the AI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts. This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDrvier to ensure the HTML context is available. +Use it only in development mode. It is recommended to run it only inside pause() mode. + ## Configuration This helper should be configured in codecept.json or codecept.conf.js @@ -26,9 +28,37 @@ This helper should be configured in codecept.json or codecept.conf.js - `config` +### askForPageObject + +Generates PageObject for current page using AI. + +It saves the PageObject to the output directory. You can review the page object and adjust it as needed and move to pages directory. +Prompt can be customized in a global config file. + +```js +// create page object for whole page +I.askForPageObject('home'); + +// create page object with extra prompt +I.askForPageObject('home', 'implement signIn(username, password) method'); + +// create page object for a specific element +I.askForPageObject('home', null, '.detail'); +``` + +Asks for a page object based on the provided page name, locator, and extra prompt. + +#### Parameters + +- `pageName` **[string][1]** The name of the page to retrieve the object for. +- `extraPrompt` **([string][1] | null)** An optional extra prompt for additional context or information. +- `locator` **([string][1] | null)** An optional locator to find a specific element on the page. + +Returns **[Promise][2]<[Object][3]>** A promise that resolves to the requested page object. + ### askGptGeneralPrompt -Send a general request to ChatGPT and return response. +Send a general request to AI and return response. #### Parameters @@ -68,3 +98,5 @@ Returns **[Promise][2]<[string][1]>** A Promise that resolves to the generate [1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String [2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise + +[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object diff --git a/docs/helpers/REST.md b/docs/helpers/REST.md index cd96d5e..9532084 100644 --- a/docs/helpers/REST.md +++ b/docs/helpers/REST.md @@ -23,12 +23,13 @@ Type: [object][4] ### Properties - `endpoint` **[string][3]?** API base URL -- `prettyPrintJson` **[boolean][6]?** pretty print json for response/request on console logs -- `timeout` **[number][5]?** timeout for requests in milliseconds. 10000ms by default -- `defaultHeaders` **[object][4]?** a list of default headers +- `prettyPrintJson` **[boolean][6]?** pretty print json for response/request on console logs. +- `printCurl` **[boolean][6]?** print cURL request on console logs. False by default. +- `timeout` **[number][5]?** timeout for requests in milliseconds. 10000ms by default. +- `defaultHeaders` **[object][4]?** a list of default headers. - `httpAgent` **[object][4]?** create an agent with SSL certificate -- `onRequest` **[function][7]?** a async function which can update request object. -- `onResponse` **[function][7]?** a async function which can update response object. +- `onRequest` **[function][7]?** an async function which can update request object. +- `onResponse` **[function][7]?** an async function which can update response object. - `maxUploadFileSize` **[number][5]?** set the max content file size in MB when performing api calls. diff --git a/docs/pageobjects.md b/docs/pageobjects.md index adacb85..c0c825d 100644 --- a/docs/pageobjects.md +++ b/docs/pageobjects.md @@ -56,6 +56,8 @@ module.exports = function() { ## PageObject +> ✨ CodeceptJS can [generate PageObjects using AI](/ai#generate-pageobjects). It fetches all interactive elements from a page, generates locators and methods page and writes JS code. Generated page object can be tested on the fly within the same browser session. + If an application has different pages (login, admin, etc) you should use a page object. CodeceptJS can generate a template for it with the following command: