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-seleniumO que é o Selenium?
O Selenium WebDriver 4
controla um browser real para testes end-to-end. Este projeto combina com
Mocha 11,
Chai e
Chrome
(headless no CI, headed localmente).
- Escrito em JavaScript (Node.js 20+)
- Waits explícitos via
until.elementLocated/elementIsVisible getHelpers()global detests/hooks.js— WebDriver compartilhado por run- mochawesome localmente; allure-mocha no CI
- Page Objects em
pages/+publicRequestpara specs só de API
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)
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
Windows
winget install OpenJS.NodeJS.LTS
winget install Git.Git
winget install Docker.DockerDesktop
node --version
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
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
-
Clone o repositório
git clone https://github.com/lflucasferreira/testflow-selenium.git
cd testflow-seleniumGit GitHub -
Instale dependências
npm installnpm Node.js -
Chrome browser
Instale Google Chrome (ou Chromium). O WebDriver 4 gerencia o driver via Selenium Manager.Chrome Selenium 4 -
Execute os testes
npm testounpm run test:headedMocha npm scripts
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 telasupport/auth.js— sessão via API ou login UIdocs/en/tests/— explicação bloco a bloco de cada spectests/*.test.js— 11 specs · Mocha + Chaidata-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.
| API | Uso 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ção | Exemplo 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.ESCAPE | Fechar 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()
| Assertion | Exemplo |
|---|---|
expect(x).to.be.true | Submit 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 |
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 · typeByTestId | Interaçõ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.json | valid / invalid email e password |
team-member.json | Payload do modal invite |
schemas/*.json | Schemas 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.
| Contexto | Locator preferido | Notas |
|---|---|---|
| Elementos TestFlow | helpers.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 / iframes | frameLocator() + test ID | Ver advanced.test.js |
| Select2, SweetAlert2 | CSS vendor | .select2-container, .swal2-popup — último recurso |
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', () => { /* … */ })
})
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' })
}
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'))
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
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
getByTestIdlocators- 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
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,
})
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
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.json | Email/senha válidos e inválidos — login negativo |
team-member.json | Payload do modal invite |
wizard.json | Dados do wizard multi-step |
| support/factories/ | |
index.js | Builders 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
| Suite | Doc EN | Doc PT | Comando |
|---|---|---|---|
| Smoke | navigation.md | navigation.md | npm run test:smoke |
| Auth | login.md | login.md | npm run test:auth |
| Dashboard | dashboard.md | dashboard.md | npm run test:dashboard |
| Team | team.md | team.md | npm run test:team |
| Settings | settings.md | settings.md | npm run test:settings |
| Components | components.md | components.md | npm run test:components |
| Wizard | wizard.md | wizard.md | npm run test:wizard |
| Activity | activity.md | activity.md | npm run test:activity |
| Advanced | advanced.md | advanced.md | npm run test:advanced |
| UI States | states.md | states.md | npm run test:states |
| API auth | auth.api.md | auth.api.md | npm run test:api |
Cobertura da suíte
| Suite | Spec | Tags | O que cobre |
|---|---|---|---|
smoke | navigation.test.js | @smoke @regression | Páginas, sidebar, health API, logout |
auth | login.test.js | @regression @critical | Login UI, sessionStorage, erros, logout |
dashboard | dashboard.test.js | — | KPIs, activity, modal new run |
team | team.test.js | — | Busca, filtros, paginação, invite, inline edit |
settings | settings.test.js | — | Profile, notifications, security, integrations |
components | components.test.js | @regression | Buttons, modal, tabs, accordion |
wizard | wizard.test.js | — | Multi-step, validação, review |
activity | activity.test.js | — | API fetch, counter, pipeline, file drop |
advanced | advanced.test.js | @regression | Shadow DOM, iframe, mobile viewport |
states | states.test.js | — | Skeleton, empty, error, partial load |
api | auth.api.test.js | @api @regression | POST /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 TestFlowcreateAuthSessionnobeforeEach— specs de feature sem repetir login UIbefore+ resposta compartilhada — contratos API sem N requests idênticosvalidateWithSchema— schemas AJV para respostas RESTpublicRequest— 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/@criticalnos 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:headedno dev — acompanhe o browser enquanto debuga