diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml
index 8723fbaa7..bf2cf1e51 100644
--- a/.github/workflows/hugo.yml
+++ b/.github/workflows/hugo.yml
@@ -2,7 +2,7 @@ name: Deploy Hugo site to Pages
on:
push:
- branches: ["master", "design"]
+ branches: ["master", "multilingual"]
workflow_dispatch:
permissions:
@@ -19,7 +19,7 @@ defaults:
shell: bash
jobs:
- build:
+ build-en:
runs-on: ubuntu-latest
steps:
- name: Setup Hugo
@@ -32,13 +32,16 @@ jobs:
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
- - name: Build with Hugo
+ - name: Build EN site
env:
HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache
HUGO_ENVIRONMENT: production
TZ: America/New_York
run: |
- hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/"
+ hugo --minify \
+ --config hugo.toml,hugo-en.toml \
+ --baseURL "${{ steps.pages.outputs.base_url }}/" \
+ --destination public
# Preserve /rss URL (without .xml extension)
cp public/rss.xml public/rss
- name: Upload artifact
@@ -46,13 +49,43 @@ jobs:
with:
path: ./public
- deploy:
+ deploy-en:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
- needs: build
+ needs: build-en
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+
+ build-and-deploy-ru:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Setup Hugo
+ uses: peaceiris/actions-hugo@v3
+ with:
+ hugo-version: '0.160.1'
+ extended: true
+ - name: Checkout
+ uses: actions/checkout@v6
+ - name: Build RU site
+ env:
+ HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache
+ HUGO_ENVIRONMENT: production
+ TZ: America/New_York
+ run: |
+ hugo --minify \
+ --config hugo.toml,hugo-ru.toml \
+ --destination public-ru
+ # Preserve /rss URL (without .xml extension)
+ cp public-ru/rss.xml public-ru/rss
+ - name: Deploy to ru.selenide.org
+ uses: peaceiris/actions-gh-pages@v4
+ with:
+ personal_token: ${{ secrets.RU_SELENIDE_ORG_DEPLOY_TOKEN }}
+ external_repository: selenide/selenide-ru
+ publish_branch: gh-pages
+ publish_dir: ./public-ru
+ cname: ru.selenide.org
diff --git a/.gitignore b/.gitignore
index 48138616f..de7874a84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
# Hugo build output
public/*
!public/javadoc
+public-en/
+public-ru/
resources/_gen/
# OS files
diff --git a/CLAUDE.md b/CLAUDE.md
index 8145c4b49..6fb0b4b7d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2,11 +2,14 @@
## Project overview
-Official website for **Selenide** (selenide.org) — a Java UI test automation framework built on Selenium WebDriver. This is a Hugo static site hosted on GitHub Pages from the `selenide/selenide.github.io` repository.
+Official website for **Selenide** — a Java UI test automation framework built on Selenium WebDriver.
+This single multilingual Hugo project powers both:
+- `selenide.org` (English) — hosted on `selenide/selenide.github.io`
+- `ru.selenide.org` (Russian) — hosted on `selenide/selenide-ru` (gh-pages branch)
## Tech stack
-- **Hugo** static site generator
+- **Hugo** static site generator (multilingual)
- **Goldmark** for Markdown rendering (with `unsafe: true` for raw HTML in content)
- **Go templates** for layouts
- **jQuery 3.6.0** + jQuery UI 1.13.1 (from CDN)
@@ -15,59 +18,57 @@ Official website for **Selenide** (selenide.org) — a Java UI test automation f
## Directory structure
```
-hugo.toml Main Hugo config
+hugo.toml Main Hugo config (both languages)
+hugo-en.toml Override for EN-only production build
+hugo-ru.toml Override for RU-only production build
+i18n/
+ en.toml English UI strings
+ ru.toml Russian UI strings
content/
- _index.md Homepage (content in layouts/index.html)
- blog/ Blog posts (185+ files), _index.md is the blog list
- documentation/ _index.md + sub-pages (page-objects, screenshots, reports, clouds, selenide-vs-selenium)
- quick-start.md, faq.md, users.md, quotes.md, contacts.md, javadoc.md, thanks.md
+ en/ English content (homepage, blog, docs, etc.)
+ ru/ Russian content
layouts/
_default/ baseof.html, single.html, list.html, users.html
- blog/ single.html (post), list.html (blog index with year/month grouping)
- partials/ donate.html, main-menu.html, documentation-menu.html, quicklinks.html, title.html, analytics.html
+ blog/ single.html (post), list.html (blog index)
+ partials/ donate.html, main-menu.html, quicklinks.html, title.html, analytics.html
shortcodes/ selenide-version.html, selenium-changelog.html, documentation-menu.html
- index.html Homepage template
+ index.html Homepage template (uses i18n)
404.html Custom 404
assets/
themes/ingmar/css/ CSS files (processed via Hugo Pipes for fingerprinting)
static/
- images/ Logos, screenshots — year-based subdirs (images/2024/, etc.)
- assets/themes/ingmar/js/ JavaScript files
- test-page/ HTML test pages for Selenide demos
+ images/ Logos, screenshots — year-based subdirs
+ assets/themes/ingmar/js/ JavaScript
CNAME, favicon.ico, robots.txt
data/
users.json Companies using Selenide
- user_tags.json Tags for filtering users page
+ user_tags.json Tags for users page
```
-## Key config (hugo.toml)
-
-- `params.selenideVersion = "7.16.0"` — used via `{{* selenide-version */>}}` shortcode in content
-- `params.seleniumChangelog` — Selenium changelog URL, used via `{{* selenium-changelog */>}}` shortcode
-- Blog permalinks: `/:year/:month/:day/:title/`
-- Pages use `url:` in front matter for explicit URLs (e.g., `/quick-start.html`)
-- `buildFuture = true` — builds future-dated posts
-- `markup.goldmark.renderer.unsafe = true` — allows raw HTML in Markdown
-
## Local development
```bash
-# Install Hugo (macOS)
brew install hugo
+./start.sh # serves both languages: EN at /, RU at /ru/
+```
+
+## Production builds (each language at root)
+
+```bash
+# EN site (selenide.org)
+hugo --config hugo.toml,hugo-en.toml --destination public-en
-# Serve locally
-./start.sh
-# or manually:
-hugo server --buildFuture --port 4001
+# RU site (ru.selenide.org)
+hugo --config hugo.toml,hugo-ru.toml --destination public-ru
```
-Output goes to `public/` directory.
+Each override toml sets `disableLanguages` and `defaultContentLanguage` so the chosen language sits at the site root.
## Creating content
### New blog post
-Create `content/blog/YYYY-MM-DD-slug-name.md`:
+Create under `content/en/blog/` or `content/ru/blog/` as `YYYY-MM-DD-slug.md`:
```yaml
---
@@ -78,53 +79,42 @@ category:
headerText: "Short tagline"
tags: []
---
-
-Content here...
```
### New page
-Create in `content/` with `url:` for the desired path:
+Create under `content/en/` or `content/ru/` with `url:` for the path:
```yaml
---
title: "Page Title"
-header: "Display Header"
-cssClass: my-class
url: /page-name.html
-headerText: "Subtitle"
---
```
## Shortcodes
-- `{{* selenide-version */>}}` — outputs current Selenide version from `hugo.toml`
-- `{{* selenium-changelog */>}}` — outputs Selenium changelog URL
-- `{{* documentation-menu */>}}` — renders docs sidebar menu
+- `{{* selenide-version */>}}` — current Selenide version from `hugo.toml`
+- `{{* selenium-changelog */>}}` — Selenium changelog URL
+- `{{* documentation-menu */>}}` — renders docs sidebar menu (localized)
-## Conventions
+## Internationalization
-- Blog posts are mostly Selenide release announcements, following the pattern "Released Selenide X.Y.Z"
-- Posts use anchor-linked table of contents at the top for sections
-- Images go in `static/images/` — use year-based subdirectories (e.g., `images/2026/`)
-- Company logos for the users page go in `static/images/`
-- Pages that use `layout: users` get the custom users template with data-driven content
-- When releasing a new Selenide version: update `params.selenideVersion` in `hugo.toml`, create a release blog post
+- UI strings live in `i18n/en.toml` and `i18n/ru.toml`, accessed via `{{ T "key" }}`
+- Page content lives in language-specific `content/en/` and `content/ru/` trees
+- Layouts branch on `.Site.Language.Lang` only when the structure itself differs (e.g. docs menu has extra links in RU)
-## Layout hierarchy
+## Conventions
-```
-baseof.html (HTML shell, head, header, footer)
- └── block "main" filled by:
- ├── index.html (homepage)
- ├── blog/single.html (blog posts)
- ├── blog/list.html (blog index)
- ├── _default/single.html (regular pages)
- ├── _default/users.html (users page)
- └── 404.html
-```
+- Blog posts are mostly release announcements: "Released Selenide X.Y.Z" / "Вышла Selenide X.Y.Z"
+- Release posts should be added in BOTH `content/en/blog/` and `content/ru/blog/`
+- Images go in `static/images/` — use year-based subdirectories
+- When releasing a new Selenide version: update `params.selenideVersion` in `hugo.toml`, create release blog posts in both languages
## Deployment
-The site deploys to GitHub Pages. The `static/CNAME` file sets the custom domain `selenide.org`.
-A GitHub Actions workflow runs `hugo` and deploys the `public/` directory.
+GitHub Actions (`.github/workflows/hugo.yml`):
+1. **EN job** — builds EN site and deploys via `actions/deploy-pages` to this repo's Pages (selenide.org)
+2. **RU job** — builds RU site and pushes via `peaceiris/actions-gh-pages` to `selenide/selenide-ru` repo's `gh-pages` branch (ru.selenide.org)
+
+The RU cross-repo push requires a `RU_SELENIDE_ORG_DEPLOY_TOKEN` secret (PAT with push access to `selenide/selenide-ru`).
diff --git a/content/_index.md b/content/en/_index.md
similarity index 100%
rename from content/_index.md
rename to content/en/_index.md
diff --git a/content/archive.md b/content/en/archive.md
similarity index 81%
rename from content/archive.md
rename to content/en/archive.md
index b69bea2cd..a45fc7c54 100644
--- a/content/archive.md
+++ b/content/en/archive.md
@@ -1,6 +1,6 @@
---
title: "Selenide blog"
-url: /archive.html
+url: archive.html
---
+#{/if}
+```
+
+Естественно, приведённый код зависит от вашего фреймворка, приложения, проекта и т.д.
+Цвет и толщина рамки - от вашего дизайна. Возможно даже, это должна быть не рамка, а что-то другое.
+Главное, чтобы было видно, какой элемент был реально кликнут.
+
+Здесь можно посмотреть один из примеров реализации: [Highlighter](https://github.com/selenide-examples/gmail/blob/master/test/org/selenide/examples/gmail/Highlighter.java).
+
+## Что теперь?
+
+Если у вас есть мигающие тесты (а они есть у всех) - начните с этого шага, и вы хотя бы увидите, куда попал клик.
+А дальше уже будем разбираться.
+
+
+
+
+
+
+[Андрей Солнцев](http://asolntsev.github.io/)
+
+ru.selenide.org
diff --git a/content/ru/blog/2019-12-07-advent-calendar-csrf-protection.md b/content/ru/blog/2019-12-07-advent-calendar-csrf-protection.md
new file mode 100644
index 000000000..ddd26911f
--- /dev/null
+++ b/content/ru/blog/2019-12-07-advent-calendar-csrf-protection.md
@@ -0,0 +1,201 @@
+---
+slug: "advent-calendar-csrf-protection"
+date: 2019-12-07
+title: "Как протестировать защиту от CSRF атаки"
+description: ""
+category:
+headerText: "Selenide Advent Calendar День 7"
+tags: []
+---
+Привет!
+
+Сегодня 7 день рождественского календаря Selenide.
+И сегодня мы поговорим о тестировании безопасности.
+
+# Что такое CSRF?
+
+Одна из самых распространённых атак - это CSRF (Cross-Site Request Forgery), или подделка межсайтовых запросов.
+Подробно о ней я рассказывал в видосике [WTF Security](https://www.youtube.com/watch?v=z-aEjd22BGU), а сейчас для нас
+важно то, что защиту от этой атаки легко протестировать вашими обычными автотестами.
+
+Для того, чтобы веб-приложение было защищено от CSRF-атак, с каждым его POST-запросом должен посылаться один хитрый
+параметр. Он обычно называется `authenticityToken` (хотя и не обязательно). Когда вы заходите в одной вкладке, скажем,
+ в свой интернет-банк, а в другой вкладке на сайт с котиками, зловредный код на этом сайте может послать POST-запрос вашему
+ банку для совершения платежа на счёт хакера. Хакер может послать счёт и сумму, а также все cookies из вашей вкладки, но
+ он не сможет послать `authenticityToken` (потому, что он уникальный для каждой сессии и не хранится в cookies).
+
+А типичная ошибка такая: веб-приложение либо не посылает `authenticityToken` на сервер с каким-то POST-запросом,
+либо на сервере не проверяет пришедший токен.
+
+
+
+### Короче, как проверить защищённость?
+
+У вас уже есть куча автотестов, покрывающих весь критичный функционал вашего веб-приложения.
+Мы убьём двух зайцев разом: во время запуска этих тестов мы будем перехватывать каждый POST-запрос и посылать точно такой
+же, но с изменённым `authenticityToken`. И будем проверять, что сервер вернул ошибку. Обычно это ошибка 403 Forbidden.
+
+
+
+### Звучит сложно. Как это закодить?
+
+Не так уж сложно.
+Как вы знаете, селенид может запускать свой встроенный прокси-сервер. Изначально он использовался для скачивания файлов,
+но к нему можно добавлять и свои "листенеры", которые могут перехватывать все запросы между браузером и тестируемым приложением.
+Это мы и сделаем.
+
+
+
+#### Шаг 1. Включаем селенидовский прокси-сервер
+
+```java
+Configuration.proxyEnabled = true;
+```
+
+(это нужно сделать ДО открытия браузера)
+
+
+
+#### Шаг 2. Добавляем листенер для прокси-сервера
+
+```java
+abstract class BaseTest {
+ private AuthenticityTokenChecker authenticityTokenChecker = new AuthenticityTokenChecker();
+
+ // и где-то сразу после open("http://..."):
+ getSelenideProxy().getProxy().addRequestFilter(authenticityTokenChecker);
+}
+```
+
+На данный момент это можно сделать только ПОСЛЕ открытия браузера, что иногда не очень удобно.
+Я надеюсь, в следующей версии селенида мы сможем сделать так, чтобы листенеры для прокси-сервера можно было добавлять в любой момент.
+
+
+
+#### Шаг 3. Реализуем AuthenticityTokenChecker
+
+```java
+import com.codeborne.selenide.Configuration;
+import io.netty.handler.codec.http.*;
+import net.lightbody.bmp.filters.*;
+import net.lightbody.bmp.util.*;
+
+public class AuthenticityTokenChecker implements RequestFilter {
+ private final HttpClient httpClient = HttpClient.newBuilder().build();
+
+ private final List unprotectedUrls = new ArrayList<>(1);
+
+ public void reset() {
+ unprotectedUrls.clear();
+ }
+
+ public List getUnprotectedUrls() {
+ return unprotectedUrls;
+ }
+
+ @Override
+ public HttpResponse filterRequest(HttpRequest httpRequest, HttpMessageContents contents, HttpMessageInfo httpMessageInfo) {
+ if (httpRequest.getMethod() != HttpMethod.POST) return null; // игнорируем не-POST запросы
+ if (!httpRequest.getUri().startsWith(Configuration.baseUrl)) return null; // игнорируем запросы хрома к google.com и подобным ресурсами
+ if (Этому урлу разрешено и без токена) return null; // некоторым post-запросам не требуется защита
+
+ String body = contents.getTextContents();
+ if (!body.contains("authenticityToken=")) {
+ unprotectedUrls.add("No 'authenticityToken=' found for " + httpRequest.getUri() + " in " + body);
+ return null;
+ }
+
+ sendHackedPostRequest(httpRequest, contents);
+ return null;
+ }
+}
+```
+
+Обратите внимание: `return null;` значит "не изменяй запрос". То есть браузер пошлёт изначальный запрос на сервер без изменений,
+и нормальное течение вашего теста не будет нарушено.
+
+
+
+#### Шаг 4. Посылаем хакнутый POST-запрос
+
+```java
+
+ private void sendHackedPostRequest(HttpRequest httpRequest, HttpMessageContents contents) throws IOException, InterruptedException {
+ // Над этой строчкой придётся поработать.
+ // Формат запроса (и даже имя параметра "authenticityToken") может зависеть от вашего приложения.
+ // Обратите внимание, что параметров "authenticityToken" может быть несколько (сразу кидайте ошибку, если они разные).
+ // Если в POST-запросе сабмитится форма, да ещё и с файлами, параметр "authenticityToken" придётся выцепить немножко по-другому.
+ String hackedBody = contents.getTextContents()
+ .replace("authenticityToken=1234567890").на("authenticityToken=hack-me-if-you-can");
+
+ java.net.http.HttpRequest.Builder builder = java.net.http.HttpRequest.newBuilder()
+ .uri(URI.create(httpRequest.getUri()))
+ .timeout(Duration.ofSeconds(1));
+
+ for (Map.Entry header : httpRequest.headers()) {
+ if (!restrictedHeaders.contains(header.getKey().toLowerCase())) {
+ builder.header(header.getKey(), header.getValue());
+ }
+ }
+
+ java.net.http.HttpRequest request = builder
+ .POST(java.net.http.HttpRequest.BodyPublishers.ofString(hackedBody))
+ .build();
+
+ log.info("Sending hacked request to {}", httpRequest.getUri());
+
+ java.net.http.HttpResponse httpResponse = httpClient.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
+
+ if (httpResponse.statusCode() == 403) {
+ log.info("Hacked request was rejected: {} {}", httpResponse.statusCode(), httpRequest.getUri());
+ }
+ else {
+ log.error("HACK SUCCEEDED {} {}", httpResponse.statusCode(), httpRequest.getUri());
+ unprotectedUrls.add("Detected URL without authenticity token check: " + httpRequest.getUri());
+ }
+ }
+
+ private static final Set restrictedHeaders = Set.of("connection", "content-length",
+ "date", "expect", "from", "host", "upgrade", "via", "warning");
+
+```
+Конкретно эта реализация использует `HttpClient` из Java 11, но если вы из тех бедолаг, что до сих пор сидят на Java 8,
+вы можете заменить его на OkHttp, Apache Http Client или что-то подобное.
+
+
+
+#### Шаг 5. Валим тест, если нашлись незащищённые запросы
+
+```java
+abstract class BaseTest {
+ @Before void resetChecker() {
+ authenticityTokenChecker.reset();
+ }
+
+ @After
+ public void verifyThatAllPostRequestsAreProtectedWithAuthenticityToken() {
+ if (!authenticityTokenChecker.getUnprotectedUrls().isEmpty()) {
+ fail(String.valueOf(authenticityTokenChecker.getUnprotectedUrls()));
+ }
+ }
+}
+```
+
+
+
+## Что теперь?
+
+Мы убили двух зайцев и научились автоматически проверять защиту от CSRF-атак при запуске наших обычных автотестов.
+Это не фантазия, мы реально так сделали на одном проекте и нашли две серьёзных уязвимости в настоящем интернет-банке.
+
+Хорошо, но этого мало. На свете ещё куча атак.
+
+Пересмотрите [WTF Security](https://www.youtube.com/watch?v=z-aEjd22BGU), почитывайте [OWASP 10](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project)
+и мыслите креативно, а как ещё можно убить третьего и четвёртого зайца вашими автотестами.
+
+
+
+
+[Андрей Солнцев](http://asolntsev.github.io/)
+
+ru.selenide.org
diff --git a/content/ru/blog/2019-12-09-advent-calendar-statics.md b/content/ru/blog/2019-12-09-advent-calendar-statics.md
new file mode 100644
index 000000000..85d7c63af
--- /dev/null
+++ b/content/ru/blog/2019-12-09-advent-calendar-statics.md
@@ -0,0 +1,152 @@
+---
+slug: "advent-calendar-statics"
+date: 2019-12-09
+title: "Почему статики запретили, а потом разрешили?"
+description: ""
+category:
+headerText: "Selenide Advent Calendar День 9"
+tags: []
+---
+Привет!
+
+9 день рождественского календаря Selenide.
+Расскажу о том, что сильнее всего волнует общественность.
+
+# Почему статики запретили в 5.0.0, а потом снова разрешили?
+
+Короткий ответ: запретили случайно. Но правильно сделали.
+
+А теперь подробнее.
+
+### Коротко о хранении вебдрайверов в селениде
+
+Селенид изначально хранил вебдрайверы в ThreadLocal.
+Это позволяет запускать тесты параллельно: в разных потоках - разные вебдрайверы. Код выглядит примерно так:
+
+```java
+class WebDriverRunner {
+ private static final ThreadLocal webdriver = new ThreadLocal<>();
+ private static final ThreadLocal proxy = new ThreadLocal<>();
+}
+
+```
+
+Скажем, метод `$("a").click()` работает примерно так:
+
+```java
+ WebDriverRunner.webdriver.get().findElement("a").click();
+```
+
+## Коротко про `SelenideDriver`
+
+Но ThreadLocal накладывает ограничение: ты не можешь использовать в одном потоке два вебдрайвера
+(а также в двух потоках - один и тот же вебдрайвер, но до этого мы пока не дошли).
+
+Это мы и собирались решить в Selenide 5.0.0. Мы сделали специальный класс `SelenideDriver`, позволяющий использовать два
+вебдрайвера в одном тесте:
+
+```java
+SelenideDriver browser1 = new SelenideDriver();
+SelenideDriver browser2 = new SelenideDriver();
+
+browser1.open("https://google.com");
+browser2.open("http://yandex.ru");
+
+browser1.$(h1).shouldHave(text("Google"));
+browser2.$(h1).shouldHave(text("Yandeks"));
+```
+
+Для этого пришлось сделать большой рефакторинг, так чтобы внутри самого селенида нигде не использовались старые добрые
+статические методы `open()`, `$` и `$$`. Везде, где нужен вебдрайвер, селенид теперь использовал параметр `SelenideDriver`.
+Да-да, пришлось его прокидывать во все методы как параметр.
+
+
+## И вот тут главный вопрос
+
+А что делать со старыми добрыми статическими методами `open()`, `$` и `$$`? Они должны откуда-то взять инстанс `SelenideDriver` и передать
+его дальше в кишки селенида. Откуда его взять?
+
+
+## Selenide 5.0.0: Статикам наносят удар
+
+На тот момент этот вопрос решили так (как оказалось, это решение сильно повлияло на ход дел).
+В класс `WebDriverRunner` добавили третий ThreadLocal:
+
+```java
+ private static final ThreadLocal selenideDriver = new ThreadLocal<>();
+```
+
+Сам `SelenideDriver` - это, грубо говоря, очень простой класс с двумя полями `WebDriver` и `SelenideProxyServer`.
+И в целом всё хорошо работало и решало поставленную задачу.
+
+Чего я не мог предусмотреть на тот момент - это того, что многие люди определяли веб-элементы в **статических** полях:
+
+```java
+public class MyPageObject {
+ public static SelenideElement fname = $("#fname");
+ public static SelenideElement lname = $("#lname");
+}
+```
+
+Да ещё и переоткрывали браузер между тестами.
+
+
+### Важный нюанс:
+в Selenide 5.0.0 мы сделали ещё одно улучшение.
+Раньше селенид автоматически открывал новый браузер, если его ещё нет, что часто вызывало недоумение, ибо браузер
+иногда открывался в очень неожиданные моменты (например, при попытке залогировать ошибку, произошедшую из-за закрэшившегося браузера).
+
+Конечно, это происходило из-за плохого кода тестов, но _мы же создали селенид, чтобы помогать людям, верно?_
+Поэтому с версии 5.0.0 селенид стал чётко говорить: "Браузера нет, работать не могу. Вызови сначала метод `open()`, тогда поговорим."
+
+
+### И что же свалилось?
+
+Совпадение этих двух факторов привело к следующей проблеме:
+1. Человек создаёт статическую переменную `static SelenideElement fname = $("#fname")`.
+2. Этот `fname` запоминает, с каким инстансом `SelenideDriver` он был создан.
+3. Человек в конце теста закрывает браузер
+4. В следующем тесте человек открывает новый браузер и снова обращается к статическому полю `fname`.
+5. А `fname` обращается к своему `SelenideDriver`, с которым он был изначально создан.
+6. И кидает ошибку, потому что тот браузер уже закрылся.
+
+Больше года - с сентября 2018 до октября 2019 - я пытался объяснить всем, что статические переменные - это зло, не надо их использовать.
+Сделал даже специально доклад ["Антистатик"](https://www.youtube.com/watch?v=4JJNccWtdNI).
+
+Но в итоге сдался из тех соображений, что слишком много людей, которым пришлось бы переписывать свои тесты.
+Даже если они согласны насчёт злостности статических переменных, переписать всё - это зачастую неподъёмная задача.
+_Мы же создали селенид, чтобы помогать людям, верно?_
+
+
+## Selenide 5.4.0: Статики победили
+
+Как в итоге решили эту проблему?
+
+Довольно просто. Мы заменили `ThreadLocal` на статическую переменную (да, вы не ослышались :))
+
+```java
+private static final SelenideDriver = new ThreadLocalSelenideDriver();
+```
+
+Теперь наш "статический" `SelenideDriver` - один на всех. Он никогда не закрывается. Все статические поля, созданные с
+ним, будут жить вечно. Но он и не хранит в себе поля `WebDriver` и `SelenideProxyServer`. Он каждый берёт их из
+тредлокалов `WebDriverRunner`.
+
+
+
+### P.S.
+Поэтому в версии 5.4.0 пропал метод `WebDriverRunner.getSelenideDriver()`.
+
+Я очень удивился, что многие люди уже успели его использовать. Люди, я вас не понимаю! _Как вы умудряетесь всё так неправильно использовать?_
+Ок, я не подумав, сделал его публичным. Моя ошибка. Но он не упомянут ни в каких пресс-релизах, ни в какой документации.
+Как могла кому-то прийти в голову идея его использовать? Как эта магия вообще происходит?
+
+## Что теперь?
+
+Мы снова вернули возможность объявлять `SelenideElement` в статических полях.
+Но пожалуйста, не злоупотребляйте этим.
+Я всё ещё это не одобряю. :)
+
+[Андрей Солнцев](http://asolntsev.github.io/)
+
+ru.selenide.org
diff --git a/content/ru/blog/2019-12-10-advent-calendar-download-files.md b/content/ru/blog/2019-12-10-advent-calendar-download-files.md
new file mode 100644
index 000000000..4bd75bbe2
--- /dev/null
+++ b/content/ru/blog/2019-12-10-advent-calendar-download-files.md
@@ -0,0 +1,97 @@
+---
+slug: "advent-calendar-download-files"
+date: 2019-12-10
+title: "Как скачать файл с помощью Selenide"
+description: ""
+category:
+headerText: "Selenide Advent Calendar День 10"
+tags: []
+---
+Добрый вечер!
+
+На дворе декабрь, и в сегодняшнем посте рождественского календаря Selenide мы поговорим о том, какие возможности
+для скачивания файлов есть в Selenide.
+
+**UPD**
+Ниже описаны только два способа скачивания - [`HTTPGET`](#HTTPGET) и [`PROXY`](#PROXY).
+Позже появились [третий способ `FOLDER`](/2020/07/08/selenide-5.13.0/#new-file-download-mode-folder) и [четвёртый способ `CDP`](/2024/02/07/selenide-7.1.0/#download-files-with-cdp).
+Возможно, вам нужен как раз один из них, если у вашей ссылки нет атрибута `href`, и прокси у вас не заводится.
+
+
+
+# Как я могу скачать файл в моём тесте?
+
+В какой-то момент нашей карьеры каждый из нас сталкивается с необходимостью скачать какой-то файл в тесте.
+
+Как мы помним, в Selenium это было непросто, потому что для разных браузеров требуются разные настройки.
+Например, вот так выглядит создание профиля Firefox с нужными настройками:
+
+```java
+profile.setPreference("browser.download.dir", downloadPath);
+profile.setPreference("browser.download.folderList", 2);
+profile.setPreference("browser.download.manager.showWhenStarting", false);
+profile.setPreference("browser.helperApps.alwaysAsk.force", false);
+profile.setPreference("browser.helperApps.neverAsk.saveToDisk", mimeTypes);
+profile.setPreference("browser.download.manager.focusWhenStarting",false);
+profile.setPreference("browser.download.manager.useWindow", false);
+profile.setPreference("browser.download.manager.showAlertOnComplete", false);
+profile.setPreference("pdfjs.disabled", true);
+```
+
+### А в Selenide {#HTTPGET}
+Проблема решается гораздо проще - методом `$.download()`.
+
+Чтобы скачать файл, в Selenide достаточно просто вызвать метод:
+
+```java
+File report = element.download();
+```
+
+И Selenide автоматически сделает всё, что надо. Вам не придётся возиться со всплывающим окошком, которое спрашивает,
+куда сохранить файл, и потом закрывать его.
+
+Selenide сохранит скачанный файл в папку `build/reports/tests`. Это та же папка, где Gradle генерирует результаты прогона тестов,
+так что их как раз удобно видеть вместе.
+
+Конечно, поменять эту папку тоже можно:
+
+```java
+Configuration.downloadsFolder = ;
+```
+
+### НО: {#PROXY}
+Таким образом можно скачивать файлы только со ссылкой с атрибутом "href".
+
+Но что, если у меня ссылки с атрибутом "href"? Так бывает, например, когда файл скачивается в результате сабмита формы.
+В этом случае можно скачивать файлы с помощью встроенного в селенид прокси-сервера.
+
+Для начала нам нужно включить его (т.к. он по умолчанию выключен):
+
+```java
+Configuration.proxyEnabled = true;
+Configuration.fileDownload = PROXY;
+```
+
+После этого мы снова можем вызывать метод `$.download()`, но теперь он стал более могущественным и не требует наличия атрибута "href":
+
+```java
+File report = element.download();
+```
+
+### Хозяйке на заметку:
+Не забудьте увеличить таймаут, если собираетесь скачивать файл большого размера.
+
+Файл будет скачан в папку по умолчанию (что-то типа `C:\downloads and settings\downloads`).
+Таким образом, скачанный файл окажется в двух местах: `c:\downloads...` и `build/reports/tests`.
+
+Если это для вас проблема, можете в конце теста удалить ненужную папку, чтобы очистить место на диске:
+
+```java
+FileUtils.deleteDirectory(new File(<папка, подлежащая удалению>));
+```
+
+Узнать подробнее про механизмы скачивания файлов можно [тут](https://ru.selenide.org/2016/08/27/selenide-3.9.1/).
+
+
+
+Maciej Grymuza (figrym@gmail.com)
diff --git a/content/ru/blog/2019-12-12-advent-calendar-actions.md b/content/ru/blog/2019-12-12-advent-calendar-actions.md
new file mode 100644
index 000000000..eea37ff5e
--- /dev/null
+++ b/content/ru/blog/2019-12-12-advent-calendar-actions.md
@@ -0,0 +1,90 @@
+---
+slug: "advent-calendar-actions"
+date: 2019-12-12
+title: "Actions"
+description: ""
+category:
+headerText: "Selenide Advent Calendar День 12"
+tags: []
+---
+Привет!
+
+В сегодняшнем выпуске рождественского календаря мы рассмотрим, как можно использовать "действия" (Actions) в Selenide.
+
+Иногда при написании автотестов мы сталкиваемся со странными проблемами. Уверен на 100%, каждый из нас испытывал или
+будет испытывать необычные проблемы, которые блокируют нашу работу.
+Например, у нас часто не получается кликнуть на какой-то элемент, и стандартная селениумовская/селенидовская команда типа
+
+```java
+ element.click();
+```
+
+не работает. Причин, по которым клик может не срабатывать - множество. Но мы не можем сдаться просто так, мы должны
+найти какое-то решение. В Selenium для таких случаев есть класс `Actions`, который позволяет выполнить клик иначе:
+
+```java
+ WebElement element = ;
+ Actions actions = new Actions(driver);
+ actions.moveToElement(element);
+ actions.click();
+ actions.build().perform();
+```
+
+Этот вариант иногда срабатывает там, где обычный клик бессилен.
+
+#### Но как сделать это в Selenide?
+
+Оказывается, в Selenide это ещё проще, чем в Selenium (как, собственно, и всё в Selenide :)).
+В Selenide тоже есть `Actions`:
+
+```java
+ SelenideElement element = $();
+ actions().moveToElement(element).click(element).perform();
+```
+
+Здесь `actions()` - это один из тех методов, которые вы можете магически подключить волшебным импортом:
+
+```java
+ import static com.codeborne.selenide.Selenide.*;
+```
+
+Заметить, чтобы использовать `actions()`, не нужен webdriver!
+
+##### Чумачечий drag and drop
+
+Если вы читали документацию, вы знаете, что в Selenide по умолчанию есть два типа операций "drag and drop":
+
+1. `dragAndDropTo(java.lang.String targetCssSelector);`
+2. `dragAndDropTo(org.openqa.selenium.WebElement target);`
+
+Первый метод перетаскивает элемент в цель по CSS локатору. Второй - в другой WebElement.
+
+Но что, если мы не знаем точно, в какой элемент нужно перетащить?
+Допустим, у нас есть просто пустая страница, и мы хотим перетащить несколько объектов в разные места на этой странице.
+И тут снова на помощь приходят `Actions`. В Selenium мы бы сделали это примерно так:
+
+```java
+ WebElement element = driver.findElement(By.some);
+ Actions actions = new Actions(driver);
+ actions.dragAndDropBy(element, xOffset, yOffset).perform();
+```
+
+Где `xOffset` и `yOffset` - сдвиг по горизонтали и вертикали.
+
+В Selenide это выглядит чуть короче:
+
+```java
+ SelenideElement element = ;
+ actions().dragAndDropBy(element, xOffset, yOffset).perform();
+```
+
+Таким образом мы можем перетащить элемент в любую точку, даже не зная локатора цели.
+
+## Что дальше?
+
+Конечно же, это только пара примеров использования `actions()` в Selenide, и вы можете экспериментировать и находить другие варианты.
+
+Наслаждайтесь `actions()`!
+
+Maciej Grymuza (figrym@gmail.com)
+
diff --git a/content/ru/blog/2019-12-15-advent-calendar-drag-and-drop.md b/content/ru/blog/2019-12-15-advent-calendar-drag-and-drop.md
new file mode 100644
index 000000000..bb03678a3
--- /dev/null
+++ b/content/ru/blog/2019-12-15-advent-calendar-drag-and-drop.md
@@ -0,0 +1,33 @@
+---
+slug: "advent-calendar-drag-and-drop"
+date: 2019-12-15
+title: "Drag and Drop"
+description: ""
+category:
+headerText: "Selenide Advent Calendar День 15"
+tags: []
+---
+Привет!
+
+В сегодняшнем выпуске рождественского календаря мы посмотрим короткое, но весёлое видео о том, как перетаскивать элементы в Selenide.
+
+### Селенид умеет перетаскивать элементы?
+
+Да, в селениде есть метод Drag'n'Drop. Вот скучное описание из блога селенида:
+
+```java
+ $("#from").dragAndDropTo("#to")
+```
+
+
+А вот весёленькое описание от Martin Škarbala:
+
+
+
+Вот это подача!
+
+
+[Андрей Солнцев](http://asolntsev.github.io/)
+
+ru.selenide.org
+
diff --git a/content/ru/blog/2019-12-16-advent-calendar-browser-logs.md b/content/ru/blog/2019-12-16-advent-calendar-browser-logs.md
new file mode 100644
index 000000000..21a3f2b90
--- /dev/null
+++ b/content/ru/blog/2019-12-16-advent-calendar-browser-logs.md
@@ -0,0 +1,120 @@
+---
+slug: "advent-calendar-browser-logs"
+date: 2019-12-16
+title: "Как получить логи браузера"
+description: ""
+category:
+headerText: "Selenide Advent Calendar День 16"
+tags: []
+---
+Привет!
+
+Мы продолжаем наш рождественский календарь.
+На сей раз мы посмотрим, как можно взглянуть хрому под вкладку "developer tools".
+Это на случай, если вы хотите понять, какие ошибки писались и какие сетевые запросы летели из тестируемого приложения во время прогона тестов.
+
+> UPD Позже был реализован метод, который работает и в Firefox, и в Chromium браузерах:
+> [Собираем логи браузера с помощью DevTools/BiDi](/2025/10/29/selenide-7.12.0/#browser-logs-with-bidi)
+
+
+
+
+Chromedriver предлагает следующий рецепт.
+
+### 1. Добавить щепотку строк при открытии браузера:
+
+```java
+LoggingPreferences logPrefs = new LoggingPreferences();
+logPrefs.enable(LogType.BROWSER, Level.ALL);
+logPrefs.enable(LogType.PERFORMANCE, Level.ALL);
+capabilities.setCapability("goog:loggingPrefs", logPrefs);
+```
+
+До какой-то версии эта _капабилитя_ называлась "loggingPrefs", потом переименовали в "goog:loggingPrefs".
+Не знаю, как в других браузерах.
+
+Кстати, помимо `BROWSER` и `PERFORMANCE`, есть и другие типы логов, но у меня они как-то нестабильно работали, да я
+ и пользы в них не увидел. Знаете больше? Делитесь!
+
+
+### 2. В конце теста снять пенку с логов:
+
+```java
+Logs logs = getWebDriver().manage().logs();
+printLog(logs.get(LogType.BROWSER));
+
+void printLog(LogEntries entries) {
+ logger.info("{} log entries found", entries.getAll().size());
+ for (LogEntry entry : entries) {
+ logger.info("{} {} {}",
+ new Date(entry.getTimestamp()), entry.getLevel(), entry.getMessage()
+ );
+ }
+ }
+```
+
+### 3. Блюдо подаётся к отчёту примерно в таком виде:
+
+```java
+BROWSER logs:
+
+Mon Dec 16 19:29:42 EET 2019 SEVERE http://localhost:9126/page/image/payment-promo-campaign-ozon.png - Failed to load resource: the server responded with a status of 404 (Not Found)
+Mon Dec 16 19:49:14 EET 2019 INFO console-api 19:16 "start loading loans"
+Mon Dec 16 19:49:14 EET 2019 INFO console-api 21:18 "loaded loans"
+```
+
+Здесь видны все логи, что есть обычно в Developer Tools -> Console. В том числе сообщения `console.log` и ошибки JavaScript.
+
+
+### 4. Для гурманов можно подать десерт
+
+```json
+PERFORMANCE logs:
+
+{"message":{"method":"Network.loadingFinished","params":{"encodedDataLength":0,"requestId":"2C9E49BC49DCD3CA6EA9644255E34DE5","shouldReportCorbBlocking":false,"timestamp":141439.076528}},"webview":"FF1A4E4EAAD7143749CD3740DF9BB95F"}
+{"message":{"method":"Page.loadEventFired","params":{"timestamp":141439.234207}},"webview":"FF1A4E4EAAD7143749CD3740DF9BB95F"}
+{"message":{"method":"Page.frameStoppedLoading","params":{"frameId":"FF1A4E4EAAD7143749CD3740DF9BB95F"}},"webview":"FF1A4E4EAAD7143749CD3740DF9BB95F"}
+{"message":{"method":"Page.domContentEventFired","params":{"timestamp":141439.234834}},"webview":"FF1A4E4EAAD7143749CD3740DF9BB95F"}
+{"message":{"method":"Page.frameResized","params":{}},"webview":"FF1A4E4EAAD7143749CD3740DF9BB95F"}
+...
+{"message":{"method":"Network.dataReceived","params":{"dataLength":0,"encodedDataLength":327,"requestId":"58583.71","timestamp":141474.021635}},"webview":"FF1A4E4EAAD7143749CD3740DF9BB95F"}
+{"message":{"method":"Network.loadingFinished","params":{"encodedDataLength":586,"requestId":"58583.71","shouldReportCorbBlocking":false,"timestamp":141473.994219}},"webview":"FF1A4E4EAAD7143749CD3740DF9BB95F"}
+```
+
+### Плюс
+
+Каждая запись - это валидный JSON, его вполне можно парсить и анализировать прямо в тестах.
+
+Вот так выглядит отформатированная первая запись:
+
+```json
+{
+ "message":{
+ "method":"Network.loadingFinished",
+ "params":{
+ "encodedDataLength":0,
+ "requestId":"2C9E49BC49DCD3CA6EA9644255E34DE5",
+ "shouldReportCorbBlocking":false,
+ "timestamp":141439.076528
+ }
+ },
+ "webview":"FF1A4E4EAAD7143749CD3740DF9BB95F"
+}
+```
+
+### Минус:
+
+* Что-то понять из этих логов сложно. Нужно строить поверх какие-то анализаторы.
+* Здесь нет тела запроса.
+
+
+
+## Что теперь?
+
+В следующий раз вы изучим другие возможности получить логи - со статусами и телами запросов.
+
+
+
+[Андрей Солнцев](http://asolntsev.github.io/)
+
+ru.selenide.org
diff --git a/content/ru/blog/2019-12-17-advent-calendar-browser-logs-with-js.md b/content/ru/blog/2019-12-17-advent-calendar-browser-logs-with-js.md
new file mode 100644
index 000000000..be56e3d40
--- /dev/null
+++ b/content/ru/blog/2019-12-17-advent-calendar-browser-logs-with-js.md
@@ -0,0 +1,66 @@
+---
+slug: "advent-calendar-browser-logs-with-js"
+date: 2019-12-17
+title: "Как получить логи браузера через JavaScript"
+description: ""
+category:
+headerText: "Selenide Advent Calendar День 17"
+tags: []
+---
+Привет!
+
+В прошлом посте нашего рождественского календаря мы пробовали получить логи хрома с помощью _капабилити_ "goog:loggingPrefs".
+
+> UPD Позже был реализован метод, который работает и в Firefox, и в Chromium браузерах:
+> [Собираем логи браузера с помощью DevTools/BiDi](/2025/10/29/selenide-7.12.0/#browser-logs-with-bidi)
+
+
+
+
+
+А сегодня попробуем другой способ - с помощью JavaScript.
+
+Итак, надо всего лишь в конце теста дёрнуть такой вот JavaScript:
+
+```java
+String js =
+ "var performance = window.performance || window.mozPerformance" +
+ " || window.msPerformance || window.webkitPerformance || {};" +
+ " return performance.getEntries() || {};";
+String netData = executeJavaScript(js).toString();
+logger.info("Network traffic: {}", netData);
+```
+
+Результат получается примерно такой:
+
+```json
+Network traffic: [
+ {name=https://selenide.org/quick-start.html, connectEnd=0, connectStart=0, decodedBodySize=32582, domComplete=724, domContentLoadedEventEnd=119, domContentLoadedEventStart=115, domInteractive=104, domainLookupEnd=0, domainLookupStart=0, duration=724, encodedBodySize=32582, entryType=navigation, fetchStart=0, initiatorType=navigation, loadEventEnd=724, loadEventStart=724, nextHopProtocol=http/1.1, redirectCount=0, redirectEnd=0, redirectStart=0, requestStart=0, responseEnd=0, responseStart=0, secureConnectionStart=0, serverTiming=[], startTime=0, transferSize=0, type=navigate, unloadEventEnd=10, unloadEventStart=9, workerStart=0},
+ {name=https://selenide.org/assets/themes/ingmar/css/styles.css?001, connectEnd=12, connectStart=12, decodedBodySize=8177, domainLookupEnd=12, domainLookupStart=12, duration=29, encodedBodySize=8177, entryType=resource, fetchStart=12, initiatorType=link, nextHopProtocol=http/1.1, redirectEnd=0, redirectStart=0, requestStart=12, responseEnd=41, responseStart=21, secureConnectionStart=0, serverTiming=[], startTime=12, transferSize=0, workerStart=0},
+ {name=https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js, connectEnd=13, connectStart=13, decodedBodySize=84245, domainLookupEnd=13, domainLookupStart=13, duration=28, encodedBodySize=84245, entryType=resource, fetchStart=13, initiatorType=script, nextHopProtocol=http/1.1, redirectEnd=0, redirectStart=0, requestStart=13, responseEnd=41, responseStart=21, secureConnectionStart=0, serverTiming=[], startTime=13, transferSize=0, workerStart=0}
+]
+```
+
+### Плюсы:
+
+* Не нужно никак настраивать браузер. Оно работает из коробки.
+* Работает во всех браузерах (кажется?)
+
+### Минусы:
+
+* Здесь всё ещё нет тела запроса.
+* Это невалидный JSON. Распарсить его стандартным парсером не получится. Придётся придумывать какой-то свой обработчик.
+
+
+Но этот способ вполне подходит, чтобы просто посмотреть глазами и прикинуть, что происходит.
+
+
+## Что теперь?
+
+Наша последняя надежда - получить запросы и ответы с помощью встроенного прокси-сервера. Этот способ мы рассмотрим в следующем посте.
+
+
+
+[Андрей Солнцев](http://asolntsev.github.io/)
+
+ru.selenide.org
diff --git a/content/ru/blog/2019-12-18-advent-calendar-network-logs-with-proxy.md b/content/ru/blog/2019-12-18-advent-calendar-network-logs-with-proxy.md
new file mode 100644
index 000000000..2c7c8172a
--- /dev/null
+++ b/content/ru/blog/2019-12-18-advent-calendar-network-logs-with-proxy.md
@@ -0,0 +1,81 @@
+---
+slug: "advent-calendar-network-logs-with-proxy"
+date: 2019-12-18
+title: "Как получить сетевые запросы с помощью прокси"
+description: ""
+category:
+headerText: "Selenide Advent Calendar День 18"
+tags: []
+---
+Привет!
+
+В предыдущих постах нашего рождественского календаря мы рассмотрели два способа получить сетевые запросы между браузером и приложением.
+Оба нас расстроили тем, что не позволяют прочитать тело запроса/ответа.
+
+Наконец, дошла очередь до третьего способа - через встроенный прокси-сервер.
+
+### Перед тестом
+
+Как вы знаете, в селениде уже есть встроенный прокси-сервер, надо его всего лишь включить:
+
+```java
+Configuration.proxyEnabled = true;
+```
+
+И ещё нужно сказать прокси-серверу, чтобы он начал отслеживать запросы:
+
+```java
+ BrowserMobProxy bmp = WebDriverRunner.getSelenideProxy().getProxy();
+
+ // запоминать тело запросов (по умолчанию тело не запоминается, ибо может быть большим)
+ bmp.setHarCaptureTypes(CaptureType.getAllContentCaptureTypes());
+
+ // запоминать как запросы, так и ответы
+ bmp.enableHarCaptureTypes(CaptureType.REQUEST_CONTENT, CaptureType.RESPONSE_CONTENT);
+
+ // начинай запись!
+ bmp.newHar("pofig");
+```
+
+### После теста
+
+Теперь нужно получить HAR и анализировать все записи в нём:
+
+```java
+ List requests = bmp.getHar().getLog().getEntries();
+```
+
+HAR (HTTP Archive) - это типа такой "архив" со всеми сетевыми запросами и ответами ("entries").
+
+Каждая запись - это и есть запрос от тестируемого приложения к серверу.
+Внутри есть все данные: URL, request, response, их http status и body.
+Всё, о чём мы так давно мечтали.
+
+
+
+### Плюсы:
+
+* Есть все данные, которые нам нужны
+* Легко анализировать программно
+* Работает во всех браузерах
+
+### Минусы:
+
+Минус только один: иногда у людей возникают сложности с запуском прокси, когда браузер и тесты бегут на разных
+машинах, и с "браузерной" машины нет доступа к "тестовой" машине.
+Хотя я никогда не понимал, зачем такие сложности. Запускайте тесты и браузеры на одной и той же машине - ВСЁ будет в стотыщ раз ПРОЩЕ.
+Надо параллелить - параллельте тесты.
+Нужен кластер - запускайте ТЕСТЫ на разных нодах кластера (а с ними запустятся и браузеры). Зачем всё усложнять?
+
+
+## Что теперь?
+
+Теперь мы умеем читать сетевые запросы при прогоне тестов.
+Но я вообще-то надеюсь, что обычно это вам не должно быть нужно. Ну может, в каких-то очень уж запутанных случаях.
+Обычно должно быть достаточно почитать логи приложения, чтобы понять, какие запросы к нему прилетали. Будьте проще.
+
+
+
+[Андрей Солнцев](http://asolntsev.github.io/)
+
+ru.selenide.org
diff --git a/content/ru/blog/2019-12-20-advent-calendar-big-wait-theory.md b/content/ru/blog/2019-12-20-advent-calendar-big-wait-theory.md
new file mode 100644
index 000000000..5d6223679
--- /dev/null
+++ b/content/ru/blog/2019-12-20-advent-calendar-big-wait-theory.md
@@ -0,0 +1,107 @@
+---
+slug: "advent-calendar-big-wait-theory"
+date: 2019-12-20
+title: "Теория большого вейта"
+description: ""
+category:
+headerText: "Selenide Advent Calendar День 20"
+tags: []
+---
+# Теория большого вейта
+
+Тема ожиданий вызывает много обсуждений и споров.
+Современные веб-сайты создают проблемы для _написателей_ автотестов. Возникает много ситуаций, в которых стандартные методы Selenium неэффективны.
+
+Если вы читали документацию Selenide, вы уже знаете, что классические явные ожидания типа
+
+```java
+element = (new WebDriverWait(driver, ))
+ .until(ExpectedConditions.presenceOfElementLocated(By.cssSelector()));
+```
+
+или
+
+```java
+element = (new WebDriverWait(driver, ))
+ .until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector()));
+```
+
+были заменены в Selenide более коротким конструкциями типа
+
+```java
+element = $().should(exist);
+element = $().shouldBe(visible);
+```
+
+**Как известно, ассерты в Selenide - это новая версия явных ожиданий, что хорошо описано в [документации](https://selenide.org/documentation.html).**
+
+Сегодня мы не будем рассматривать ожидания и ассерты с технической точки зрения, а подумаем, для чего можно использовать
+ассерты в различных ситуациях.
+
+
+## Современные проблемы требуют современных решений
+
+### 1. `Thread.sleep()`
+
+Это самое ужасное, что может случиться с нашими тестами на Selenium.
+
+В некоторых ситуациях мы были вынуждены использовать слипы. У нас просто не было другого решения, чтобы обойти проблему и двигаться дальше.
+Например, слипы используют, чтобы дождаться окончания загрузки страницы. Иногда - чтобы дождаться какого-то элемента,
+когда другие ожидания не помогли. Увы, таким образом мы можем терять много времени при запуске теста.
+
+Если поставить один слип - это ещё ничего. Вы потеряете, скажем, 4 секунды - не смертельно.
+Но если вы используете слип в 150 тестах, время их выполнения увеличится заметно.
+Нет смысла объяснять, почему это плохо.
+
+Хотя команда `sleep()` есть и в Selenide, вышеупомянутые "умные ожидания" делают слипы почти ненужными для ожидания появления чего-то либо на странице.
+Смотри следующие пункты.
+
+
+### 2. Как дождаться окончания загрузки страницы?
+
+Самый простой способ - выбрать какой-то элемент на странице, который редко меняется (скажем, заголовок), и использовать метод Selenide:
+
+```java
+$(cssSelector).shouldBe(visible);
+```
+
+Selenide сначала попытается найти элемент, а потом проверить, что он видимый.
+Если это не удалось за 4 секунды, вы можете сделать вывод, что страница не загрузилась.
+
+Ещё вы можете выбрать какой-то элемент на _предыдущей_ странице и дождаться, пока он исчезнет:
+
+```java
+$(element).should(disappear);
+```
+
+Таким образом мы создаём двойную проверку, что мы перешли с одной страницы на другую.
+И это будет работать, даже если загрузка страницы занимает значительное время. Как видите, обошлись без всяких `Thread.sleep()`.
+
+### 3. Изменение состояния элемента
+
+Иногда нам нужно проверить, что состояние элемента поменялось в результате действий пользователя.
+Например, элемент может содержать текст, сигнализирующий об успешной или неуспешной загрузке файла.
+Допустим, загрузка файла занимает какое-то время, потому что файл большой, или сервер должен запустить какую-то сложную обработку этого файла.
+
+Обычно мы в тесте загружаем файл и проверяем состояние элемента (скажем, текст "файл загружен").
+Но как узнать, _когда_ именно состояние элемента должно поменяться? Загрузка-то происходит не мгновенно.
+В этой ситуации многие используют `Thread.sleep()`.
+
+А в Selenide у нас есть умный инструмент для "отложенной" проверки состояния элемента:
+
+```java
+$(cssSelector).shouldHave(exactText());
+```
+
+В этом случае Selenide сам дождётся, пока состояние элемента изменится, и в нём появится нужный текст.
+Нам не нужно писать лишних строк для ожиданий, и мы можем быть уверены, что Selenide точно дождётся.
+
+### Что теперь?
+
+Мы рассмотрели всего лишь несколько простых идей, как можно "ждать" с помощью Selenide.
+В реальности ситуаций намного больше. И здорово, что теперь у нас есть хороший инструмент, позволяющий нам обходится без слипов и не терять драгоценное время.
+Используйте умные инструменты и не теряйте время - время ценно. :)
+
+
+Maciej Grymuza (figrym@gmail.com)
+
diff --git a/content/ru/blog/2019-12-22-advent-calendar-defaulta-lingvo.md b/content/ru/blog/2019-12-22-advent-calendar-defaulta-lingvo.md
new file mode 100644
index 000000000..62496a26a
--- /dev/null
+++ b/content/ru/blog/2019-12-22-advent-calendar-defaulta-lingvo.md
@@ -0,0 +1,81 @@
+---
+slug: "advent-calendar-defaulta-lingvo"
+date: 2019-12-22
+title: "Defaŭlta lingvo"
+description: "Как определить язык по умолчанию"
+category:
+headerText: "Selenide Advent Calendar День 22"
+tags: []
+---
+# Defaŭlta lingvo
+
+Название сегодняшней темы пришло из языка Эсперанто и означает "язык по умолчанию".
+
+Вы могли заметить, что некоторые веб-приложения и сайты автоматически меняют свой язык в зависимости от настроек вашего браузера или вашего местоположения.
+
+## Проблема
+В том случае, когда у вас в команде интернационал разработчиков, которые пишут и запускают тесты на разных компьютерах, вы могли обратить внимание, что иногда одни и те же тесты начинают падать из-за того, приложение запустилось не на том языке, для которого писались тесты.
+
+Если приложение выбирает язык в зависимости от местоположения пользователя, то писать стабильные тесты запускающиеся в разных странах будет непросто. Зато, если приложение всего лишь смотрит в браузере на язык предпочитаемый пользователем по умолчанию, задача сильно упрощается.
+
+## Решение
+
+Итак, допустим у вас есть тесты, написанные для языка, другого чем тот, который является языком вашего браузера по умолчанию. Например - ваши тесты ожидают _**немецкий**_.
+
+Теперь у вас есть следующие опции:
+
+- Поменяйте язык по умолчанию вашей системы. Теперь большинство ваших программ на компьютере заговорят по-немецки. _**Ordnung muss sein!**_
+- Поменяйте в вашем браузере порядок языков так, чтобы самым предпочитаемым стал немецкий. Сохраните профиль браузера. С помощью гугла, напильника и удачи сконфигурируйте тесты так, чтобы профиль загружался перед запуском каждого теста. Да, не забудьте удалить немецкий из топа в списке предпочитаемых языков, иначе, ну вы поняли - _**Ordnung....**_
+- Ну или - просто воспользуйтесь Chrome preference "intl.accept_languages" установив её значение на "de" (немецкий язык).
+
+Разумеется, вы можете легко сделать это в Selenide.
+Установите значение системной переменной `chromeoptions.prefs=intl.accept_languages=de` или в коде:
+
+```java
+System.setProperty("chromeoptions.prefs","intl.accept_languages=de");
+```
+
+или, лучше, в конфигурационных файлах Maven или Gradle
+
+### Maven
+
+maven `pom.xml`
+```xml
+ ...
+
+ maven-surefire-plugin
+ 2.xx.yy
+
+
+ ...
+ intl.accept_languages=de
+
+
+
+ ...
+```
+
+### Gradle
+
+аналогично для Gradle в `gradle.properties` (вам придётся еще добавить строчку-другую в `build.gradle` чтобы передать эти параметры в test task грэдла, но про это - в другой раз)
+```properties
+systemProp.chromeoptions.prefs=intl.accept_languages=de
+```
+
+### Командная строка
+
+Вы можете переопределять значения при запуске `mvn test` или `gradle test` определяя новое значение в `-Dchromeoptions.prefs=intl.accept_languages=ru`
+
+
+## Пример
+
+Просто запускайте этот маленький тест с различными параметрами языка и понаблюдайте за результатом.
+
+```java
+open("http://wikipedia.org");
+$("[data-jsl10n=slogan]").shouldHave(exactText("Die freie Enzyklopädie"));
+```
+
+Я желаю всем вам _**Fröhliche Weihnachten**_ и _**Guten Rutsch**_!
+
+**Alexei Vinogradov**
diff --git a/content/ru/blog/2019-12-24-advent-calendar-javascript-tricks.md b/content/ru/blog/2019-12-24-advent-calendar-javascript-tricks.md
new file mode 100644
index 000000000..95593fb80
--- /dev/null
+++ b/content/ru/blog/2019-12-24-advent-calendar-javascript-tricks.md
@@ -0,0 +1,276 @@
+---
+slug: "advent-calendar-javascript-tricks"
+date: 2019-12-24
+title: "Трюки с JavaScript"
+description: ""
+category:
+headerText: "Selenide Advent Calendar День 24"
+tags: []
+---
+Привет!
+
+На дворе 24 декабря, католическое рождество. А это значит, что Advent Calendar подошёл к концу.
+
+И напоследок мы поиграемся с JavaScript.
+
+Как язык JavaScript, конечно, дно, но он даёт большие возможности при написании автотестов.
+Он позволяет залезть в такие дыры, куда с обычным вебдрайвером и не снилось.
+
+Приведу несколько примеров из реальных проектов.
+
+## Выбрать дату
+
+Есть масса всевозможных элементов для выбора даты - т.н. "date picker". И выбрать в них нужную дату - это вечная головная боль.
+
+Если реализовывать такой метод в лоб:
+
+```java
+@Test {
+ setDateByName("recurrent.startDate", "16.01.2009");
+}
+```
+
+Придётся сделать примерно следующие шаги:
+1. Тыкнуть иконку "календарик"
+2. Тыкнуть год
+3. Тыкнуть стрелку “месяц назад” (сколько раз?)
+4. Тыкнуть день
+5. Ой, фсё, сегодня 29 февраля. Тест упал.
+
+**Долго, сложно, ненадёжно.**
+
+
+
+#### А вот как этот метод можно реализовать с помощью JS:
+
+```java
+void setDateByName(String name, String date) {
+ executeJavaScript(
+ String.format("$('[name=\"%s\"]').val('%s')",
+ name, date)
+ );
+}
+```
+
+**Быстро и надёжно.**
+
+Возможно, вам смутит, что это не совсем "честный" способ. Но об этом мы поговорим в конце. Не переключайтесь.
+
+## Спрятать календарик
+
+Допустим, календарик всё же открылся. Как его спрятать?
+Решение в лоб - тыкнуть крестик в углу. Опять же, это медленно и ненадёжно:
+* крестик вечно располагается в разных углах
+* расположение крестика часто меняется в зависимости от дизайна, размера страницы и т.п.
+* иногда календарик открывается не сразу - нужно добавить ожидание на открытие, чтобы тут же закрыть.
+
+Идиотская ситуация.
+
+
+
+#### А вот как этот метод можно реализовать с помощью JS:
+
+```java
+executeJavaScript(
+ "$('.datepicker').hide();"
+);
+```
+
+**Быстро и надёжно.**
+
+
+## Перелистнуть перелистывалку
+
+Представим себе страницу, на которой есть "перелистывалка" карт. Нужно выбрать нужную карту, двигая наманикюренным пальчиком.
+_Как сэмулировать это селениумом?_
+Можно, конечно. Всякие там Drag'n'Drop, Actions. Нажал, потянул, отпустил. Но всё это **медленно и нестабильно**.
+Это легко может сломаться от малейших изменений дизайна, изменения размерна окна браузера, потери фокуса и т.д.
+
+#### А вот как можно с помощью JS:
+
+```java
+void selectAccount(String accountId) {
+ executeJavaScript(
+ String.format("$('[data-account-id=\"%s\"]').attr('data-card-account', 'true')", accountId)
+ );
+}
+```
+
+Это нетривиально. Пришлось поизучать код этой "перелистывалки" и понять, какой JS код она дёргает при перелистывании.
+И дёрнуть из теста аналогичный JS код. Придётся поработать головой, но зато это **быстро и надёжно.**
+
+
+## Выбрать опцию в bootstrap select
+
+Многие UI фреймворки заменяют стандартный `