added functional testing and gradle

This commit is contained in:
Alexander Karpov 2025-05-12 23:21:14 +03:00
parent babf31f4f1
commit ed4c642b0a
3 changed files with 845 additions and 0 deletions

480
backend/build.gradle Normal file
View File

@ -0,0 +1,480 @@
plugins {
id 'java'
id 'war'
id 'jacoco'
id 'org.sonarqube' version '3.5.0.2730'
id 'com.github.node-gradle.node' version '3.5.1'
}
group = 'ru.akarpov.web4'
version = '1.0-SNAPSHOT'
sourceCompatibility = '17'
// Load all properties from gradle.properties
def props = new Properties()
file("${projectDir}/gradle.properties").withInputStream { props.load(it) }
repositories {
mavenCentral()
}
dependencies {
// Jakarta EE
providedCompile "jakarta.platform:jakarta.jakartaee-api:${props.jakartaEeVersion}"
// Hibernate
implementation "org.hibernate.orm:hibernate-core:${props.hibernateVersion}"
// PostgreSQL
implementation "org.postgresql:postgresql:${props.postgresqlVersion}"
// JWT for authentication
implementation "com.auth0:java-jwt:${props.jwtVersion}"
// JSON-B
implementation "jakarta.json.bind:jakarta.json.bind-api:${props.jsonbVersion}"
// Swagger/OpenAPI
providedCompile "org.eclipse.microprofile.openapi:microprofile-openapi-api:${props.openapiVersion}"
implementation "io.swagger.core.v3:swagger-annotations:${props.swaggerVersion}"
implementation "io.swagger.core.v3:swagger-jaxrs2-jakarta:${props.swaggerVersion}"
implementation "org.webjars:swagger-ui:${props.swaggeruiVersion}"
// Testing
testImplementation "org.junit.jupiter:junit-jupiter-api:${props.junitVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${props.junitVersion}"
testImplementation "org.mockito:mockito-core:${props.mockitoVersion}"
testImplementation "org.mockito:mockito-junit-jupiter:${props.mockitoVersion}"
// Selenium for functional testing
testImplementation "org.seleniumhq.selenium:selenium-java:${props.seleniumVersion}"
testImplementation "io.github.bonigarcia:webdrivermanager:${props.webdriverManagerVersion}"
}
// Define the main Java source directory
sourceSets {
main {
java {
srcDirs = ['src/main/java']
}
resources {
srcDirs = ['src/main/resources']
}
}
test {
java {
srcDirs = ['src/test/java']
}
resources {
srcDirs = ['src/test/resources']
}
}
}
// Configure Node plugin for frontend build
node {
version = "${props.nodeVersion}"
npmVersion = "${props.npmVersion}"
download = true
nodeProjectDir = file("${projectDir}/frontend")
}
// Task 1: compile - Compile Java sources
task compile(type: JavaCompile) {
source = sourceSets.main.java.srcDirs
classpath = sourceSets.main.compileClasspath
destinationDir = sourceSets.main.java.outputDir
options.encoding = 'UTF-8'
}
// Task 2: build - Build the entire project
task buildProject(dependsOn: [compile, war, npmBuild]) {
description = 'Builds the entire project (backend and frontend)'
group = 'build'
}
// NPM tasks for frontend
task npmInstall(type: NpmTask) {
description = 'Installs all dependencies from package.json'
workingDir = file("${projectDir}/frontend")
args = ['install']
}
task npmBuild(type: NpmTask, dependsOn: npmInstall) {
description = 'Builds the frontend'
workingDir = file("${projectDir}/frontend")
args = ['run', 'build']
}
// Task to copy frontend build to backend webapp directory
task copyFrontendToBackend(type: Copy, dependsOn: npmBuild) {
from "${projectDir}/frontend/build"
into "${buildDir}/webapp"
// Preserve swagger-ui.html
doFirst {
mkdir "${buildDir}/temp"
if (file("${projectDir}/src/main/webapp/swagger-ui.html").exists()) {
copy {
from "${projectDir}/src/main/webapp/swagger-ui.html"
into "${buildDir}/temp"
}
}
}
doLast {
if (file("${buildDir}/temp/swagger-ui.html").exists()) {
copy {
from "${buildDir}/temp/swagger-ui.html"
into "${buildDir}/webapp"
}
}
}
}
// Task 3: clean - Clean build directories
clean {
description = 'Cleans all build directories and temporary files'
delete "${buildDir}"
delete "${projectDir}/frontend/build"
delete "${projectDir}/frontend/node_modules"
delete fileTree(dir: "${projectDir}/src/main/webapp", excludes: ["WEB-INF/**"])
}
// Configure war task to include frontend
war {
dependsOn copyFrontendToBackend
webAppDirectory = file("${buildDir}/webapp")
archiveName = "web-lab4.war"
manifest {
attributes(
'Implementation-Title': project.name,
'Implementation-Version': project.version,
'Main-Class': "${props.mainClass}"
)
}
}
// Task 4: test - Run JUnit tests
test {
useJUnitPlatform()
finalizedBy jacocoTestReport
// Exclude functional tests from regular test task
exclude '**/functional/**'
}
// Task 5: scp - Transfer built project to a server
task scp(dependsOn: buildProject) {
description = 'Transfers the built project to a remote server'
doLast {
exec {
commandLine 'scp', "${buildDir}/libs/web-lab4.war", "${props.remoteUser}@${props.remoteHost}:${props.remotePath}"
}
}
}
// Task 6: xml - Validate all XML files
task xml {
description = 'Validates all XML files in the project'
doLast {
fileTree(dir: projectDir, include: '**/*.xml').each { file ->
javaexec {
classpath = configurations.runtimeClasspath
main = 'javax.xml.validation.SchemaFactory'
args = [file.absolutePath]
}
}
}
}
// Task 7: doc - Add MD5 and SHA-1 to MANIFEST and generate JavaDoc
task doc(dependsOn: war) {
description = 'Adds MD5 and SHA-1 checksums to MANIFEST and generates JavaDoc'
doLast {
// Generate JavaDoc
exec {
workingDir projectDir
commandLine 'javadoc', '-d', "${buildDir}/docs/javadoc",
'-sourcepath', "${projectDir}/src/main/java",
'-subpackages', 'ru.akarpov.web4'
}
// Calculate checksums and add to MANIFEST
def warFile = file("${buildDir}/libs/web-lab4.war")
def md5 = warFile.digest('MD5')
def sha1 = warFile.digest('SHA-1')
war.manifest {
attributes(
'MD5-Checksum': md5,
'SHA1-Checksum': sha1
)
}
// Rebuild the WAR file with updated MANIFEST
war.execute()
// Add JavaDoc to the WAR file
ant.zip(destfile: warFile, update: true) {
zipfileset(dir: "${buildDir}/docs/javadoc", prefix: 'javadoc')
}
}
}
// Task 8: native2ascii - Convert localization files
task native2ascii {
description = 'Converts localization files using native2ascii'
doLast {
fileTree(dir: "${projectDir}/src/main/resources", include: '**/*.properties').each { file ->
def outputFile = new File(file.absolutePath + '.ascii')
exec {
commandLine 'native2ascii', '-encoding', 'UTF-8', file.absolutePath, outputFile.absolutePath
}
outputFile.renameTo(file)
}
}
}
// Task 9: music - Play music after build
task music(dependsOn: buildProject) {
description = 'Plays music after successful build'
doLast {
if (System.properties['os.name'].toLowerCase().contains('windows')) {
exec {
commandLine 'cmd', '/c', "start ${props.musicFile}"
}
} else if (System.properties['os.name'].toLowerCase().contains('mac')) {
exec {
commandLine 'afplay', "${props.musicFile}"
}
} else {
exec {
commandLine 'xdg-open', "${props.musicFile}"
}
}
}
}
// Task 10: report - Save JUnit report to XML and commit to Git
task report(dependsOn: test) {
description = 'Saves JUnit report to XML and commits to Git'
doLast {
if (!project.gradle.taskGraph.hasTask(':test') || tasks.test.state.failure == null) {
def reportFile = file("${buildDir}/test-results/test/TEST-junit-report.xml")
exec {
commandLine 'git', 'add', reportFile.absolutePath
}
exec {
commandLine 'git', 'commit', '-m', 'Add JUnit test report'
}
println "Test report committed to Git: ${reportFile.absolutePath}"
} else {
println "Tests failed, report not committed"
}
}
}
// Task 11: diff - Check working copy and commit if specified classes are not changed
task diff {
description = 'Checks working copy and commits if specified classes are not changed'
doLast {
def changedFiles = ""
exec {
commandLine 'git', 'status', '--porcelain'
standardOutput = new ByteArrayOutputStream()
changedFiles = standardOutput.toString().trim()
}
def excludedClasses = props.excludedClasses.split(',')
boolean canCommit = true
for (String changedFile : changedFiles.split('\n')) {
def fileName = changedFile.substring(3)
for (String excludedClass : excludedClasses) {
if (fileName.contains(excludedClass)) {
canCommit = false
break
}
}
}
if (canCommit && !changedFiles.isEmpty()) {
exec {
commandLine 'git', 'add', '.'
}
exec {
commandLine 'git', 'commit', '-m', 'Auto-commit: Changes not affecting excluded classes'
}
println "Changes committed to Git"
} else {
println "Changes affect excluded classes or no changes found, not committing"
}
}
}
// Task 12: team - Get previous revisions, build them and package as zip
task team {
description = 'Gets previous revisions, builds them and packages as zip'
doLast {
def currentDir = file("${buildDir}/team")
currentDir.mkdirs()
// Get the last 2 revisions
for (int i = 1; i <= 2; i++) {
def revisionDir = new File(currentDir, "revision-${i}")
revisionDir.mkdirs()
exec {
commandLine 'git', 'archive', "HEAD~${i}", '--format=tar',
'--output', "${revisionDir}/archive.tar"
}
exec {
workingDir revisionDir
commandLine 'tar', '-xf', 'archive.tar'
}
// Build the revision
exec {
workingDir revisionDir
commandLine './gradlew', 'build'
}
}
// Package the builds into a zip file
ant.zip(destfile: "${buildDir}/team-builds.zip") {
fileset(dir: "${buildDir}/team")
}
}
}
// Task 13: alt - Create alternative version with renamed variables and classes
task alt {
description = 'Creates alternative version with renamed variables and classes'
doLast {
def altDir = file("${buildDir}/alt")
copy {
from "${projectDir}/src"
into "${altDir}/src"
}
def replacements = [
'User': 'AppUser',
'Point': 'Coordinate',
'PointService': 'CoordinateService',
'UserService': 'AppUserService'
]
fileTree(dir: altDir, include: '**/*.java').each { file ->
def content = file.text
replacements.each { key, value ->
content = content.replaceAll(key, value)
}
file.text = content
}
// Build the alternative version
exec {
workingDir altDir
commandLine './gradlew', 'build'
}
}
}
// Task 14: history - Try to compile, if fails, get previous version from Git
task history {
description = 'If compilation fails, gets previous version from Git until a working one is found'
doLast {
boolean compileSuccess = false
int revision = 0
while (!compileSuccess && revision < 10) { // Limit to last 10 revisions
try {
if (revision > 0) {
// Checkout previous revision
exec {
commandLine 'git', 'checkout', "HEAD~${revision}"
}
}
// Try to compile
tasks.compile.execute()
compileSuccess = true
if (revision > 0) {
// Create diff file with the next revision (first broken one)
exec {
standardOutput = new FileOutputStream("${buildDir}/first-broken-diff.patch")
commandLine 'git', 'diff', "HEAD..HEAD~${revision-1}"
}
println "Found working revision at HEAD~${revision}, diff saved to first-broken-diff.patch"
}
} catch (Exception e) {
println "Compilation failed for revision HEAD~${revision}, trying older version"
revision++
}
}
// Restore to original revision
if (revision > 0) {
exec {
commandLine 'git', 'checkout', 'HEAD'
}
}
if (!compileSuccess) {
println "Could not find a working revision in the last 10 commits"
}
}
}
// Task 15: env - Build and run in alternative environments
task env {
description = 'Builds and runs the program in alternative environments'
doLast {
def environments = props.environments.split(';')
environments.each { env ->
def (javaVersion, vmArgs) = env.split(':')
println "Building and running with Java ${javaVersion} and VM args ${vmArgs}"
exec {
environment 'JAVA_HOME', "${props.javaHome}${javaVersion}"
commandLine './gradlew', 'build'
}
exec {
environment 'JAVA_HOME', "${props.javaHome}${javaVersion}"
commandLine "${props.javaHome}${javaVersion}/bin/java",
vmArgs.split(',') as List,
'-jar',
"${buildDir}/libs/web-lab4.war"
}
}
}
}
// Task for functional testing
task functionalTest(type: Test) {
description = 'Runs functional tests'
group = 'verification'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
// Only run tests in the functional package
include '**/functional/**'
// Set system property to identify functional tests
systemProperty 'test.type', 'functional'
// Report results
reports {
html.enabled = true
junitXml.enabled = true
}
}
// Integrate functional testing into the build process
build.dependsOn functionalTest

33
backend/gradle.properties Normal file
View File

@ -0,0 +1,33 @@
# Version configurations
jakartaEeVersion=10.0.0
hibernateVersion=6.2.7.Final
postgresqlVersion=42.6.0
jwtVersion=4.4.0
jsonbVersion=3.0.0
openapiVersion=3.1
swaggerVersion=2.2.15
swaggeruiVersion=5.10.3
junitVersion=5.9.1
mockitoVersion=5.3.1
seleniumVersion=4.9.1
webdriverManagerVersion=5.3.2
nodeVersion=18.16.0
npmVersion=9.5.1
# Application settings
mainClass=ru.akarpov.web4.MainClass
# Remote deployment settings
remoteHost=your-server.com
remoteUser=username
remotePath=/var/www/web-lab4
# Music file for 'music' task
musicFile=build/resources/main/success.wav
# Classes excluded from auto-commit
excludedClasses=ru.akarpov.web4.entity.User,ru.akarpov.web4.entity.Point
# Java environments for 'env' task
javaHome=/usr/lib/jvm/
environments=11:-Xmx512m;17:-Xmx1024m,-XX:+UseG1GC

View File

@ -0,0 +1,332 @@
package ru.akarpov.web4.functional;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.junit.jupiter.api.*;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
import java.util.List;
public class WebApplicationFunctionalTest {
private static WebDriver driver;
private static final String BASE_URL = "http://localhost:8080/web-lab4";
private static final String TEST_USERNAME = "testuser" + System.currentTimeMillis();
private static final String TEST_PASSWORD = "Password123";
@BeforeAll
public static void setUp() {
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless");
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
driver = new ChromeDriver(options);
driver.manage().window().maximize();
}
@AfterAll
public static void tearDown() {
if (driver != null) {
driver.quit();
}
}
@BeforeEach
public void navigateToHome() {
driver.get(BASE_URL);
}
@Test
@DisplayName("TC-01: User should be able to register successfully")
public void testUserRegistration() {
driver.findElement(By.xpath("//button[text()='Register']")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//form//input[@type='text']")));
String username = TEST_USERNAME;
driver.findElement(By.xpath("//form//input[@type='text']")).sendKeys(username);
driver.findElement(By.xpath("//form//input[@type='password'][1]")).sendKeys(TEST_PASSWORD);
driver.findElement(By.xpath("//form//input[@type='password'][2]")).sendKeys(TEST_PASSWORD);
driver.findElement(By.xpath("//button[text()='Register']")).click();
wait.until(ExpectedConditions.urlContains("/main"));
Assertions.assertTrue(driver.getCurrentUrl().contains("/main"));
}
@Test
@DisplayName("TC-02: User should be able to login with valid credentials")
public void testUserLogin() {
// Ensure user exists
try {
registerTestUser();
} catch (Exception e) {
// User might already exist, continue with login
}
driver.get(BASE_URL);
driver.findElement(By.xpath("//form//input[@type='text']")).sendKeys(TEST_USERNAME);
driver.findElement(By.xpath("//form//input[@type='password']")).sendKeys(TEST_PASSWORD);
driver.findElement(By.xpath("//button[contains(text(),'Login')]")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.urlContains("/main"));
Assertions.assertTrue(driver.getCurrentUrl().contains("/main"));
}
@Test
@DisplayName("TC-03: User should not be able to login with invalid credentials")
public void testInvalidLogin() {
driver.findElement(By.xpath("//form//input[@type='text']")).sendKeys("nonexistentuser");
driver.findElement(By.xpath("//form//input[@type='password']")).sendKeys("wrongpassword");
driver.findElement(By.xpath("//button[contains(text(),'Login')]")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//div[contains(@class, 'text-red-500')]")));
Assertions.assertFalse(driver.getCurrentUrl().contains("/main"));
WebElement errorMessage = driver.findElement(By.xpath("//div[contains(@class, 'text-red-500')]"));
Assertions.assertTrue(errorMessage.isDisplayed());
}
@Test
@DisplayName("TC-04: User should be able to add a point through the form")
public void testAddPointViaForm() {
loginUser();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//form//input[@type='number']")));
driver.findElement(By.xpath("//form//input[@type='number'][1]")).clear();
driver.findElement(By.xpath("//form//input[@type='number'][1]")).sendKeys("1.5");
driver.findElement(By.xpath("//form//input[@type='number'][2]")).clear();
driver.findElement(By.xpath("//form//input[@type='number'][2]")).sendKeys("2.0");
driver.findElement(By.xpath("//form//input[@type='number'][3]")).clear();
driver.findElement(By.xpath("//form//input[@type='number'][3]")).sendKeys("2.0");
driver.findElement(By.xpath("//button[contains(text(), 'Add Point')]")).click();
// Wait for point to appear in table
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//table//tbody/tr[1]/td[1]")));
String xValue = driver.findElement(By.xpath("//table//tbody/tr[1]/td[1]")).getText();
String yValue = driver.findElement(By.xpath("//table//tbody/tr[1]/td[2]")).getText();
Assertions.assertEquals("1.5", xValue);
Assertions.assertEquals("2.0", yValue);
}
@Test
@DisplayName("TC-05: User should be able to add a point by clicking on the graph")
public void testAddPointViaClick() {
loginUser();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.tagName("canvas")));
WebElement canvas = driver.findElement(By.tagName("canvas"));
// Get number of points before clicking
int pointsBeforeClick = countTableRows();
// Click center of canvas to add a point
Actions actions = new Actions(driver);
actions.moveToElement(canvas).click().perform();
// Wait for a new point to appear in the table
wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(
By.xpath("//table//tbody/tr"), pointsBeforeClick));
int pointsAfterClick = countTableRows();
Assertions.assertTrue(pointsAfterClick > pointsBeforeClick,
"Point should be added to the table after clicking on canvas");
}
@Test
@DisplayName("TC-06: User should be able to change the R value")
public void testChangeRValue() {
loginUser();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//form//input[@type='number']")));
// Change R value
driver.findElement(By.xpath("//form//input[@type='number'][3]")).clear();
driver.findElement(By.xpath("//form//input[@type='number'][3]")).sendKeys("3.0");
// Add a point to verify R was applied
driver.findElement(By.xpath("//form//input[@type='number'][1]")).clear();
driver.findElement(By.xpath("//form//input[@type='number'][1]")).sendKeys("1.0");
driver.findElement(By.xpath("//form//input[@type='number'][2]")).clear();
driver.findElement(By.xpath("//form//input[@type='number'][2]")).sendKeys("1.0");
driver.findElement(By.xpath("//button[contains(text(), 'Add Point')]")).click();
// Wait for point to appear in table
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//table//tbody/tr[1]/td[3]")));
String rValue = driver.findElement(By.xpath("//table//tbody/tr[1]/td[3]")).getText();
Assertions.assertEquals("3.0", rValue);
}
@Test
@DisplayName("TC-07: Point hit status should be displayed correctly")
public void testPointHitStatus() {
loginUser();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
// Add a point inside the area (quarter circle, x=1, y=-1, r=2)
addTestPoint(1.0, -1.0, 2.0);
// Verify hit status is Yes (point should be inside the quarter circle)
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//table//tbody/tr[1]/td[4]")));
String hitStatus = driver.findElement(By.xpath("//table//tbody/tr[1]/td[4]")).getText();
Assertions.assertEquals("Yes", hitStatus);
// Add a point outside the area (x=3, y=3, r=2)
addTestPoint(3.0, 3.0, 2.0);
// Verify hit status is No (second row now)
wait.until(ExpectedConditions.numberOfElementsToBe(By.xpath("//table//tbody/tr"), 2));
hitStatus = driver.findElement(By.xpath("//table//tbody/tr[1]/td[4]")).getText();
Assertions.assertEquals("No", hitStatus);
}
@Test
@DisplayName("TC-08: User should be able to clear all points")
public void testClearAllPoints() {
loginUser();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//form//input[@type='number']")));
// Add a point first
addTestPoint(1.0, 1.0, 2.0);
// Verify point was added
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//table//tbody/tr[1]/td[1]")));
// Clear all points
WebElement clearButton = driver.findElement(By.xpath("//button[contains(text(), 'Clear All')]"));
clearButton.click();
// Wait for table to be empty or show "No points added yet" message
wait.until(ExpectedConditions.or(
ExpectedConditions.textToBePresentInElementLocated(
By.xpath("//table//tbody/tr/td"), "No points added yet"),
ExpectedConditions.numberOfElementsToBe(
By.xpath("//table//tbody/tr[not(contains(., 'No points'))]"), 0)
));
// Check if table is empty or shows the message
List<WebElement> tableRows = driver.findElements(By.xpath("//table//tbody/tr[not(contains(., 'No points'))]"));
boolean tableEmpty = tableRows.isEmpty() || tableRows.get(0).getText().contains("No points");
Assertions.assertTrue(tableEmpty, "Table should be empty after clearing points");
}
@Test
@DisplayName("TC-09: User should be able to logout")
public void testLogout() {
loginUser();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.urlContains("/main"));
// Find and click logout button
driver.findElement(By.xpath("//button[contains(text(), 'Logout')]")).click();
// Verify redirect to login page
wait.until(ExpectedConditions.urlContains(BASE_URL));
Assertions.assertFalse(driver.getCurrentUrl().contains("/main"));
// Try to access main page directly
driver.get(BASE_URL + "/#/main");
// Should be redirected back to login page
wait.until(ExpectedConditions.not(ExpectedConditions.urlContains("/main")));
Assertions.assertFalse(driver.getCurrentUrl().contains("/main"));
}
// Helper methods
private void registerTestUser() {
driver.get(BASE_URL);
driver.findElement(By.xpath("//button[text()='Register']")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//form//input[@type='text']")));
driver.findElement(By.xpath("//form//input[@type='text']")).sendKeys(TEST_USERNAME);
driver.findElement(By.xpath("//form//input[@type='password'][1]")).sendKeys(TEST_PASSWORD);
driver.findElement(By.xpath("//form//input[@type='password'][2]")).sendKeys(TEST_PASSWORD);
driver.findElement(By.xpath("//button[text()='Register']")).click();
wait.until(ExpectedConditions.or(
ExpectedConditions.urlContains("/main"),
ExpectedConditions.visibilityOfElementLocated(By.xpath("//div[contains(@class, 'text-red-500')]"))
));
}
private void loginUser() {
// Create test user if doesn't exist
try {
registerTestUser();
} catch (Exception e) {
// User might already exist, continue with login
}
driver.get(BASE_URL);
driver.findElement(By.xpath("//form//input[@type='text']")).sendKeys(TEST_USERNAME);
driver.findElement(By.xpath("//form//input[@type='password']")).sendKeys(TEST_PASSWORD);
driver.findElement(By.xpath("//button[contains(text(),'Login')]")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.urlContains("/main"));
}
private int countTableRows() {
return driver.findElements(By.xpath("//table//tbody/tr[not(contains(., 'No points'))]")).size();
}
private void addTestPoint(double x, double y, double r) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//form//input[@type='number']")));
driver.findElement(By.xpath("//form//input[@type='number'][1]")).clear();
driver.findElement(By.xpath("//form//input[@type='number'][1]")).sendKeys(String.valueOf(x));
driver.findElement(By.xpath("//form//input[@type='number'][2]")).clear();
driver.findElement(By.xpath("//form//input[@type='number'][2]")).sendKeys(String.valueOf(y));
driver.findElement(By.xpath("//form//input[@type='number'][3]")).clear();
driver.findElement(By.xpath("//form//input[@type='number'][3]")).sendKeys(String.valueOf(r));
driver.findElement(By.xpath("//button[contains(text(), 'Add Point')]")).click();
// Wait for point to appear in table
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//table//tbody/tr[1]/td[1]")));
}
}