From ed4c642b0a63dd72ab2849c32a29e0be7443c023 Mon Sep 17 00:00:00 2001 From: Alexander-D-Karpov Date: Mon, 12 May 2025 23:21:14 +0300 Subject: [PATCH] added functional testing and gradle --- backend/build.gradle | 480 ++++++++++++++++++ backend/gradle.properties | 33 ++ .../WebApplicationFunctionalTest.java | 332 ++++++++++++ 3 files changed, 845 insertions(+) create mode 100644 backend/build.gradle create mode 100644 backend/gradle.properties create mode 100644 backend/src/main/java/ru/akarpov/web4/functional/WebApplicationFunctionalTest.java diff --git a/backend/build.gradle b/backend/build.gradle new file mode 100644 index 0000000..4ec6249 --- /dev/null +++ b/backend/build.gradle @@ -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 \ No newline at end of file diff --git a/backend/gradle.properties b/backend/gradle.properties new file mode 100644 index 0000000..02903f9 --- /dev/null +++ b/backend/gradle.properties @@ -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 \ No newline at end of file diff --git a/backend/src/main/java/ru/akarpov/web4/functional/WebApplicationFunctionalTest.java b/backend/src/main/java/ru/akarpov/web4/functional/WebApplicationFunctionalTest.java new file mode 100644 index 0000000..2f1e054 --- /dev/null +++ b/backend/src/main/java/ru/akarpov/web4/functional/WebApplicationFunctionalTest.java @@ -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 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]"))); + } +} \ No newline at end of file