Skip to content

Commit

Permalink
Add codecov integration
Browse files Browse the repository at this point in the history
  • Loading branch information
Romanow committed Sep 11, 2024
2 parents 3de3ca8 + 5afa6fa commit 14d379f
Show file tree
Hide file tree
Showing 27 changed files with 18,488 additions and 5 deletions.
53 changes: 49 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ on:
- master
jobs:
build:
name: Build and Publish
name: Build
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: actions/checkout@v4

Expand All @@ -21,9 +19,56 @@ jobs:
- name: Validate
uses: pre-commit/[email protected]

- name: Build project
- uses: docker/setup-buildx-action@v3

- name: Install playwright
run: npx playwright install-deps

- name: Build and Run Test Page
run: |
docker compose build
docker compose up -d --wait
- name: Build and Test
run: ./gradlew clean build

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: jar
path: build/libs/*.jar
retention-days: 1

- name: Stop containers
if: always()
continue-on-error: true
run: docker compose down -v

publish:
name: Publish
runs-on: ubuntu-latest
needs: build
permissions:
packages: write
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
distribution: "corretto"
java-version: "17"
cache: "gradle"

- uses: actions/download-artifact@v4
with:
name: jar
path: build/libs/

- name: Publish artefacts
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: ./gradlew publish
Expand Down
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
exclude: .key$
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
Expand Down
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,63 @@

[![Build project](https://github.com/Romanow/playwright-page-object/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/Romanow/playwright-page-object/actions/workflows/build.yml)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)

## Шаблон проектирования _Page Object_

_Page Object_ – это шаблон проектирования, который помогает инкапсулировать работу с отдельными элементами страницы, что
позволяет уменьшить количество кода и его поддержку. Если, к примеру, дизайн одной из страниц изменён, то нам нужно
будет переписать только соответствующий класс, описывающий эту страницу.

Основные преимущества:

* Разделение кода тестов и описания страниц.
* Объединение всех действий по работе с веб-страницей в одном месте.

_Page Object_ описывает действия над элементами страницы, при этом не выставляя наружу элементы страницы: т.е.
коммуникация со страницей выполняются внутри метода, тест лишь вызывает действия более высокого уровня (обычно это
хорошо ложится на шаги теста).

## Основные элементы Playwright

* `Page` – предоставляет метод для взаимодействия со страницей браузера.
* `Locator` – объект для поиска элемента на странице. Вычисляется в момент обращения к
элементу (`click()`, `isVisible()` и т.п.).

## Структура проекта

Т.к. страницы могут содержать большое количество элементов, для большей читабельности вводится понятие _Component
Object_ – элемент страницы. _Component Object_ на _Page Object_ описывается с помощью
аннотации [`@Component`](src/main/kotlin/ru/romanow/playwright/annotations/Component.kt) и инициализируется
автоматически с помощью `ComponentFactory` наравне с полями, помеченными аннотацией `@FindBy`.

### ComponentFactory

[`ComponentFactory`](src/main/kotlin/ru/romanow/playwright/ComponentFactory.kt) – фабрика для создания _Page Object_.
Принимает на вход `Page` (объект Playwright) и класс `PageObject`, который нужно создать. С помощью reflection обходит
аннотации `@FindBy` и `@Component`.

### Локаторы `@FindBy`

Локатор [`@FindBy`](src/main/kotlin/ru/romanow/playwright/annotations/FindBy.kt) используется для задания условия поиска
элемента на странице. В момент создания страницы элемент может отсутствовать на странице, поиск производится в момент
обращения к Location.

Есть 4 условия поиска, при этом если задано одно условие, то другие не выполняются (кроме `byRole` и `byText` – они
могут идти вместе). Условия поиска перечислены в порядке приоритета:

* `byTestId` – поиск по `data-testid`;
* `byCss` – поиск по CSS селектор;
* `byRole` – поиск по атрибуту `role`;
* `byXpath` – поиск XPath;
* `byText` – поиск по тексту (может применяться совместно с фильтром `byRole`);
* `parent` – используется, если нужно выделить поддерево, в рамках которого выполнять дальнейший поиск
элементов ([`Parent`](src/main/kotlin/ru/romanow/playwright/annotations/Parent.kt)`byCss`, `byTestId`, `byXpath`,
`byRole`).

## Соглашения о разработке

### Структура пакетов теста

* _utils_ – утильные инструменты, константы и т.п.
* _pages_ и _components_ – классы, описывающие _Page Object_ и_Component Object_.
* _tests_ – тесты, внутри нужно делить по функциональности.
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id "idea"
id "jacoco"
id "maven-publish"
id "org.jetbrains.kotlin.jvm" version "1.9.22"
id "org.jlleitschuh.gradle.ktlint" version "12.1.0"
Expand Down Expand Up @@ -51,6 +52,15 @@ test {
}
}

jacocoTestReport {
reports {
xml.required = true
html.required = false
}
}

check.dependsOn jacocoTestReport

publishing {
publications {
maven(MavenPublication) {
Expand Down
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
test-page:
build: test-page
image: "romanowalex/test-page:latest"
container_name: test-page
ports:
- "8080:80"
healthcheck:
test: "curl --fail http://localhost || exit 1"
interval: 5s
timeout: 3s
retries: 5
Binary file not shown.
3 changes: 2 additions & 1 deletion src/main/kotlin/ru/romanow/playwright/ComponentFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import ru.romanow.playwright.annotations.FindBy
import ru.romanow.playwright.annotations.Parent
import java.lang.reflect.Field
import kotlin.reflect.KClass
import kotlin.reflect.jvm.javaType

class ComponentFactory private constructor() {

companion object {
fun <T : BaseComponent> create(page: Page, cls: KClass<T>): T {
val constructor =
cls.constructors.first { it.parameters.size == 1 && it.parameters[0].type == Page::class }
cls.constructors.first { it.parameters.size == 1 && it.parameters[0].type.javaType == Page::class.java }
val element = constructor.call(page)

fillLocators(page, cls, element)
Expand Down
32 changes: 32 additions & 0 deletions src/test/kotlin/ru/romanow/playwright/pages/MainPage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ru.romanow.playwright.pages

import com.microsoft.playwright.Locator
import com.microsoft.playwright.Page
import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat
import org.assertj.core.api.Assertions.assertThat
import ru.romanow.playwright.BaseComponent
import ru.romanow.playwright.annotations.FindBy

class MainPage(page: Page) : BaseComponent(page) {

@FindBy(byTestId = "page-header")
private lateinit var pageHeader: Locator

@FindBy(byCss = "p[data-testid='description'] > ul > li")
private lateinit var elements: Locator

@FindBy(byXpath = "//p//li[last()]")
private lateinit var lastElement: Locator

fun assertFindByTestId(expected: String) {
assertThat(pageHeader).hasText(expected)
}

fun assertFindByCss() {
assertThat(elements.count()).isEqualTo(5)
}

fun assertFindByXpath() {
assertThat(lastElement).containsText("parent")
}
}
22 changes: 22 additions & 0 deletions src/test/kotlin/ru/romanow/playwright/tests/PageTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ru.romanow.playwright.tests

import com.microsoft.playwright.Page
import com.microsoft.playwright.junit.UsePlaywright
import org.junit.jupiter.api.Test
import ru.romanow.playwright.ComponentFactory
import ru.romanow.playwright.pages.MainPage
import ru.romanow.playwright.utils.BrowserOptions

@UsePlaywright(BrowserOptions::class)
internal class PageTest {

@Test
fun test(page: Page) {
page.navigate("/")
val mainPage = ComponentFactory.create(page, MainPage::class)

mainPage.assertFindByTestId("Тестовая страница")
mainPage.assertFindByCss()
mainPage.assertFindByXpath()
}
}
22 changes: 22 additions & 0 deletions src/test/kotlin/ru/romanow/playwright/utils/BrowserOptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ru.romanow.playwright.utils

import com.microsoft.playwright.Browser.NewContextOptions
import com.microsoft.playwright.BrowserType.LaunchOptions
import com.microsoft.playwright.junit.Options
import com.microsoft.playwright.junit.OptionsFactory
import ru.romanow.playwright.utils.PropertiesHelper.Companion.get

class BrowserOptions : OptionsFactory {
override fun getOptions(): Options {
return Options()
.also {
it.headless = get("headless-mode", Boolean::class)
it.contextOptions = NewContextOptions().also { opt ->
opt.baseURL = get("base-url")
}
it.launchOptions = LaunchOptions().also { opt ->
opt.slowMo = if (get("slow-mode", Boolean::class) == true) 2000.0 else 0.0
}
}
}
}
67 changes: 67 additions & 0 deletions src/test/kotlin/ru/romanow/playwright/utils/PropertiesHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package ru.romanow.playwright.utils

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import java.util.*
import kotlin.reflect.KClass

class PropertiesHelper private constructor() {
private var holder: MutableMap<String, String>

init {
val stream = object {}.javaClass.classLoader.getResourceAsStream("config.properties")
stream.use {
this.holder = HashMap<String, String>()
val properties = Properties().also { p -> p.load(it) }
val pattern = Regex("\\$\\{([^}]+)}")
for (key in properties.keys) {
var value = properties[key] as String
var matcher = pattern.find(value)
while (matcher != null) {
val env = matcher.groups[1]!!
var envValue = env.value
envValue = if (envValue.contains(":")) {
val parts = envValue.split(":".toRegex(), limit = 2)
System.getenv(parts[0]) ?: parts[1]
} else {
System.getenv(envValue) ?: ""
}
value = pattern.replaceFirst(value, envValue)
matcher = matcher.next()
}
holder[key.toString()] = value
}
}
}

companion object {
private val instance: PropertiesHelper by lazy { PropertiesHelper() }

@Suppress("UNCHECKED_CAST")
fun <T : Any> get(key: String, cls: KClass<T>): T? {
val value = instance.holder[key]
return when (cls) {
String::class -> value as T?
Boolean::class -> value.toBoolean() as T?
Int::class -> value?.toInt() as T?
Double::class -> value?.toDouble() as T?
else -> throw ClassCastException(
"PropertyHolder supports only String, Integer, Double and Boolean types"
)
}
}

fun get(key: String): String? {
return get(key, String::class)
}
}
}

internal class PropertiesHelperTest {

@Test
fun test() {
assertThat(PropertiesHelper.get("test.base-url", String::class)).isEqualTo("http://localhost:8080")
assertThat(PropertiesHelper.get("test.stop-on-error", Boolean::class)).isTrue
}
}
7 changes: 7 additions & 0 deletions src/test/resources/config.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
base-url=${BASE_HOST:http://localhost}:${BASE_PORT:8080}
headless-mode=${HEADLESS_MODE:true}
slow-mode=${SLOW_MODE:true}

# for test only
test.base-url=${TEST_BASE_HOST:http://localhost}:${TEST_BASE_PORT:8080}
test.stop-on-error=true
Empty file added test-page/.dockerignore
Empty file.
2 changes: 2 additions & 0 deletions test-page/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/node_modules
/build
11 changes: 11 additions & 0 deletions test-page/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM node:18 AS builder
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm install
COPY . ./
RUN npm run build

FROM nginx:1.21-alpine
COPY --from=builder /usr/src/app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Loading

0 comments on commit 14d379f

Please sign in to comment.