Selenium — Guia passo a passo

Do zero à referência completa: Selenium WebDriver 4, Mocha 11, Chai, Page Objects, testes API, waits explícitos e todos os scripts npm do testflow-selenium.

E2E · API · POM · Mocha · Chai · testflow-selenium

O que é o Selenium?

O Selenium WebDriver 4 controla um browser real para testes end-to-end. Este projeto combina com Mocha 11, Chai e ChromeChrome (headless no CI, headed localmente).

  • Escrito em JavaScript (Node.js 20+)
  • Waits explícitos via until.elementLocated / elementIsVisible
  • getHelpers() global de tests/hooks.js — WebDriver compartilhado por run
  • mochawesome localmente; allure-mocha no CI
  • Page Objects em pages/ + publicRequest para specs só de API
Diferente de runners in-browser como Cypress, o Selenium WebDriver controla o browser de fora da aplicação — o padrão usado na maioria dos times de QA enterprise. Veja a documentação oficial.

Por que usar Selenium?

E2E com browser real

Chrome WebDriver em modo headed (test:headed) ou headless no CI.

Locators estáveis

helpers.getByTestId em data-testid — centralizados nos Page Objects.

Mocha + Chai

Estrutura describe / it com assertions expect do Chai.

UI + API no mesmo repo

Page Objects para UI; publicRequest para contratos REST sem browser.

Pré-requisitos

Ambiente

  • Node.js 20+
  • npm (ou pnpm / yarn)
  • App TestFlow na porta 5050
  • Editor: VS Code / Cursor (extensão Selenium opcional)
$ node --version
v20.x.x

$ docker run -p 5050:5050 qaschool/testflow:latest
# app disponível em localhost:5050

Instalação do ambiente por SO

Antes de clonar o repositório, instale Node.js 20+, Git e Docker (para subir o TestFlow na porta 5050). Os passos do projeto (npm install, browsers) são iguais em todos os sistemas.

macOS

# Homebrew — https://brew.sh
brew install node@20 git
brew link --overwrite node@20
brew install --cask docker

node --version   # v20+
git --version
Homebrew nvm Docker Desktop

Windows

winget install OpenJS.NodeJS.LTS
winget install Git.Git
winget install Docker.DockerDesktop

node --version
winget PowerShell Git Bash

Instaladores: nodejs.org, Git for Windows, Docker Desktop.

Linux

# Debian / Ubuntu
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs git docker.io
sudo usermod -aG docker $USER

node --version
NodeSource install-deps Docker

Após npm install: npx selenium install-deps (libs dos browsers). Fedora: dnf install nodejs git docker.

# TestFlow — igual em macOS, Windows e Linux
docker run --rm -p 5050:5050 qaschool/testflow:latest

Instalação — testflow-selenium

  1. Clone o repositório
    git clone https://github.com/lflucasferreira/testflow-selenium.git
    cd testflow-selenium
    Git GitHub
  2. Instale dependências
    npm install
    npm Node.js
  3. Chrome browser
    Instale Google Chrome (ou Chromium). O WebDriver 4 gerencia o driver via Selenium Manager.
    Chrome Selenium 4
  4. Execute os testes
    npm test ou npm run test:headed
    Mocha npm scripts
Configure BASE_URL, SELENIUM_DEMO_EMAIL e SELENIUM_DEMO_PASSWORD via config/env.js ou variáveis de ambiente. Veja o README do projeto para secrets de CI.

Estrutura do projeto

testflow-selenium/
├── config/env.js       # BASE_URL, credenciais, headless, window size
├── docs/
│   ├── en/tests/       # walkthroughs EN (bloco a bloco)
│   └── pt/tests/       # walkthroughs PT
├── fixtures/           # credentials.json, team-member.json, schemas/
├── pages/              # Page Object Model (LoginPage, TeamPage, …)
├── support/
│   ├── driver.js       # builder Chrome WebDriver
│   ├── helpers.js      # getByTestId, waits, screenshots
│   ├── auth.js         # createAuthSession, visitWithSession
│   ├── api.js          # helper publicRequest
│   └── factories/      # builders de payload wizard
├── tests/
│   ├── api/            # auth.api.test.js
│   ├── auth/ smoke/ dashboard/ team/ settings/
│   ├── components/ wizard/ activity/ advanced/ states/
├── tests/hooks.js      # hooks globais Mocha (lifecycle do driver)
├── scripts/wd-run.sh   # wrapper npm test (--grep, --spec)
├── .mocharc.cjs
└── package.json
  • pages/ — seletores, ações e assertions por tela
  • support/auth.js — sessão via API ou login UI
  • docs/en/tests/ — explicação bloco a bloco de cada spec
  • tests/*.test.js — 11 specs · Mocha + Chai
  • data-testid — convenção de seletores estáveis

Configuração — .mocharc.cjs

module.exports = {
  require: ['tests/hooks.js'],
  timeout: 120000,
  reporter: process.env.CI ? 'allure-mocha' : 'mochawesome',
  spec: 'tests/**/*.test.js',
}

tests/hooks.js cria um Chrome WebDriver por run e expõe getHelpers() globalmente. Localmente, abra o relatório mochawesome em reports/; no CI, resultados Allure vão para allure-results/. Filtre por tag com npm run test:smoke ou npm run test:regression (--grep '@smoke' / '@regression' via scripts/wd-run.sh).

Seu primeiro teste

// tests/auth/login.test.js
const { expect } = require('chai')
const LoginPage = require('../../pages/LoginPage')
const { getConfig } = require('../../support/driver')

describe('Authentication @regression', function () {
  let loginPage

  beforeEach(async function () {
    const helpers = getHelpers()
    loginPage = new LoginPage(helpers)
    await loginPage.open()
  })

  it('logs in via UI and redirects to dashboard @smoke @critical', async function () {
    const config = getConfig()
    await loginPage.loginWith(config.demoEmail, config.demoPassword)
    await loginPage.shouldRedirectToDashboard()
  })
})

Convenção: data-testid="login-email" → helpers.getByTestId('login-email') dentro dos Page Objects.

Locators & navegação

API Selenium usada na suíte TestFlow — seletores via data-testid.

APIUso no TestFlow
driver.get(url)Abrir login, dashboard, team…
helpers.getByTestId(id)Selector padrão — login-email, page-team
helpers.clickByTestId(id)Submit, links da sidebar, modais
helpers.typeByTestId(id, text)Email, senha, campos de busca
helpers.getTextByTestId(id)Labels visíveis, mensagens de erro
driver.findElements(By.css(...))tbody tr, linhas da tabela
helpers.executeScript(...)sessionStorage pós-login
helpers.readSessionStorage(key)Assert token de auth persistido
driver.navigate().refresh()Recarregar após mudança de estado
helpers.takeScreenshot(name)Captura em falha (hooks anexam no CI)

Interações

AçãoExemplo TestFlow
helpers.typeByTestId(...)Email, senha, busca na tabela
element.clear()Limpar busca antes de novo filtro
helpers.clickByTestId(...)Submit, sidebar, paginação, modais
helpers.selectByTestId(...)Role filter, invite role, run suite
checkbox.click()Remember me, API toggle
Key.ESCAPEFechar modal de invite / new run
element.getText()Extrair valor numérico de KPI
element.isDisplayed()Condicional — toast ou result msg
helpers.executeScript(...)Checkbox coberto por label custom
driver.actions().sendKeys(...)Atalhos de teclado na UI

Assertions — Chai expect()

AssertionExemplo
expect(x).to.be.trueSubmit habilitado, estado de checkbox
expect(text).to.include(...)Erros, greeting, row count label
expect(url).to.include(...)Redirect pós-login
expect(value).to.equal(...)Placeholder, tipo password
expect(arr).to.have.length(n)Linhas visíveis, activity items
expect(text).to.match(/regex/)Pass rate ^\d+%$
expect(res.status).to.equal(200)Contratos API
expect(duration).to.be.below(ms)SLA de resposta API
Combine waits explícitos (waitForVisible, getByTestId) com assertions Chai — o Selenium não faz auto-retry de assertions como o Playwright expect(locator).

Helpers compartilhados

support/helpers.js
getByTestId(testId)Wait + retorna elemento visível
clickByTestId · typeByTestIdInterações comuns
takeScreenshot(name)Salva PNG em screenshots/
readSessionStorage(key)Estado de auth após login
support/auth.js
createAuthSession(helpers)Auth programática via API
visitWithSession(helpers, path)Navegar já autenticado
clearSession(helpers)Reset antes dos specs de login
support/api.js
publicRequest(path, opts)Helper HTTP para specs API (sem browser)
loadFixture(name)Carrega JSON de fixtures/
fixtures/
credentials.jsonvalid / invalid email e password
team-member.jsonPayload do modal invite
schemas/*.jsonSchemas AJV para respostas API

Estratégia de seletores

Este projeto usa exclusivamente data-testid no app TestFlow — sem component tests. Veja também docs/selector-strategy.md.

ContextoLocator preferidoNotas
Elementos TestFlowhelpers.getByTestId(...)Estratégia principal — estável, definido pelo app
Modais, tabs (ARIA)helpers.getByTestId(...)Complementa test IDs onde há roles ARIA
Estado interno (CSS)page.locator('.class')Só quando não há test ID (.active, .spinner)
Shadow DOM / iframesframeLocator() + test IDVer advanced.test.js
Select2, SweetAlert2CSS vendor.select2-container, .swal2-popup — último recurso
Page Objects em pages/ são a fonte única de locators — nunca espalhe seletores nos specs.

Estrutura de specs

describe('Team', () => {
  beforeEach(async ({ page, request }) => {
    await visitAuthenticated(page, request, '/web/team.html')
    await expect(new TeamPage(page).pageRoot()).toBeVisible()
  })

  describe('Search', () => {
    it('filters rows by member name', async ({ page }) => {
      const team = new TeamPage(page)
      await team.search('Alice')
      await team.shouldHaveRowCount(1)
    })
  })

  describe('Invite modal', () => { /* … */ })
})
describe aninhado beforeEach setup 1 cenário / it()

beforeEach vs before

// beforeEach — estado fresco por teste
beforeEach(async function () {
  const helpers = getHelpers()
  await createAuthSession(helpers)
  await helpers.driver.get(`${config.baseUrl}/web/dashboard.html`)
})
// before — resposta API compartilhada (auth.api.test.js)
let res

before(async function () {
  res = await publicRequest('/api/auth/login', {
    method: 'POST',
    body: { email: config.demoEmail, password: config.demoPassword },
  })
})

it('returns status 200', function () {
  expect(res.status).to.equal(200)
})

before evita N chamadas API idênticas; beforeEach isola estado de UI entre cenários.

Data-driven tests

// tests/smoke/navigation.test.js
const PAGES = [
  { path: '/web/dashboard.html', testId: 'page-dashboard', title: 'Dashboard' },
  { path: '/web/team.html', testId: 'page-team', title: 'Team' },
  // settings, components, activity, wizard, states…
]

PAGES.forEach(({ path, testId, title }) => {
  it(`${title} page loads without error @smoke`, async function () {
    const helpers = getHelpers()
    await helpers.driver.get(`${config.baseUrl}${path}`)
    await helpers.getByTestId(testId)
    expect(await helpers.getTitle()).to.include(title)
  })
})

Um loop gera N testes nomeados — smoke cobre todas as rotas com um único padrão.

Waits explícitos — checks estáveis na UI

const { until, By } = require('selenium-webdriver')

// helpers.getByTestId encapsula waitForVisible:
async waitForVisible(locator, timeout = TIMEOUTS.DEFAULT) {
  const element = await this.driver.wait(until.elementLocated(locator), timeout)
  await this.driver.wait(until.elementIsVisible(element), timeout)
  return element
}

// Assertion no Page Object — wait primeiro, depois lê estado
async shouldRedirectToDashboard() {
  await this.helpers.getByTestId('page-dashboard')
  const url = await this.helpers.getCurrentUrl()
  if (!url.includes('/web/dashboard.html')) {
    throw new Error(`Expected dashboard, got ${url}`)
  }
}

Sempre aguarde elementos antes de interagir. Implicit wait está em 0 em support/driver.js — waits explícitos nos helpers mantêm falhas diagnosticáveis.

API testing — request fixture

// tests/api/auth.api.test.js
const ENDPOINT = '/api/auth/login'
const VALID = { email: DEMO_EMAIL, password: DEMO_PASSWORD }

describe('Valid credentials', () => {
  beforeAll(async ({ request }) => {
    res = await request.post(ENDPOINT, { data: VALID })
    body = await res.json()
  })

  it('returns status 200', () => expect(res.status()).toBe(200))
  it('body has token as non-empty string', () => {
    expect(typeof body.token).toBe('string')
  })
  it('responds within 2000ms', () => expect(duration).toBeLessThan(2000))
})

validateSchema — contrato REST

export function validateSchema(obj, schema: Schema) {
  for (const [key, type] of Object.entries(schema)) {
    expect(obj).toHaveProperty(key)
    expect(typeof obj[key]).toBe(type)
  }
}

for (const user of body.users) {
  validateSchema(user, { name: 'string', email: 'string', role: 'string' })
}
GET /api/users GET /health GET /api/errors/404 GET /api/errors/422

helpers.net — spy & mock (CDP)

Equivalente ao cy.intercept do Cypress / page.route do Playwright. Requer Chrome. Anexado em tests/hooks.js como helpers.net.

const { interceptLogin, stubLoginFailure, mockApiGet } = require('./support/network/presets')
const { waitForIntercept, assertResponseSchema } = require('./support/network/assertions')

// Fase 1 — Spy
interceptLogin(helpers.net)
await loginPage.toggleUseApi()
await loginPage.loginWith(config.demoEmail, config.demoPassword)
const call = await waitForIntercept(helpers.net, 'loginApi')
expect(call.response.status).to.equal(200)

// Fase 2 — Mock lista vazia
await mockApiGet(helpers.net, 'users/empty-list', /\/api\/users/)
await helpers.clickByTestId('fetch-users-btn')
await waitForIntercept(helpers.net, 'mock_users_empty-list')

// Fase 2 — Mock 500 login
await stubLoginFailure(helpers.net, 500)

// Fase 3 — Contrato Ajv
assertResponseSchema(call, loadFixture('schemas/auth-login.json'))
interceptLogin interceptGetUsers stubLoginFailure mockApiGet interceptSlowApi waitForIntercept

Intercept na UI — Team & Settings (opcional)

No TestFlow atual, invite/senha/rotate são UI-only. Use helpers.net.get(alias) com if (call), como no Cypress.

const { interceptInvite } = require('./support/network/presets')

interceptInvite(helpers.net)
await teamPage.submitInvite()

const call = helpers.net.get('inviteApi')
if (call) {
  assertRequestBodyKeys(call, ['name', 'email'])
}

Auth helpers — setup rápido

async function injectAuth(page, session) {
  await page.addInitScript((auth) => {
    sessionStorage.setItem('sandbox-auth', JSON.stringify(auth))
    sessionStorage.setItem('sandbox-token', auth.token)
  }, session)
}

export async function loginViaApi(page, request) {
  const session = await fetchAuthToken(request)
  await injectAuth(page, session)
  await page.goto('/web/dashboard.html')
  await helpers.getByTestId('page-dashboard').waitFor()
}

Usado em dashboard, team, settings — specs focam na feature, não no login.

Fixtures customizadas

Substitui comandos globais — injeta token e headers em todo request.

export const test = base.extend<{
  authToken: string
  userAccessToken: string
}>({
  authToken: async ({}, use) => {
    const token = await getOrRefreshSystemToken()
    await use(token)
  },
  request: async ({ request }, use) => {
    await use(wrapRequestWithDefaultHeaders(request, DEFAULT_HEADERS))
  },
})

afterEach(async ({}, testInfo) => {
  if (testInfo.status === 'failed') {
    await testInfo.attach('failure-details', { body: formatError(testInfo) })
  }
})

EXPECT — status codes tipados

export const EXPECT = {
  happy: 200,
  noAuth: 403,
  invalidBearer: 401,
  notFound: 404,
  validationError: 422,
} as const

it(`[${EXPECT.noAuth}] GET without Authorization`, async ({ request }) => {
  const res = await request.get('/api/resources')
  expect(res.status()).toBe(EXPECT.noAuth)
})

Route modules + exchange helpers

export const AUTH_LOGIN_PATH = '/api/auth/login'

export async function postLoginExchange(
  request, testInfo, label, body
) {
  const response = await request.post(AUTH_LOGIN_PATH, { data: body })
  return attachHttpExchangeReport(testInfo, {
    label, method: 'POST', url: AUTH_LOGIN_PATH, response,
  })
}
  • URLs centralizadas — sem strings espalhadas
  • Exchange helper = request + attach report
  • Path aliases: @pw/core/*, @pw/helpers/*

attachHttpExchangeReport

await attachHttpExchangeReport(testInfo, {
  label: 'login-200',
  method: 'POST',
  url: '/api/auth/login',
  requestHeaders: { Authorization: 'Bearer …' },  // mascarado
  response,
})
// Anexa: status, headers, body JSON, curl reproduzível
testInfo.attach curl replay mask Authorization

Tags, grep & modo serial

describe('Happy path suite @needs-browser', () => {
  describe.configure({ mode: 'serial', timeout: 150_000 })
  beforeAll(async () => { /* setup MFA / OAuth once */ })
})

describe('Negative testing', { tag: '@negative-testing' }, () => { /* … */ })
# CI — jobs separados por tag
npx npm test --grep-invert '@needs-browser'   # API only
npx npm test --grep '@needs-browser'          # OAuth browser flow
npx npm test --grep '@negative-testing'
npm run test:grep:smoke                              # @smoke (Chromium)

globalSetup — cache de sessão

export default defineConfig({
  globalSetup: require.resolve('./globalSetup'),
})

// tests/hooks.js — roda uma vez antes de todos os workers
// 1. Reutiliza token em .selenium/token-cache.json
// 2. Login via API
// 3. Grava storageState em .selenium/.auth/user.json

Equivalente ao cy.session — login pesado uma vez, reutilizado em N specs.

API enterprise — visão geral

Suíte API-only: sem browser, sem UI — foco em contratos REST, OAuth e sync entre serviços.

Legado Cypress

Service Objects + cy.request + localStorage token

Selenium atual

request fixture + helpers + JWT cache

Organização

api/gets/ · api/patch/ · fixtures JSON

CI

PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1

Service Objects — Cypress legado

Equivalente a POM, mas para API — encapsula URLs, auth e métodos HTTP.

export class ProfileApiClient {
  flushProfile() {
    return cy.request({
      method: 'PUT',
      url: this.url.flushProfile,
      headers: { Authorization: `Bearer ${localStorage.getItem('access_token')}` },
    }).its('status').should('eq', 204)
  }

  getCountries() {
    return cy.request({ method: 'GET', url: this.url.countries, headers: { /* … */ } })
  }
}

cy.wrap(new ProfileApiClient()).as('profileApi')

Auth cache — Cypress → Selenium

// Cypress — localStorage plugin
before(() => {
  preCondition.loginByApi({ username, password })
  cy.saveLocalStorage()
})
beforeEach(() => cy.restoreLocalStorage())
// Selenium — JWT parse + Map cache
export async function getOrRefreshToken(user) {
  const cached = tokenCache.get(user.username)
  if (cached && isTokenValid(cached)) return cached
  const token = await fetchTokenFromOAuth(user)
  tokenCache.set(user.username, token)
  return token
}

if (process.env.AUTH_TOKEN) return process.env.AUTH_TOKEN

test.use — override de usuário por suite

export const test = base.extend<{
  authToken: string
  testUser: TestUserCredentials | null
}>({
  testUser: [null, { option: true }],
  authToken: async ({ testUser }, use) => {
    const token = testUser
      ? await getOrRefreshTokenForUser(testUser)
      : await getOrRefreshBusinessToken()
    await use(token)
  },
})

test.use({ testUser: getTestUser('lendingPartner') })

Headers de contexto obrigatórios

export function buildApiHeaders(options: {
  profileContext: 'KYC' | 'OAA'
  lineOfBusiness?: 'Brokerage' | 'ConsumerLending'
}) {
  return {
    'X-Profile-Context': options.profileContext,
    'Correlation-Id': uuid(),
    'Session-Id': uuid(),
    'Client-Ip': '192.168.1.1',
    LineOfBusiness: options.lineOfBusiness ?? 'Brokerage',
    SourceLob: 'selenium-automation',
  }
}

Golden fixtures — contrato sem JSON Schema

export function withoutId(arr) {
  return arr.map(({ id, ...rest }) => rest)
}

export function expectSameMembers(expected, received) {
  // compara arrays ignorando ordem — diff legível no report
}

const { body } = await fetchLookup(request, authToken, '/v1/lookups/countries', testInfo)
expect(response.status()).toBe(200)
expect(withoutId(body)).toEqual(expectedCountriesFixture)

Data-driven — runPatchTests()

export function runPatchTests(context, { payloads, tag }) {
  for (const [label, payload] of Object.entries(payloads)) {
    describe(label, () => {
      it(`204 — PATCH accepted, GET profile matches`, async ({ request, authToken }) => {
        const patchRes = await request.patch(PATCH_PATH, { headers, data: payload })
        expect(patchRes.status()).toBe(204)
        const profile = await pollGetUntilMatch(request, GET_PATH, headers, payload)
        expect(profile).toMatchObject(normalizePayload(payload))
      })
    })
  }
}

runPatchTests('KYC', { payloads: EMPLOYED_PAYLOADS, tag: 'employmentCode' })

PATCH → poll GET — eventual consistency

async function pollGetUntilMatch(request, path, headers, expected, deadlineMs = 10_000) {
  const start = Date.now()
  while (Date.now() - start < deadlineMs) {
    const res = await request.get(path, { headers })
    if (res.status() === 200) {
      const body = normalizeProfile(await res.json())
      if (matchesExpected(body, expected)) return body
    }
    await new Promise((r) => setTimeout(r, 500))
  }
  throw new Error('GET did not match PATCH payload within deadline')
}

Verificação dual write + read

profileApi.patchCustomerName(nameBody).should(r => expect(r.status).to.eq(204))
profileApi.getBasicProfile().should(r => {
  expect(r.body.pendingProfile.customerName).to.deep.include(nameBody)
})
lifecycleApi.getBasicProfile().should(r => {
  expect(r.body.pendingProfile).to.deep.include(nameBody)
})

PATCH no Profile API + GET no Lifecycle Platform confirma propagação do estado pendente.

Diff + curl no report de falha

await testInfo.attach('comparison-diff', {
  body: buildValidationReport(label, expected, received),
  contentType: 'text/markdown',
})
await testInfo.attach('curl-replay', { body: buildCurl('PATCH', url, headers, body) })

Postman → Selenium migration

  • Pre-request script → buildApiHeaders()
  • Tests tab assertions → expect() + golden fixtures
  • Collection env vars → .env + CI/CD variables
  • JWT validation → isTokenValid(token, claims)

CI — env diagnostics & skip condicional

it('required CI variables are set', async () => {
  expect(process.env.CLIENT_ID, 'CLIENT_ID').toBeTruthy()
  expect(process.env.API_BASE_URL, 'API_BASE_URL').toBeTruthy()
})

beforeAll(async () => {
  test.skip(!process.env.OAUTH_CLIENT_ID, 'OAuth client not configured in CI')
})

Page Object Model (POM)

Encapsula seletores, ações e assertions — o spec descreve o cenário, não o DOM. Para API-only: use Service Objects ou helpers.

Spec (.test.js)

  • Given / When / Then
  • Sem seletores espalhados
  • Um cenário por it()
→

Page Object

  • getByTestId locators
  • Actions (loginWith)
  • Assertions (shouldRedirect…)

Anatomia — LoginPage.ts

export class LoginPage {
  constructor(private readonly page: Page) {}

  emailInput() { return this.helpers.getByTestId('login-email') }
  submitBtn()  { return this.helpers.getByTestId('login-submit') }

  async visit() {
    await this.page.goto('/web/login.html')
    return this
  }

  async loginWith(email: string, password: string) {
    await this.emailInput().fill(email)
    await this.passwordInput().fill(password)
    await this.submitBtn().click()
    return this
  }

  async shouldRedirectToDashboard() {
    await expect(this.helpers.getByTestId('page-dashboard')).toBeVisible()
    await expect(this.page).toHaveURL(/\/web\/dashboard\.html/)
  }
}

TeamPage — locators & ações

Locators
tableRows()users-table tbody tr
nameCell(id)cell-name-{id}
inviteModal()Modal de convite
Actions
search(term)Filtrar tabela
filterByRole(role)Select de role
openInviteModal()Abrir + assert visible
Assertions
shouldHaveRowCount(n)Contagem de linhas
shouldShowInviteError(text)Validação do modal

Page Objects do TestFlow

LoginPage.js auth, validação, API toggle
DashboardPage.js KPIs, activity, new run modal
TeamPage.js tabela, invite, inline edit
SettingsPage.js forms, toggles, save
WizardPage.js multi-step, validação, review

Specs de Components, Activity, Advanced e States usam helpers diretamente — sem Page Object dedicado ainda.

Waits do WebDriver

O Selenium WebDriver 4 usa waits explícitos em support/helpers.js. Cada chamada getByTestId aguarda o elemento existir e ficar visível antes de retornar.

const { until, By } = require('selenium-webdriver')

await driver.wait(until.elementLocated(By.css('[data-testid="page-dashboard"]')), 10000)
await driver.wait(until.elementIsVisible(element), 10000)

// Timeouts centralizados em support/enums/timeouts.js
await driver.manage().setTimeouts({
  implicit: 0,
  pageLoad: TIMEOUTS.PAGE_LOAD,
  script: TIMEOUTS.DEFAULT,
})
until.elementLocated until.elementIsVisible TIMEOUTS.DEFAULT

Hooks Mocha & reporters

tests/hooks.js registra hooks globais via .mocharc.cjs.

module.exports = {
  mochaHooks: {
    async beforeAll() {
      const driver = await getDriver()
      helpers = new Helpers(driver)
    },
    async afterEach() {
      if (this.currentTest?.state === 'failed') {
        const filePath = await helpers.takeScreenshot(`failed-${name}`)
        await attachFailureScreenshot(filePath) // Allure no CI
      }
    },
    async afterAll() {
      await quitDriver()
    },
  },
}

Runs locais: mochawesome → reports/selenium-report.html. CI: allure-mocha → allure-results/.

Allure no CI

O workflow .github/workflows/selenium.yml faz upload de resultados Allure dos jobs smoke, regression e suite completa. O job publish-allure mescla artifacts e publica o relatório no GitHub Pages.

npm run report:allure          # gerar localmente
npm run report:allure:serve    # gerar + abrir
npm run allure:merge-results   # mesclar artifacts do CI
npm run pages:prepare-allure   # preparar para GitHub Pages
publish-allure publish-pages screenshots on failure

Specs API + Page Objects

Suites de UI usam Page Objects; a suite API usa publicRequest — sem browser.

// API — tests/api/auth.api.test.js
const { publicRequest } = require('../../support/api')

before(async function () {
  res = await publicRequest('/api/auth/login', { method: 'POST', body: VALID })
})

// UI — tests/team/team.test.js
const TeamPage = require('../../pages/TeamPage')

beforeEach(async function () {
  const helpers = getHelpers()
  await createAuthSession(helpers)
  teamPage = new TeamPage(helpers)
  await teamPage.open()
})

Veja as seções Page Object Model e API testing abaixo.

Factories & fixtures de dados

fixtures/ (JSON)
credentials.jsonEmail/senha válidos e inválidos — login negativo
team-member.jsonPayload do modal invite
wizard.jsonDados do wizard multi-step
support/factories/
index.jsBuilders de payload wizard e invite
support/schemaValidator.js
validateWithSchema(obj, schema)Validação AJV para respostas API
config/env.js
resolveConfig()BASE_URL, credenciais demo, flag headless

Documentação de treinamento (bloco a bloco)

Cada spec tem um walkthrough detalhado com Given/When/Then, tags e comandos de execução. Escolha o idioma: English · Português

SuiteDoc ENDoc PTComando
Smokenavigation.mdnavigation.mdnpm run test:smoke
Authlogin.mdlogin.mdnpm run test:auth
Dashboarddashboard.mddashboard.mdnpm run test:dashboard
Teamteam.mdteam.mdnpm run test:team
Settingssettings.mdsettings.mdnpm run test:settings
Componentscomponents.mdcomponents.mdnpm run test:components
Wizardwizard.mdwizard.mdnpm run test:wizard
Activityactivity.mdactivity.mdnpm run test:activity
Advancedadvanced.mdadvanced.mdnpm run test:advanced
UI Statesstates.mdstates.mdnpm run test:states
API authauth.api.mdauth.api.mdnpm run test:api

Cobertura da suíte

SuiteSpecTagsO que cobre
smokenavigation.test.js@smoke @regressionPáginas, sidebar, health API, logout
authlogin.test.js@regression @criticalLogin UI, sessionStorage, erros, logout
dashboarddashboard.test.js—KPIs, activity, modal new run
teamteam.test.js—Busca, filtros, paginação, invite, inline edit
settingssettings.test.js—Profile, notifications, security, integrations
componentscomponents.test.js@regressionButtons, modal, tabs, accordion
wizardwizard.test.js—Multi-step, validação, review
activityactivity.test.js—API fetch, counter, pipeline, file drop
advancedadvanced.test.js@regressionShadow DOM, iframe, mobile viewport
statesstates.test.js—Skeleton, empty, error, partial load
apiauth.api.test.js@api @regressionPOST /api/auth/login contracts

11 specs · Chrome WebDriver · Mocha 11 + Chai · mochawesome / Allure

Como executar

Relatório HTML local

npm test
npm run report:open

Headed

npm run test:headed
npm run test:team
npm test                         # suite completa (11 specs)
npm run test:smoke               # tag @smoke
npm run test:regression          # tag @regression
npm run test:critical            # tag @critical
npm run test:auth                # login UI
npm run test:dashboard           # dashboard
npm run test:team                # team table
npm run test:settings            # settings forms
npm run test:components          # UI library
npm run test:wizard              # multi-step wizard
npm run test:activity            # dynamic activity page
npm run test:advanced            # shadow DOM / iframe
npm run test:states              # loading / empty / error
npm run test:api                 # auth.api.test.js
npm run report:allure            # relatório Allure (estilo CI)
npm run report:open              # abrir HTML mochawesome

CI — GitHub Actions

Jobs de CI

smoke, regression, selenium-run (suite completa) — Chrome headless contra TestFlow na porta 5050

Service container

qaschool/testflow:latest na porta 5050

Allure + Pages

publish-allure mescla resultados e publica Allure Report 3 no GitHub Pages a cada push em main

Artefatos

Resultados Allure por job; screenshots de falha quando testes quebram

Workflow: .github/workflows/selenium.yml · jobs: setup → smoke / regression / selenium-run → publish-pages → publish-allure

Boas práticas

  • Page Objects em pages/ — locators, actions e assertions isolados
  • helpers.getByTestId + data-testid — seletores estáveis no TestFlow
  • createAuthSession no beforeEach — specs de feature sem repetir login UI
  • before + resposta compartilhada — contratos API sem N requests idênticos
  • validateWithSchema — schemas AJV para respostas REST
  • publicRequest — testes API sem lançar browser
  • Fixtures JSON — dados determinísticos (credentials, team-member)
  • Um cenário por it() — falhas fáceis de diagnosticar no report
  • Tags @smoke / @regression / @critical nos títulos — filtrar com npm scripts
  • Waits explícitos em helpers.js — nunca confie em implicit wait (está em 0)
  • visitWithSession — navega autenticado para qualquer rota
  • Screenshots de falha anexados ao Allure no CI via tests/hooks.js
  • Factories em support/factories/ — payloads wizard reutilizáveis
  • Leia o walkthrough bloco a bloco em docs/pt/ ou docs/en/
  • npm run test:headed no dev — acompanhe o browser enquanto debuga

Próximos passos