added functional testing and gradle
This commit is contained in:
parent
babf31f4f1
commit
ed4c642b0a
480
backend/build.gradle
Normal file
480
backend/build.gradle
Normal 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
33
backend/gradle.properties
Normal 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
|
|
@ -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]")));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user