diff --git a/.gitignore b/.gitignore index f393bf7..759520c 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,4 @@ screenshot.png app.test.ts gen.test.ts *.test.ts +examples/java_selenium/src/test/java/BasicTest.java diff --git a/.vscode/settings.json b/.vscode/settings.json index 91ac2b6..5446d94 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "editor.inlineSuggest.suppressSuggestions": false, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } \ No newline at end of file diff --git a/examples/java_selenium/.gitattributes b/examples/java_selenium/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/examples/java_selenium/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/examples/java_selenium/.gitignore b/examples/java_selenium/.gitignore new file mode 100644 index 0000000..b93e4aa --- /dev/null +++ b/examples/java_selenium/.gitignore @@ -0,0 +1,26 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +target diff --git a/examples/java_selenium/.vscode/launch.json b/examples/java_selenium/.vscode/launch.json new file mode 100644 index 0000000..89c209a --- /dev/null +++ b/examples/java_selenium/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Current File", + "request": "launch", + "mainClass": "${file}" + }, + { + "type": "java", + "name": "Main", + "request": "launch", + "mainClass": "com.example.Main", + "projectName": "demo" + } + ] +} \ No newline at end of file diff --git a/examples/java_selenium/.vscode/settings.json b/examples/java_selenium/.vscode/settings.json new file mode 100644 index 0000000..dc197c8 --- /dev/null +++ b/examples/java_selenium/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic", + "[xml]": { + "editor.defaultFormatter": "redhat.vscode-xml", + "editor.formatOnSave": true, + "editor.autoClosingBrackets": "never", + "files.trimFinalNewlines": true + }, + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/examples/java_selenium/LICENSE b/examples/java_selenium/LICENSE new file mode 100644 index 0000000..5165288 --- /dev/null +++ b/examples/java_selenium/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 wuttinanhi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/java_selenium/README.md b/examples/java_selenium/README.md new file mode 100644 index 0000000..510d987 --- /dev/null +++ b/examples/java_selenium/README.md @@ -0,0 +1,6 @@ +# aitestgen java test + +## Run test +```bash +mvn test +``` \ No newline at end of file diff --git a/examples/java_selenium/pom.xml b/examples/java_selenium/pom.xml new file mode 100644 index 0000000..d5a15ae --- /dev/null +++ b/examples/java_selenium/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + com.example + demo + 1.0-SNAPSHOT + + + 17 + 17 + 4.26.0 + + + + + org.seleniumhq.selenium + selenium-java + ${selenium.version} + + + + + org.junit.jupiter + junit-jupiter-engine + 5.11.3 + test + + + \ No newline at end of file diff --git a/examples/java_selenium/src/main/java/com/example/Main.java b/examples/java_selenium/src/main/java/com/example/Main.java new file mode 100644 index 0000000..03b4b7e --- /dev/null +++ b/examples/java_selenium/src/main/java/com/example/Main.java @@ -0,0 +1,7 @@ +package com.example; + +public class Main { + public static void main(String[] args) { + System.out.println("Hello world!"); + } +} diff --git a/examples/java_selenium/src/test/java/BasicTest.java b/examples/java_selenium/src/test/java/BasicTest.java new file mode 100644 index 0000000..943320e --- /dev/null +++ b/examples/java_selenium/src/test/java/BasicTest.java @@ -0,0 +1,48 @@ +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.support.ui.WebDriverWait; + +public class BasicTest { + + WebDriver driver; + + @BeforeEach + public void setup() { + driver = new ChromeDriver(); + } + + @AfterEach + public void teardown() { + driver.quit(); + } + + @Test + public void SimpleTest() { + driver.get("https://www.selenium.dev/selenium/web/web-form.html"); + + WebElement textInput = driver.findElement(By.cssSelector("#my-text-id")); + + WebElement submitButton = driver.findElement(By.cssSelector(".btn")); + + textInput.sendKeys("hello"); + + submitButton.click(); + + new WebDriverWait(driver, Duration.ofSeconds(10)).until(webDriver -> ((JavascriptExecutor) webDriver).executeScript("return document.readyState").equals("complete")); + + WebElement successMessage = driver.findElement(By.cssSelector("#message")); + String textsuccessMessage = successMessage.getText(); + assertEquals("Received!", textsuccessMessage); + + driver.quit(); + } +} diff --git a/examples/testprompts/selenium.xml b/examples/testprompts/selenium.xml new file mode 100644 index 0000000..265c70f --- /dev/null +++ b/examples/testprompts/selenium.xml @@ -0,0 +1,20 @@ + + Selenium Test + examples/java_selenium/src/test/java/BasicTest.java + java + selenium + openai + gpt-4o-mini + BasicTest + + + SimpleTest + + 1. go to https://www.selenium.dev/selenium/web/web-form.html + 2. set text input to "hello" + 3. click submit + 4. expected received message + + + + \ No newline at end of file diff --git a/package.json b/package.json index 4c2af9a..e4c81af 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "openai": "4.68.4", "ora": "^8.1.1", "prettier": "^3.3.3", + "prettier-plugin-java": "^2.6.5", "puppeteer": "^23.5.0", "puppeteer-element2selector": "^0.0.3", "ts-node": "^10.9.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 640858a..b5977a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: prettier: specifier: ^3.3.3 version: 3.3.3 + prettier-plugin-java: + specifier: ^2.6.5 + version: 2.6.5 puppeteer: specifier: ^23.5.0 version: 23.8.0(typescript@5.6.3) @@ -74,6 +77,21 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -664,6 +682,14 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + chromium-bidi@0.8.0: resolution: {integrity: sha512-uJydbGdTw0DEUjhoogGveneJVWX/9YuqkWePzMmkBYwtdAqo5d3J/ovNKFr+/2hWXYmYCr6it8mSSTIj6SS6Ug==} peerDependencies: @@ -914,6 +940,9 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + java-parser@2.3.2: + resolution: {integrity: sha512-/O42UbEHy3VVJw8W0ruHkQjW75oWvQx4QisoUDRIGir6q3/IZ4JslDMPMYEqp7LU56PYJkH5uXdQiBaCXt/Opw==} + js-tiktoken@1.0.15: resolution: {integrity: sha512-65ruOWWXDEZHHbAo7EjOcNxOGasQKbL4Fq3jEr2xsCqSsoOo6VVSqzWQb6PRIqypFSDcma4jO90YP0w5X8qVXQ==} @@ -994,6 +1023,12 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} @@ -1139,6 +1174,14 @@ packages: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} + prettier-plugin-java@2.6.5: + resolution: {integrity: sha512-2RkPNXyYpP5dRhr04pz45n+e5LXwYWTh1JXrztiCkZTGGokIGYrfwUuGa8csnDoGbP6CDPgVm8zZSIm/9I0SRQ==} + + prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + prettier@3.3.3: resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} @@ -1510,6 +1553,23 @@ snapshots: '@babel/helper-validator-identifier@7.25.9': {} + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -1940,6 +2000,20 @@ snapshots: check-error@2.1.1: {} + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.21 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + chromium-bidi@0.8.0(devtools-protocol@0.0.1367902): dependencies: devtools-protocol: 0.0.1367902 @@ -2208,6 +2282,12 @@ snapshots: is-unicode-supported@2.1.0: {} + java-parser@2.3.2: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + lodash: 4.17.21 + js-tiktoken@1.0.15: dependencies: base64-js: 1.5.1 @@ -2264,6 +2344,10 @@ snapshots: lines-and-columns@1.2.4: {} + lodash-es@4.17.21: {} + + lodash@4.17.21: {} + log-symbols@6.0.0: dependencies: chalk: 5.3.0 @@ -2416,6 +2500,14 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prettier-plugin-java@2.6.5: + dependencies: + java-parser: 2.3.2 + lodash: 4.17.21 + prettier: 3.2.5 + + prettier@3.2.5: {} + prettier@3.3.3: {} progress@2.0.3: {} diff --git a/src/cmds/cli.ts b/src/cmds/cli.ts index 9fd4a2d..f598857 100644 --- a/src/cmds/cli.ts +++ b/src/cmds/cli.ts @@ -8,10 +8,11 @@ import { runGenMode } from "../modes/gen.ts"; import { runPromptMode } from "../modes/prompt.ts"; import { runTestMode } from "../modes/test.ts"; import { parseTestPrompt } from "../testprompt/parser.ts"; -import { getTemplateByTranslatorName, getTranslator } from "../translators/index.ts"; import { GenCommand } from "./gen.ts"; import { PromptCommand } from "./prompt.ts"; import { TestCommand } from "./test.ts"; +import { getTranslator } from "../translators/index.ts"; +import { getTemplateByTranslatorName } from "../templates/index.ts"; export async function main() { const program = new Command(); diff --git a/src/cmds/gen.ts b/src/cmds/gen.ts index cab1604..15d673b 100644 --- a/src/cmds/gen.ts +++ b/src/cmds/gen.ts @@ -7,6 +7,7 @@ export class GenCommand extends Command { this.description("Generate test from test prompt file"); this.option("-f, --file ", "Specify test prompt file path", ""); + this.option("-translate, --translate ", "Translate from json file only", ""); addGenericOptions(this as any); } diff --git a/src/helpers/files.ts b/src/helpers/files.ts index 2624963..468905e 100644 --- a/src/helpers/files.ts +++ b/src/helpers/files.ts @@ -36,3 +36,7 @@ export async function createDir(dirPath: string) { export async function fileExists(filePath: string) { return existsSync(filePath); } + +export function fileBaseName(filepath: string) { + return path.basename(filepath); +} diff --git a/src/helpers/formatter.ts b/src/helpers/formatter.ts index fa408ff..28a08ec 100644 --- a/src/helpers/formatter.ts +++ b/src/helpers/formatter.ts @@ -1,6 +1,7 @@ import * as prettier from "prettier"; +// import * as prettierPluginJava from "prettier-plugin-java"; -export async function formatTSCode(code: string): Promise { +export async function formatTypescriptCode(code: string): Promise { return await prettier.format(code, { parser: "typescript", semi: true, @@ -11,7 +12,7 @@ export async function formatTSCode(code: string): Promise { export async function formatCodeByLanguage(lang: string, code: string) { switch (lang) { case "typescript": - return formatTSCode(code); + return formatTypescriptCode(code); default: throw new Error(`formatter unknown language: ${lang}`); } diff --git a/src/testprompt/types.ts b/src/interfaces/testprompt.ts similarity index 90% rename from src/testprompt/types.ts rename to src/interfaces/testprompt.ts index b50b44f..7fbc98b 100644 --- a/src/testprompt/types.ts +++ b/src/interfaces/testprompt.ts @@ -10,6 +10,8 @@ export interface Testsuite { provider: string; model: string; testcases: Testcases; + // Java + java_classname: string; } export interface Testcases { diff --git a/src/interfaces/translator.ts b/src/interfaces/translator.ts new file mode 100644 index 0000000..e1119ce --- /dev/null +++ b/src/interfaces/translator.ts @@ -0,0 +1,5 @@ +import { Step } from "./step.ts"; + +export interface TestTranslator { + generate(steps: Step[]): Promise; +} diff --git a/src/modes/gen.ts b/src/modes/gen.ts index 5a4aadc..08319f8 100644 --- a/src/modes/gen.ts +++ b/src/modes/gen.ts @@ -1,13 +1,13 @@ import { BaseMessage } from "@langchain/core/messages"; import { createTestStepGeneratorWithOptions, createWebControllerWithOptions } from "../helpers/cli.ts"; -import { formatCodeByLanguage, formatTSCode } from "../helpers/formatter.ts"; import { Step } from "../interfaces/step.ts"; import { AIModel } from "../models/types.ts"; import { createMessageBuffer, parseModel } from "../models/wrapper.ts"; -import { Testcase, TestPrompt } from "../testprompt/types.ts"; import { TestsuiteTestcaseObject } from "../testsuites/puppeteer.testsuite.ts"; import { getTestsuiteGeneratorByTranslator } from "../testsuites/wrapper.ts"; -import { getTemplateByTranslatorName } from "../translators/index.ts"; +import { Testcase, TestPrompt } from "../interfaces/testprompt.ts"; +import { formatCodeByLanguage } from "../helpers/formatter.ts"; +import { getTemplateByTranslatorName } from "../templates/index.ts"; export interface genModeOptions { genDir: string; diff --git a/src/modes/prompt.ts b/src/modes/prompt.ts index 4427454..b1beadb 100644 --- a/src/modes/prompt.ts +++ b/src/modes/prompt.ts @@ -1,5 +1,6 @@ import { TestStepGenerator } from "../generators/generator.ts"; import { WebController } from "../interfaces/controller.ts"; +import { TestTranslator } from "../interfaces/translator.ts"; import { AIModel } from "../models/types.ts"; import { createMessageBuffer } from "../models/wrapper.ts"; import { PuppeteerTranslator } from "../translators/index.ts"; @@ -9,7 +10,7 @@ export interface promptModeOptions { model: AIModel; webController: WebController; testStepGenerator: TestStepGenerator; - translator: PuppeteerTranslator; + translator: TestTranslator; testCodeTemplate: string; } diff --git a/src/templates/index.ts b/src/templates/index.ts new file mode 100644 index 0000000..efdc22f --- /dev/null +++ b/src/templates/index.ts @@ -0,0 +1,22 @@ +import { readFileSync } from "node:fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +// ES module polyfill __dirname +const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file +const __dirname = path.dirname(__filename); // get the name of the directory + +export const DEFAULT_PUPPETEER_TEMPLATE = readFileSync(`${__dirname}/puppeteer/puppeteer.template.ts`, "utf-8"); + +export const DEFAULT_SELENIUM_TEMPLATE = readFileSync(`${__dirname}/selenium/template.java`, "utf-8"); + +export function getTemplateByTranslatorName(translatorName: string) { + switch (translatorName) { + case "puppeteer": + return DEFAULT_PUPPETEER_TEMPLATE; + case "selenium": + return DEFAULT_SELENIUM_TEMPLATE; + default: + throw new Error(`Template unknown translator: ${translatorName}`); + } +} diff --git a/src/translators/puppeteer/puppeteer.template.ts b/src/templates/puppeteer/puppeteer.template.ts similarity index 100% rename from src/translators/puppeteer/puppeteer.template.ts rename to src/templates/puppeteer/puppeteer.template.ts diff --git a/src/templates/selenium/template.java b/src/templates/selenium/template.java new file mode 100644 index 0000000..384c3a9 --- /dev/null +++ b/src/templates/selenium/template.java @@ -0,0 +1,39 @@ +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.openqa.selenium.Keys; +import org.openqa.selenium.interactions.Actions; + +public class CLASS_NAME_HERE { + WebDriver driver; + + @BeforeEach + public void setup() { + driver = new ChromeDriver(); + } + + + @AfterEach + public void teardown() { + driver.quit(); + } + + // --- START TESTCASE --- + @Test + public void TESTCASE_NAME() { + // {{TESTCASE_GENERATED_CODE}} + } + // --- END TESTCASE --- + + // {{TESTCASES}} +} diff --git a/src/testprompt/parser.ts b/src/testprompt/parser.ts index 9068b69..da05255 100644 --- a/src/testprompt/parser.ts +++ b/src/testprompt/parser.ts @@ -1,5 +1,5 @@ import { XMLParser } from "fast-xml-parser"; -import { TestPrompt } from "./types.ts"; +import { TestPrompt } from "../interfaces/testprompt.ts"; export function parseTestPrompt(xmlString: string) { const parser = new XMLParser(); diff --git a/src/testsuites/puppeteer.testsuite.ts b/src/testsuites/puppeteer.testsuite.ts index b20403b..2b11e81 100644 --- a/src/testsuites/puppeteer.testsuite.ts +++ b/src/testsuites/puppeteer.testsuite.ts @@ -1,5 +1,5 @@ import { Step } from "../interfaces/step.ts"; -import { Testcase } from "../testprompt/types.ts"; +import { Testcase } from "../interfaces/testprompt.ts"; import { PuppeteerTranslator } from "../translators/puppeteer/puppeteer.translator.ts"; export interface PuppeteerTestsuiteGeneratorOptions { @@ -37,7 +37,7 @@ export class PuppeteerTestsuiteGenerator { public async generate(testsuiteName: string, testcases: TestsuiteTestcaseObject[]) { let generatedTestcasesCode = ""; - const testcaseTranslator = new PuppeteerTranslator("browser", "page"); + const testcaseTranslator = new PuppeteerTranslator(); for (const testcase of testcases) { // generate test code from steps diff --git a/src/testsuites/selenium.testsuite.ts b/src/testsuites/selenium.testsuite.ts new file mode 100644 index 0000000..1aa15c1 --- /dev/null +++ b/src/testsuites/selenium.testsuite.ts @@ -0,0 +1,82 @@ +import { Step } from "../interfaces/step.ts"; +import { Testcase } from "../interfaces/testprompt.ts"; +import { SeleniumTranslator } from "../translators/selenium/selenium.translator.ts"; + +export interface SeleniumTestsuiteGeneratorOptions { + placeholderTestcasesCode: string; // {{TESTCASES}} + templateTestcaseStart: string; // --- START TESTCASE --- + templateTestcaseEnd: string; // --- END TESTCASE --- + placeholderTestcaseStepCode: string; // {{TESTCASE_GENERATED_CODE}} + placeholderJavaMethodName: string; // TESTCASE_NAME + placeholderJavaClassName: string; // CLASS_NAME_HERE +} + +export interface TestsuiteTestcaseObject { + testcase: Testcase; + steps: Step[]; +} + +export class SeleniumTestsuiteGenerator { + private templateTestsuite: string; + private templateTestcase: string; + private options: SeleniumTestsuiteGeneratorOptions; + + constructor(template: string, options: SeleniumTestsuiteGeneratorOptions) { + this.templateTestsuite = template; + this.options = options; + + const extractedTestcaseTemplate = this.extractedTestcaseTemplate(options.templateTestcaseStart, options.templateTestcaseEnd, template); + if (!extractedTestcaseTemplate) { + throw new Error(`Can't extract testcase template got: ${extractedTestcaseTemplate}`); + } + + this.templateTestcase = extractedTestcaseTemplate.extracted; + this.templateTestsuite = extractedTestcaseTemplate.modifiedTemplate; + } + + public async generate(javaClassName: string, testcases: TestsuiteTestcaseObject[]) { + let generatedTestcasesCode = ""; + + const testcaseTranslator = new SeleniumTranslator(); + + for (const testcase of testcases) { + // generate test code from steps + const generatedCode = await testcaseTranslator.generate(testcase.steps); + + // replace `TESTCASE_NAME` with testcase name + let buffer = this.templateTestcase.replace(this.options.placeholderJavaMethodName, testcase.testcase.name); + + // replace `// {{TESTCASE_GENERATED_CODE}}` with generated code + buffer = buffer.replace(this.options.placeholderTestcaseStepCode, generatedCode); + + // add code to buffer + generatedTestcasesCode += buffer; + } + + // replace `CLASS_NAME_HERE` with class name + let testsuiteCode = this.templateTestsuite.replace(this.options.placeholderJavaClassName, javaClassName); + + // replace `// {{TESTCASES}}` with generated testcases code + testsuiteCode = testsuiteCode.replace(this.options.placeholderTestcasesCode, generatedTestcasesCode); + + return testsuiteCode; + } + + protected extractedTestcaseTemplate( + startMarker: string, + endMarker: string, + template: string, + ): { extracted: string; modifiedTemplate: string } | null { + const startIndex = template.indexOf(startMarker) + startMarker.length; + const endIndex = template.indexOf(endMarker); + + if (startIndex > -1 && endIndex > startIndex) { + const extracted = template.slice(startIndex, endIndex).trim(); + const modifiedTemplate = template.slice(0, startIndex - startMarker.length).trim() + "\n" + template.slice(endIndex + endMarker.length).trim(); + + return { extracted, modifiedTemplate }; + } + + return null; + } +} diff --git a/src/testsuites/wrapper.ts b/src/testsuites/wrapper.ts index b1fa961..737689b 100644 --- a/src/testsuites/wrapper.ts +++ b/src/testsuites/wrapper.ts @@ -1,4 +1,5 @@ import { PuppeteerTestsuiteGenerator } from "./puppeteer.testsuite.ts"; +import { SeleniumTestsuiteGenerator } from "./selenium.testsuite.ts"; export function getTestsuiteGeneratorByTranslator(translatorName: string, templateCode: string) { switch (translatorName) { @@ -13,6 +14,17 @@ export function getTestsuiteGeneratorByTranslator(translatorName: string, templa }); return testsuiteGen; + case "selenium": + const testsutieGen = new SeleniumTestsuiteGenerator(templateCode, { + placeholderTestcasesCode: "// {{TESTCASES}}", + templateTestcaseStart: "// --- START TESTCASE ---", + templateTestcaseEnd: "// --- END TESTCASE ---", + placeholderTestcaseStepCode: "// {{TESTCASE_GENERATED_CODE}}", + placeholderJavaMethodName: "TESTCASE_NAME", + placeholderJavaClassName: "CLASS_NAME_HERE", + }); + + return testsutieGen; default: throw new Error(`Testsuite generator unknown translator ${translatorName}`); } diff --git a/src/translators/index.ts b/src/translators/index.ts index 993b295..96580f5 100644 --- a/src/translators/index.ts +++ b/src/translators/index.ts @@ -1,29 +1,15 @@ export * from "./puppeteer/puppeteer.translator.ts"; -import { readFileSync } from "node:fs"; -import path from "path"; -import { fileURLToPath } from "url"; import { PuppeteerTranslator } from "./puppeteer/puppeteer.translator.ts"; - -const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file -const __dirname = path.dirname(__filename); // get the name of the directory - -export const DEFAULT_PUPPETEER_TEMPLATE = readFileSync(`${__dirname}/puppeteer/puppeteer.template.ts`, "utf-8"); +import { SeleniumTranslator } from "./selenium/selenium.translator.ts"; export function getTranslator(translatorName: string) { switch (translatorName) { case "puppeteer": - return new PuppeteerTranslator("browser", "page"); + return new PuppeteerTranslator(); + case "selenium": + return new SeleniumTranslator(); default: throw new Error(`Unknown translator: ${translatorName}`); } } - -export function getTemplateByTranslatorName(translatorName: string) { - switch (translatorName) { - case "puppeteer": - return DEFAULT_PUPPETEER_TEMPLATE; - default: - throw new Error(`Template unknown translator: ${translatorName}`); - } -} diff --git a/src/translators/puppeteer/puppeteer.translator.ts b/src/translators/puppeteer/puppeteer.translator.ts index d1b3a0d..f77c24a 100644 --- a/src/translators/puppeteer/puppeteer.translator.ts +++ b/src/translators/puppeteer/puppeteer.translator.ts @@ -1,6 +1,6 @@ import { WebController } from "../../interfaces/controller.ts"; import { writeFileString } from "../../helpers/files.ts"; -import { formatTSCode } from "../../helpers/formatter.ts"; +import { formatTypescriptCode } from "../../helpers/formatter.ts"; import { FrameData } from "../../interfaces/framedata.ts"; import { Step } from "../../interfaces/step.ts"; import { @@ -30,9 +30,10 @@ import { TypeSetOptionValueParams, TypeSetTabParams, } from "../../tools/defs.ts"; +import { TestTranslator } from "../../interfaces/translator.ts"; -export class PuppeteerTranslator implements WebController { - private browserVar: string; +export class PuppeteerTranslator implements WebController, TestTranslator { + private browserVar: string = "page"; private defaultPageVar: string = "page"; private currentPageVar: string = "page"; @@ -41,11 +42,7 @@ export class PuppeteerTranslator implements WebController { private iframeVarStack: string[] = []; private getIframeVarStack: string[] = []; - constructor(templateBrowserVar: string, templatePageVar: string) { - this.browserVar = templateBrowserVar; - this.defaultPageVar = templatePageVar; - this.currentPageVar = templatePageVar; - } + constructor() {} public async generate(steps: Step[]) { let generatedCode = ""; @@ -66,7 +63,7 @@ export class PuppeteerTranslator implements WebController { const replaceTemplateCode = templateCode.replace(templateGenCodePlaceholder, generatedTestCode); // try formatting the generated code - let formattedCode = await formatTSCode(replaceTemplateCode); + let formattedCode = await formatTypescriptCode(replaceTemplateCode); // save to file await writeFileString(outFilePath, formattedCode); diff --git a/src/translators/selenium/selenium.translator.ts b/src/translators/selenium/selenium.translator.ts new file mode 100644 index 0000000..e001a06 --- /dev/null +++ b/src/translators/selenium/selenium.translator.ts @@ -0,0 +1,249 @@ +import { WebController } from "../../interfaces/controller.ts"; +import { FrameData } from "../../interfaces/framedata.ts"; +import { Step } from "../../interfaces/step.ts"; +import { TestTranslator } from "../../interfaces/translator.ts"; +import { + TypeClickElementParams, + TypeCloseBrowserParams, + TypeCloseTabParams, + TypeCompleteParams, + TypeCreateSelectorVariableParams, + TypeExpectElementTextParams, + TypeExpectElementVisibleParams, + TypeGetCurrentUrlParams, + TypeGetHtmlSourceParams, + TypeGetInputValueParams, + TypeGetOptionValueParams, + TypeGetTabsParams, + TypeGoBackHistoryParams, + TypeGoForwardHistoryParams, + TypeIframeGetDataParams, + TypeIframeResetParams, + TypeIframeSwitchParams, + TypeLaunchBrowserParams, + TypeNavigateToParams, + TypePressKeyParams, + TypeQuickSelectorParams, + TypeResetParams, + TypeSetInputValueParams, + TypeSetOptionValueParams, + TypeSetTabParams, +} from "../../tools/defs.ts"; + +export class SeleniumTranslator implements WebController, TestTranslator { + private driverVar: string = "driver"; + + private lastGetIframeData: FrameData[] = []; + private iframeDepth: number = 0; + private iframeVarStack: string[] = []; + private getIframeVarStack: string[] = []; + + constructor() {} + + public async generate(steps: Step[]) { + let generatedCode = ""; + + for (const [index, step] of steps.entries()) { + // if step 0 is launchBrowser then ignore it + // because we already launch browser in BeforeEach + if (index === 0 && step.methodName === "launchBrowser") { + continue; + } + + const line = await this.generateStep(step); + generatedCode += line + "\n"; + } + + return generatedCode; + } + + protected async generateStep(step: Step) { + const stepName = step.methodName; + const stepArgs = step.functionArgs; + + if (stepName === "iframeGetData") { + this.lastGetIframeData = step.iframeGetDataResult; + } + + // invoke self method + const result = await (this as any)[stepName as any](stepArgs); + + return result; + } + + async createSelectorVariable(params: TypeCreateSelectorVariableParams): Promise { + const selectorValue = params.selectorValue; + const selectorType = params.selectorType; + const varName = params.varName; + + let result: string; + + switch (selectorType) { + case "css": + result = `WebElement ${varName} = ${this.driverVar}.findElement(By.cssSelector("${selectorValue}"));`; + break; + case "xpath": + result = `WebElement ${varName} = ${this.driverVar}.findElement(By.xpath("${selectorValue}"));`; + break; + case "id": + result = `WebElement ${varName} = ${this.driverVar}.findElement(By.id("${selectorValue}"));`; + break; + default: + throw new Error("Unknown selector type"); + } + + return result; + } + + async clickElement(params: TypeClickElementParams): Promise { + const varSelector = params.varSelector; + + return `${varSelector}.click();`; + } + + async setInputValue(params: TypeSetInputValueParams): Promise { + const varSelector = params.varSelector; + const value = params.value; + + return `${varSelector}.sendKeys("${value}");`; + } + + async expectElementVisible(params: TypeExpectElementVisibleParams): Promise { + const varSelector = params.varSelector; + const visible = params.visible; + + if (visible == true) { + return `Assertions.assertTrue(${varSelector}.isDisplayed(), "The element is not visible on the page.");`; + } else { + return `Assertions.assertFalse(${varSelector}.isDisplayed(), "The element is visible on the page.");`; + } + } + + async expectElementText(params: TypeExpectElementTextParams): Promise { + const varSelector = params.varSelector; + const expectedText = params.expectedText; + + return `String text${varSelector} = ${varSelector}.getText(); +assertEquals("${expectedText}", text${varSelector}); +`; + } + + async waitForPageLoad(params: any): Promise { + return `new WebDriverWait(driver, Duration.ofSeconds(10)).until( +webDriver -> ((JavascriptExecutor) webDriver).executeScript("return document.readyState").equals("complete") +); +`; + } + + async launchBrowser(params: TypeLaunchBrowserParams): Promise { + return "driver = new ChromeDriver();"; + } + + async closeBrowser(params: TypeCloseBrowserParams): Promise { + return `${this.driverVar}.quit();`; + } + + async navigateTo(params: TypeNavigateToParams): Promise { + const url = params.url; + return `${this.driverVar}.get("${url}");`; + } + + async reset(params: TypeResetParams): Promise { + return ""; + } + + async complete(params: TypeCompleteParams): Promise { + return ""; + } + + async getCurrentUrl(params: TypeGetCurrentUrlParams): Promise { + return ""; + } + + async getHtmlSource(params: TypeGetHtmlSourceParams): Promise { + return ""; + } + + async getInputValue(params: TypeGetInputValueParams): Promise { + return ""; + } + + async setOptionValue(params: TypeSetOptionValueParams): Promise { + const varSelector = params.varSelector; + const value = params.value; + + return `Select select${varSelector} = new Select(dropdown); +select${varSelector}.selectByValue(value);`; + } + + async getOptionValue(params: TypeGetOptionValueParams): Promise { + return ""; + } + + async goBackHistory(params: TypeGoBackHistoryParams): Promise { + return `${this.driverVar}.navigate().back();`; + } + + async goForwardHistory(params: TypeGoForwardHistoryParams): Promise { + return `${this.driverVar}.navigate().forward();`; + } + + async getTabs(params: TypeGetTabsParams): Promise { + return ""; + } + + protected haveDeclaredTabsVariable = false; + + protected createTabVariable() { + if (this.haveDeclaredTabsVariable) { + return `tabs = new ArrayList<>(${this.driverVar}.getWindowHandles());`; + } else { + return `List tabs = new ArrayList<>(${this.driverVar}.getWindowHandles());`; + } + } + + async setTab(params: TypeSetTabParams): Promise { + const tabId = params.tabId; + let out = this.createTabVariable(); + + out += `${this.driverVar}.switchTo().window(tabs.get(${tabId}));`; + + return out; + } + + async closeTab(params: TypeCloseTabParams): Promise { + const tabId = params.tabId; + + let out = this.createTabVariable(); + + out += `${this.driverVar}.switchTo().window(tabs.get(${tabId})); +${this.driverVar}.close(); +${this.driverVar}.switchTo().window(tabs.get(tabs.size() - 1)); // switch to latest tab +`; + + return out; + } + + async iframeGetData(params: TypeIframeGetDataParams): Promise { + return "// TODO: implements iframeGetData"; + } + + async iframeSwitch(params: TypeIframeSwitchParams): Promise { + return "// TODO: implements iframeSwitch"; + } + + async iframeReset(params: TypeIframeResetParams): Promise { + return "// TODO: implements iframeReset"; + } + + async quickSelector(params: TypeQuickSelectorParams): Promise { + return ""; + } + + async pressKey(params: TypePressKeyParams): Promise { + const keyboardKey = String(params.key).toUpperCase(); + + return `Actions actions = new Actions(driver); +actions.sendKeys(Keys.${keyboardKey}).perform();`; + } +}