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
+
+ 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();`;
+ }
+}