QA.GURU
AI-first QA · live: wrong password
36 · лаборатория
← листы 30–35

промпт

Добавь автотест на неуспешный логин с неправильным паролем

Слои

Skill · Rule · RAG · ADR из git

Выкл — агент этот файл не читает.

читает

skill · SKILL.md

docs/agent-skills/qa-write-test/SKILL.md

# Разработай автотест

RAG (этот промпт, 4 id):
po-locators, po-step, test-negative, cfg-stands.
ADR: docs/adr/009-login-401-is-api.md

## When
«напиши e2e», «добавь negative login»

## Do not
- CSS/xpath в *Tests
- новый ChromeDriver в тесте
Commit / URL / -Denv — rule
401 vs форма — ADR 009

## Steps
1. Один @Layer
2. Стенды pipeline / stage / prod
3. Расширь PO, локаторы не в тесте
4. @Tag яруса + positive/negative
5. ./gradlew test -Denv=ci -DincludeTags=e2e \
     -Dtest=LoginTests#<method>

## DoD
слой, стенды, @Step на PO, exit

как попадает: «прочитай SKILL.md»

When  напиши e2e / покрой логин
Don't CSS в *Tests, ChromeDriver
RAG   po-locators · po-step · test-negative · cfg-stands
ADR   009 — не решай 401 в чате
Шаги  слой → стенды → PO → -Dtest=
DoD   слой, стенды, @Step на PO, exit
читает

rule · .cursor/rules rule · .clinerules rule · AGENTS.md + .clinerules rule · AGENTS.md

.cursor/rules/01…04.mdc + AGENTS.md

# 01-qa-java-gradle.mdc
---
alwaysApply: false
globs: tests/java/.../**
---
./gradlew test -Denv=ci -DincludeTags=e2e \
  -DexcludeTags=screenshot,mock
# task testE2e нет
# @Tag("smoke") — prod slice, не эта команда (ADR 005)

# 02-git-safety.mdc
---
alwaysApply: true
---
не commit / push / reset --hard без OK

# 03-env-and-stand.mdc
всегда -Denv=
URL не в Java
pipeline / stage / prod

# 04-one-task-one-layer.mdc
один task = один ярус

.clinerules/01…04.md + AGENTS.md

# 01-qa-java-gradle.md
---
paths: tests/java/.../**
---
./gradlew test -Denv=ci -DincludeTags=e2e \
  -DexcludeTags=screenshot,mock
# task testE2e нет
# @Tag("smoke") — prod slice, не эта команда (ADR 005)

# 02-git-safety.md
не commit / push / reset --hard без OK

# 03-env-and-stand.md
всегда -Denv=
URL не в Java
pipeline / stage / prod

# 04-one-task-one-layer.md
один task = один ярус

AGENTS.md + .clinerules/01…04.md

# в takeaway нет CLAUDE.md / .claude/rules
# те же лимиты: AGENTS.md + .clinerules

# AGENTS.md
не commit / push / reset --hard без OK
URL из properties, всегда -Denv=
один task = один @Layer
pipeline / stage / prod

# .clinerules/01-qa-java-gradle.md
---
paths: tests/java/.../**
---
./gradlew test -Denv=ci -DincludeTags=e2e \
  -DexcludeTags=screenshot,mock
# task testE2e нет
# @Tag("smoke") — prod slice, не эта команда (ADR 005)

# 02-git-safety.md
не commit / push / reset --hard без OK

# 03-env-and-stand.md
pipeline / stage / prod

# 04-one-task-one-layer.md
один task = один ярус

AGENTS.md · корень репо

# AGENTS.md
# QA Agent — takeaway Java/Selenide

## E2e
cd tests/java/tests-java-...-selenide
./gradlew test -Denv=ci -DincludeTags=e2e \
  -DexcludeTags=screenshot,mock
# task testE2e нет
# @Tag("smoke") — prod slice, не эта команда (ADR 005)

## Ограничения
не commit / push / reset --hard без OK
URL из properties, не localhost в коде
один task = один @Layer
pipeline / stage / prod

как попадает: сам, если alwaysApply / globs как попадает: сам, если Rules ON как попадает: AGENTS.md + .clinerules (в репо нет CLAUDE.md) как попадает: сам, читает AGENTS.md

01  e2e = tags + -Denv, task testE2e нет
02  не commit / push без OK
03  URL только из properties, всегда -Denv
04  один task = один ярус
читает

rag · po-locators · po-step · test-negative · cfg-stands

docs/agent-skills/rag/*.md

# po-locators
loginInput  = $("[data-testid='login-input']")
errorMessage= $("[data-testid='error-message']")
# не $("input") в *Tests

# po-step
@Step на методах PO, не на классе
@Step("Type password")     // не {password}
не Allure.step в *Tests + @Step

# test-negative
typeUsername().typePassword()
          .submitExpectingError()
# не fillAndSubmitForm — ждёт HomePage
текст «Wrong login or password»
@Tag("negative")

# cfg-stands
URL из config
user1 есть на pipeline / stage / prod

как попадает: skill назвал эти 2–4 файла

data-testid только в LoginPage
typeUsername / submitExpectingError
@Step на методах PO, не на классе
пароль не в тексте шага
текст «Wrong login or password»
стенды pipeline / stage / prod
читает

adr · 009-login-401-is-api.md

docs/adr/009-login-401-is-api.md

# ADR 009: UI-текст e2e, 401 JSON — api
Статус: принято

Контекст: неверный пароль — форма и JSON.
В чате хочется второй e2e «на 401».

Решение:
- текст на форме →
  LoginTests#shouldShowErrorWhenPasswordIsWrong
- 401 JSON → уже
  AuthApiTests#loginWithInvalidPassword
- новый браузерный тест «на 401» не пишем

Последствия:
- не $("pre") «401»
- селекторы и текст ошибки — RAG, не ADR

как попадает: skill ссылается, сам не копирует

401 JSON → AuthApiTests#loginWithInvalidPassword
текст на форме → LoginTests e2e
не $("pre") «401»

Контекст что открыто

Идеальный все слои

Ожидаемый с тумблерами

читает

test · LoginTests.java

tests/e2e/LoginTests.java · вкладка

@Layer("e2e")
@Epic("Authentication")
@Feature("Login")
class LoginTests extends TestBase {

  @Test
  @Tag("e2e")
  @Tag("smoke")
  @Tag("positive")
  @DisplayName("User is logged in with valid credentials")
  void shouldLoginWithValidCredentials() {
    loginPage.openPage()
        .fillAndSubmitForm("user1", "password1")
        .shouldHaveWelcomeMessage("Welcome, user1!");
  }

  // shouldShowErrorWhenPasswordIsWrong
  // на занятии комментируют —
  // иначе skill скажет «не дублируй»
}

вкладка: успешный логин

@Layer("e2e")
@Epic("Authentication")
@Feature("Login")
class LoginTests extends TestBase {

  @Test
  @Tag("e2e")
  @Tag("smoke")
  @Tag("positive")
  @DisplayName("User is logged in with valid credentials")
  void shouldLoginWithValidCredentials() {
    loginPage.openPage()
        .fillAndSubmitForm("user1", "password1")
        .shouldHaveWelcomeMessage("Welcome, user1!");
  }
}
читает

page · LoginPage.java

pages/LoginPage.java · вкладка

class LoginPage {

  loginInput    = $("[data-testid='login-input']");
  passwordInput = $("[data-testid='password-input']");
  submitButton  = $("[data-testid='submit-button']");

  @Step("Open login page")
  LoginPage openPage() {
    open("/login");
    return this;
  }

  @Step("Fill and submit form")
  HomePage fillAndSubmitForm(String username, String password) {
    typeUsername(username).typePassword(password).submit();
    return new HomePage();
  }

  @Step("Type username: {username}")
  LoginPage typeUsername(String username) {
    loginInput.setValue(username);
    return this;
  }

  @Step("Type password")
  LoginPage typePassword(String password) {
    passwordInput.setValue(password);
    return this;
  }
}

вкладка: форма, happy path, @Step на методах

class LoginPage {

  loginInput    = $("[data-testid='login-input']");
  passwordInput = $("[data-testid='password-input']");
  submitButton  = $("[data-testid='submit-button']");

  @Step("Open login page")
  LoginPage openPage() {
    open("/login");
    return this;
  }

  @Step("Fill and submit form")
  HomePage fillAndSubmitForm(String username, String password) {
    typeUsername(username).typePassword(password).submit();
    return new HomePage();
  }

  @Step("Type username: {username}")
  LoginPage typeUsername(String username) {
    loginInput.setValue(username);
    return this;
  }

  @Step("Type password")
  LoginPage typePassword(String password) {
    passwordInput.setValue(password);
    return this;
  }
}
читает

page · HomePage.java

pages/HomePage.java · вкладка

class HomePage {

  welcomeMessage = $("[data-testid='welcome-message']");

  @Step("Verify welcome message: {message}")
  HomePage shouldHaveWelcomeMessage(String message) {
    welcomeMessage.shouldHave(text(message));
    return this;
  }
}

fillAndSubmitForm возвращает HomePage · @Step на методе

class HomePage {

  welcomeMessage = $("[data-testid='welcome-message']");

  @Step("Verify welcome message: {message}")
  HomePage shouldHaveWelcomeMessage(String message) {
    welcomeMessage.shouldHave(text(message));
    return this;
  }
}
читает

base · TestBase.java

tests/TestBase.java

class TestBase {
  protected LoginPage loginPage = new LoginPage();
  protected static final TestConfig config =
      ConfigReader.testConfig;

  @BeforeAll
  static void setup() {
    Configuration.baseUrl = config.baseUrl();
    Configuration.browser = config.browser();
    Configuration.browserSize = config.browserSize();
    Configuration.headless = config.headless();
  }
  // не new ChromeDriver() в тесте
}

вкладкой не открываем — видит сам

protected LoginPage loginPage = new LoginPage();

@BeforeAll
static void setup() {
  Configuration.baseUrl = config.baseUrl();
  Configuration.browser = config.browser();
  Configuration.browserSize = config.browserSize();
  Configuration.headless = config.headless();
}
// не new ChromeDriver() в тесте
читает

props · ci.properties

config/ci.properties · -Denv=ci

baseUrl=http://localhost:9821/
browser=chrome
browserVersion=148
browserSize=1920x1280
headless=true

не трогал — URL не в Java

baseUrl=http://localhost:9821/
browser=chrome
browserVersion=148
browserSize=1920x1280
headless=true
читает

build · build.gradle

tests/java/tests-java-gradle-junit5-allure3-selenide/build.gradle

javaVersion     = 21
junitVersion    = '5.11.4'
selenideVersion = '7.17.0'
allureVersion   = '3.13.0'
ownerVersion    = '1.0.12'

testImplementation (
  "com.codeborne:selenide:${selenideVersion}",
  "org.junit.jupiter:junit-jupiter:${junitVersion}",
  "io.qameta.allure:allure-selenide",
  "org.aeonbits.owner:owner:${ownerVersion}"
)

test {
  useJUnitPlatform {
    def includeTagsProp = System.getProperty('includeTags')
    if (includeTagsProp) {
      includeTags(*(includeTagsProp.split(',')*.trim()))
    }
    def excludeTagsProp = System.getProperty('excludeTags')
    if (excludeTagsProp) {
      excludeTags(*(excludeTagsProp.split(',')*.trim()))
    }
  }
  systemProperty 'env', testEnv
}

вкладка: версии, -DincludeTags, -Denv=; task testE2e нет

javaVersion     = 21
junitVersion    = '5.11.4'
selenideVersion = '7.17.0'
allureVersion   = '3.13.0'
ownerVersion    = '1.0.12'

testImplementation (
  "com.codeborne:selenide:${selenideVersion}",
  "org.junit.jupiter:junit-jupiter:${junitVersion}",
  "io.qameta.allure:allure-selenide",
  "org.aeonbits.owner:owner:${ownerVersion}"
)

test {
  useJUnitPlatform {
    def includeTagsProp = System.getProperty('includeTags')
    if (includeTagsProp) {
      includeTags(*(includeTagsProp.split(',')*.trim()))
    }
    def excludeTagsProp = System.getProperty('excludeTags')
    if (excludeTagsProp) {
      excludeTags(*(excludeTagsProp.split(',')*.trim()))
    }
  }
  systemProperty 'env', testEnv
}
читает

cli · терминал

шелл · вкладкой не открываем

# агент гоняет сам, не третья вкладка
cd tests/java/tests-java-gradle-junit5-allure3-selenide
./gradlew test -Denv=ci -DincludeTags=e2e \
  -Dtest=LoginTests#shouldLoginWithValidCredentials
# task testE2e нет

прогон из шелла, не Java

cd tests/java/tests-java-gradle-junit5-allure3-selenide
./gradlew test -Denv=ci -DincludeTags=e2e \
  -Dtest=LoginTests#shouldLoginWithValidCredentials
# task testE2e нет
читает

ci · ci.yml

.github/workflows/ci.yml

# два флага: -Denv / -DincludeTags
# стенд и срез. task testE2e нет.

e2e-tests:
  env:
    LAYER: e2e
  steps:
    - uses: ./tests/.github/actions/e2e
      with:
        stand: prod
        # внутри action:
        # ./gradlew test -Denv=prod
        #   -DincludeTags=e2e
        #   -DexcludeTags=mock,screenshot

job вызывает action, не task testE2e

e2e-tests:
  env:
    LAYER: e2e
  steps:
    - uses: ./tests/.github/actions/e2e
      with:
        stand: prod
# ./gradlew test -Denv=prod
#   -DincludeTags=e2e
#   -DexcludeTags=mock,screenshot
# task testE2e нет