commit 17bd3b79177d14615ac6007ce67b58a889adf6a9 Author: Yevhen Unico Date: Sun Dec 1 20:14:27 2024 +0200 Completed Test Task diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2c0be8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.idea/workspace.xml +*.xml +!/**/assembly.xml +!/**/pom.xml +!/**/*log4j2*.xml +!/**/extent-config.xml +tests/allure-results +tests/resources/videos +buid/ +target/ +gradle/ +.idea/ +path/ +out/ +*.iml +*.log \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9ae7e59 --- /dev/null +++ b/build.gradle @@ -0,0 +1,61 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' version '1.8.20' +} + +def java_version = 17 +java { + sourceCompatibility = JavaVersion.toVersion(java_version) + targetCompatibility = JavaVersion.toVersion(java_version) +} + +tasks.withType(KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = java_version.toString() + } +} + +dependencies { + implementation 'com.microsoft.playwright:playwright:1.34.0' + implementation 'org.testng:testng:7.8.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.20' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.8.20' + implementation 'io.cucumber:cucumber-testng:7.14.0' + implementation 'io.cucumber:cucumber-java:7.14.0' + implementation 'org.slf4j:slf4j-api:1.7.32' + implementation 'ch.qos.logback:logback-classic:1.2.6' + implementation 'org.projectlombok:lombok:1.18.30' + implementation 'org.jsoup:jsoup:1.15.4' + implementation 'org.springframework:spring-context:6.0.9' + implementation 'org.testng:testng:7.8.0' + implementation("io.rest-assured:rest-assured:5.1.1") +} + +repositories { + mavenLocal() + mavenCentral() + google() + maven { + url 'https://repo.maven.apache.org/maven2/' + } + maven { + url "https://plugins.gradle.org/m2" + } + maven { + url "https://repo1.maven.org/maven2/" + } +} + +test { + systemProperty('encryptionKey', System.getProperty('encryptionKey')) + useTestNG() { + listeners << 'core.listeners.TestNGListener' + suites('src/test/resources/testng.xml') + } + jvmArgs('--add-opens', 'java.base/java.lang=ALL-UNNAMED') + jvmArgs('--add-opens', 'java.base/java.lang.invoke=ALL-UNNAMED') + jvmArgs('--add-opens', 'java.base/java.nio=ALL-UNNAMED') +} diff --git a/src/main/kotlin/core/BaseApi.kt b/src/main/kotlin/core/BaseApi.kt new file mode 100644 index 0000000..c1d6ab8 --- /dev/null +++ b/src/main/kotlin/core/BaseApi.kt @@ -0,0 +1,20 @@ +package core + +import io.restassured.RestAssured +import io.restassured.http.ContentType +import io.restassured.response.Response + +class BaseApi { + fun getRequest(url: String): Response { + return RestAssured.given() + .contentType(ContentType.JSON) + .get(url) + } + + fun postRequest(url: String, body: Map): Response { + return RestAssured.given() + .contentType(ContentType.JSON) + .body(body) + .post(url) + } +} \ No newline at end of file diff --git a/src/main/kotlin/core/listeners/TestNGListener.kt b/src/main/kotlin/core/listeners/TestNGListener.kt new file mode 100644 index 0000000..f9f2971 --- /dev/null +++ b/src/main/kotlin/core/listeners/TestNGListener.kt @@ -0,0 +1,33 @@ +package core.listeners + +import core.web.browser.InitialBrowser +import org.testng.ISuiteListener +import org.testng.ITestContext +import org.testng.ITestListener +import org.testng.ITestResult + +class TestNGListener : ITestListener, ISuiteListener { + override fun onTestStart(result: ITestResult?) { + } + + override fun onTestSuccess(result: ITestResult?) { + InitialBrowser.getInstance().destroy() + } + + override fun onTestFailure(result: ITestResult?) { + InitialBrowser.getInstance().destroy() + } + + override fun onTestSkipped(result: ITestResult?) { + } + + override fun onTestFailedButWithinSuccessPercentage(result: ITestResult?) { + } + + override fun onStart(context: ITestContext?) { + InitialBrowser.getInstance() + } + + override fun onFinish(context: ITestContext?) { + } +} diff --git a/src/main/kotlin/core/properties/PropertiesReader.kt b/src/main/kotlin/core/properties/PropertiesReader.kt new file mode 100644 index 0000000..2b97d08 --- /dev/null +++ b/src/main/kotlin/core/properties/PropertiesReader.kt @@ -0,0 +1,30 @@ +package core.properties + +import java.io.IOException +import java.util.* + +class PropertiesReader { + private val urlProperties = URLProperties() + + init { + val urlPropertiesFile = "urls.properties" + urlProperties.loadFromProperties(loadProperty(urlPropertiesFile)) + } + + fun urlsProperties(): URLProperties { + return urlProperties + } + + private fun loadProperty(fileName: String?): Properties { + val properties = Properties() + try { + PropertiesReader::class.java.getResourceAsStream("/$fileName").use { inputStream -> + properties.load(inputStream) + } + } catch (e: IOException) { + throw RuntimeException(e) + } + return properties + } +} + diff --git a/src/main/kotlin/core/properties/URLProperties.kt b/src/main/kotlin/core/properties/URLProperties.kt new file mode 100644 index 0000000..b01625b --- /dev/null +++ b/src/main/kotlin/core/properties/URLProperties.kt @@ -0,0 +1,10 @@ +package core.properties + +import java.util.* + +class URLProperties { + lateinit var exampleURL: String + fun loadFromProperties(properties: Properties) { + exampleURL = properties.getProperty("exampleURL") + } +} diff --git a/src/main/kotlin/core/utils/api/ApiOpertations.kt b/src/main/kotlin/core/utils/api/ApiOpertations.kt new file mode 100644 index 0000000..cb331ea --- /dev/null +++ b/src/main/kotlin/core/utils/api/ApiOpertations.kt @@ -0,0 +1,20 @@ +package core.utils.api + +import io.restassured.response.Response +import org.testng.Assert + +class ApiOpertations { + fun assertStatusCode(response: Response, expectedStatusCode: Int) { + Assert.assertEquals(response.statusCode, expectedStatusCode, "Status code should be $expectedStatusCode") + } + + fun assertFieldNotNull(response: Response, field: String) { + val value = response.jsonPath().getString(field) + Assert.assertNotNull(value, "$field field should not be null") + } + + fun assertIdGreaterThanZero(response: Response) { + val id = response.jsonPath().getInt("id") + Assert.assertTrue(id > 0, "ID should be greater than 0") + } +} \ No newline at end of file diff --git a/src/main/kotlin/core/utils/helpers/Logger.kt b/src/main/kotlin/core/utils/helpers/Logger.kt new file mode 100644 index 0000000..d86eac8 --- /dev/null +++ b/src/main/kotlin/core/utils/helpers/Logger.kt @@ -0,0 +1,48 @@ +package core.utils.helpers + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.reflect.KClass + +// Return logger for name +fun T.logger(name: String): Logger { + return LoggerFactory.getLogger(name) +} + + +// Return logger for Java class, if companion object fix the name +fun logger(forClass: Class): Logger { + return LoggerFactory.getLogger(unwrapCompanionClass(forClass)) +} + +// unwrap companion class to enclosing class given a Java Class +fun unwrapCompanionClass(ofClass: Class): Class<*> { + return ofClass.enclosingClass?.takeIf { + ofClass.enclosingClass.kotlin.objectInstance?.javaClass == ofClass + } ?: ofClass +} + +// unwrap companion class to enclosing class given a Kotlin Class +fun unwrapCompanionClass(ofClass: KClass): KClass<*> { + return unwrapCompanionClass(ofClass.java).kotlin +} + +// Return logger for Kotlin class +fun logger(forClass: KClass): Logger { + return logger(forClass.java) +} + +// return logger from extended class (or the enclosing class) +fun T.logger(): Logger { + return logger(this.javaClass) +} + +// return a lazy logger property delegate for enclosing class +fun R.lazyLogger(): Lazy { + return lazy { logger(this.javaClass) } +} + +// return a logger property delegate for enclosing class +fun R.injectLogger(): Lazy { + return lazyOf(logger(this.javaClass)) +} \ No newline at end of file diff --git a/src/main/kotlin/core/web/browser/InitialBrowser.kt b/src/main/kotlin/core/web/browser/InitialBrowser.kt new file mode 100644 index 0000000..081f74e --- /dev/null +++ b/src/main/kotlin/core/web/browser/InitialBrowser.kt @@ -0,0 +1,95 @@ +package core.web.browser + +import com.microsoft.playwright.Page +import com.microsoft.playwright.Playwright +import core.exeptions.BrowserException +import core.utils.helpers.logger +import java.nio.file.Paths + +object InitialBrowser { + private const val browserName = "CHROME" + private val browserThread = ThreadLocal() + + fun getInstance(): InitialBrowser = browserThread.get() ?: synchronized(this) { + browserThread.get() ?: InitialBrowser.also { browserThread.set(it) } + } + + private var page: Page? = null + + fun getPage(): Page { + if (page == null) { + page = initialBrowser() + page?.context()?.pages()?.get(0)?.close() + if (logger().isDebugEnabled) { + logger().debug("Browser: " + page?.context()?.browser()?.browserType()?.name()) + } + } + return page ?: throw IllegalStateException("Page is null") + } + + fun setPage(page: Page) { + this.page = page + } + + private fun initialBrowser(): Page { + if (logger().isDebugEnabled) { + logger().debug("Initializing browser...") + } + return when (browserName) { + "CHROME" -> { + if (logger().isDebugEnabled) { + logger().debug("Creating driver instance: Chrome local") + } + val playwright = Playwright.create() + val context = playwright.chromium().launchPersistentContext( + Paths.get(""), + Options().browserOptions() + ) + val page = context.newPage() + setPage(page) + page + } + + else -> throw BrowserException("Browser was not set") + } + } + + fun destroy() { + page?.let { + try { + if (logger().isDebugEnabled) { + logger().debug("Closing page...") + } + it.close() + if (logger().isDebugEnabled) { + logger().debug("Closing all context pages...") + } + it.context().pages().forEach { page -> page.close() } + val browser = it.context().browser() + if (browser != null) { + if (logger().isDebugEnabled) { + logger().debug("Closing browser...") + } + browser.close() + } else { + if (logger().isWarnEnabled) { + logger().warn("Browser is null, cannot close it.") + } + } + if (logger().isDebugEnabled) { + logger().debug("Driver destroyed successfully.") + } + } catch (e: Exception) { + if (logger().isErrorEnabled) { + logger().error("Error during driver destruction", e) + } + } finally { + page = null + } + } ?: run { + if (logger().isDebugEnabled) { + logger().debug("Driver destroy called with no driver present.") + } + } + } +} diff --git a/src/main/kotlin/core/web/browser/Options.kt b/src/main/kotlin/core/web/browser/Options.kt new file mode 100644 index 0000000..c47aedb --- /dev/null +++ b/src/main/kotlin/core/web/browser/Options.kt @@ -0,0 +1,20 @@ +package core.web.browser + +import com.microsoft.playwright.BrowserType + +class Options { + fun browserOptions(): BrowserType.LaunchPersistentContextOptions { + val type = BrowserType.LaunchPersistentContextOptions() + val launchOptions: MutableList = ArrayList() + launchOptions.add("--start-maximized") + launchOptions.add("--disable-gpu") + type.setHeadless(false) + .setArgs(launchOptions) + .setTimeout(30000.0) + .setDevtools(false) + .setChromiumSandbox(false) + .setViewportSize(null) + .setAcceptDownloads(true) + return type + } +} \ No newline at end of file diff --git a/src/main/kotlin/core/web/elements/Window.kt b/src/main/kotlin/core/web/elements/Window.kt new file mode 100644 index 0000000..1c20887 --- /dev/null +++ b/src/main/kotlin/core/web/elements/Window.kt @@ -0,0 +1,16 @@ +package core.web.elements + +import com.microsoft.playwright.Locator +import com.microsoft.playwright.Page +import core.utils.helpers.logger +import core.web.browser.InitialBrowser +import kotlinx.coroutines.runBlocking + +class Window : Element() { + + fun navigateTo(url: String) { + logger().info("Url: $url") + page.navigate(url) + } + +} diff --git a/src/main/kotlin/projects/example/ExamplePageStepDefs.kt b/src/main/kotlin/projects/example/ExamplePageStepDefs.kt new file mode 100644 index 0000000..6735aac --- /dev/null +++ b/src/main/kotlin/projects/example/ExamplePageStepDefs.kt @@ -0,0 +1,14 @@ +package projects.example + +import core.properties.PropertiesReader +import core.web.elements.Window +import io.cucumber.java.en.Given + +class ExamplePageStepDefs { + private val window: Window = Window() + + @Given("^user is on example URL$") + fun openExampleURL() { + window.navigateTo(PropertiesReader().urlsProperties().exampleURL) + } +} \ No newline at end of file diff --git a/src/main/resources/urls.properties b/src/main/resources/urls.properties new file mode 100644 index 0000000..2e3433b --- /dev/null +++ b/src/main/resources/urls.properties @@ -0,0 +1 @@ +exampleURL=https://example.com \ No newline at end of file diff --git a/src/test/kotlin/ApiTestTask.kt b/src/test/kotlin/ApiTestTask.kt new file mode 100644 index 0000000..dc2535d --- /dev/null +++ b/src/test/kotlin/ApiTestTask.kt @@ -0,0 +1,29 @@ +import core.BaseApi +import core.utils.api.ApiOpertations +import org.testng.annotations.Test + +class ApiTestTask { + private val baseApi = BaseApi() + private val validator = ApiOpertations() + + @Test + fun assertGetRequestSuccessfulAndContainsTitleField() { + val response = baseApi.getRequest("https://jsonplaceholder.typicode.com/posts/1") + + validator.assertStatusCode(response, 200) + validator.assertFieldNotNull(response, "title") + + println("GET request successful. Title: ${response.jsonPath().getString("title")}") + } + + @Test + fun assertPostRequestCreatesNewResource() { + val payload = mapOf("title" to "foo", "body" to "bar", "userId" to 1) + val response = baseApi.postRequest("https://jsonplaceholder.typicode.com/posts", payload) + + validator.assertStatusCode(response, 201) + validator.assertIdGreaterThanZero(response) + + println("POST request successful. Created resource ID: ${response.jsonPath().getInt("id")}") + } +} \ No newline at end of file diff --git a/src/test/kotlin/UITestTask.kt b/src/test/kotlin/UITestTask.kt new file mode 100644 index 0000000..1c33057 --- /dev/null +++ b/src/test/kotlin/UITestTask.kt @@ -0,0 +1,9 @@ +import org.testng.annotations.Test +import projects.example.ExamplePageStepDefs + +class UITestTask { + @Test + fun openExampleUrl() { + ExamplePageStepDefs().openExampleURL() + } +} \ No newline at end of file