Selenium — Step-by-step guide
From zero to full reference: Selenium WebDriver 4, Mocha 11, Chai, Page Objects, API tests, explicit waits, and every npm script in the testflow-selenium suite.
E2E · API · POM · Mocha · Chai · testflow-seleniumWhat is Selenium?
Selenium WebDriver 4
drives a real browser for end-to-end testing. This project pairs it with
Mocha 11,
Chai, and
Chrome
(headless in CI, headed locally).
- Written in JavaScript (Node.js 20+)
- Explicit waits via
until.elementLocated/elementIsVisible - Global
getHelpers()fromtests/hooks.js— shared WebDriver per run - mochawesome HTML report locally; allure-mocha in CI
- Page Objects in
pages/+publicRequestfor API-only specs
Why use Selenium?
Real browser E2E
Chrome WebDriver with headed (test:headed) or headless CI runs.
Stable locators
helpers.getByTestId on data-testid — centralized in Page Objects.
Mocha + Chai
describe / it structure with Chai expect assertions.
UI + API in one repo
Page Objects for UI; publicRequest for REST contracts without a browser.
Prerequisites
Environment
- Node.js 20+
- npm (or pnpm / yarn)
-
TestFlow app on port
5050 - Editor: VS Code / Cursor (Selenium extension optional)
Environment setup by platform
Before cloning the repository, install
Node.js 20+,
Git, and
Docker
(to run TestFlow on port 5050).
Project steps (npm install, browsers) are the same on every OS.
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
Installers: 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
After npm install: npx selenium install-deps (browser system libs). Fedora: dnf install nodejs git docker.
# TestFlow — same on macOS, Windows, and Linux
docker run --rm -p 5050:5050 qaschool/testflow:latest
Installation — testflow-selenium
-
Clone the repository
git clone https://github.com/lflucasferreira/testflow-selenium.git
cd testflow-seleniumGit GitHub -
Install dependencies
npm installnpm Node.js -
Chrome browser
Install Google Chrome (or Chromium). WebDriver 4 manages the driver automatically via Selenium Manager.Chrome Selenium 4 -
Run tests
npm testornpm run test:headedMocha npm scripts
BASE_URL, SELENIUM_DEMO_EMAIL, and SELENIUM_DEMO_PASSWORD via
config/env.js or environment variables. See the project README for CI secrets.
Project structure
testflow-selenium/
├── config/env.js # BASE_URL, credentials, headless, window size
├── docs/
│ ├── en/tests/ # EN walkthroughs (block-by-block)
│ └── pt/tests/ # PT walkthroughs
├── fixtures/ # credentials.json, team-member.json, schemas/
├── pages/ # Page Object Model (LoginPage, TeamPage, …)
├── support/
│ ├── driver.js # Chrome WebDriver builder
│ ├── helpers.js # getByTestId, waits, screenshots
│ ├── auth.js # createAuthSession, visitWithSession
│ ├── api.js # publicRequest helper
│ └── factories/ # wizard / user payload builders
├── tests/
│ ├── api/ # auth.api.test.js
│ ├── auth/ smoke/ dashboard/ team/ settings/
│ ├── components/ wizard/ activity/ advanced/ states/
├── tests/hooks.js # Mocha global hooks (driver lifecycle)
├── scripts/wd-run.sh # npm test wrapper (--grep, --spec)
├── .mocharc.cjs
└── package.json
pages/— selectors, actions, and assertions per screensupport/auth.js— session via API or UI logindocs/en/tests/— block-by-block explanation of each spectests/*.test.js— 11 specs · Mocha + Chaidata-testid— stable selector convention
Configuration — .mocharc.cjs
module.exports = {
require: ['tests/hooks.js'],
timeout: 120000,
reporter: process.env.CI ? 'allure-mocha' : 'mochawesome',
spec: 'tests/**/*.test.js',
}
tests/hooks.js creates one Chrome WebDriver per run and exposes
getHelpers() globally. Locally, open the mochawesome HTML report under
reports/; in CI, Allure results go to allure-results/.
Filter by tag with npm run test:smoke or npm run test:regression
(--grep '@smoke' / '@regression' via scripts/wd-run.sh).
Your first test
// 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()
})
})
Convention: data-testid="login-email" →
helpers.getByTestId('login-email') inside Page Objects.
Locators & navigation
Selenium API used in the TestFlow suite — selectors via data-testid.
| API | TestFlow usage |
|---|---|
driver.get(url) | Open login, dashboard, team… |
helpers.getByTestId(id) | Default selector — login-email, page-team |
helpers.clickByTestId(id) | Submit, sidebar links, modals |
helpers.typeByTestId(id, text) | Email, password, search fields |
helpers.getTextByTestId(id) | Visible labels, error messages |
driver.findElements(By.css(...)) | tbody tr, table rows |
helpers.executeScript(...) | sessionStorage after login |
helpers.readSessionStorage(key) | Assert auth token persisted |
driver.navigate().refresh() | Reload after state change |
helpers.takeScreenshot(name) | Failure capture (hooks attach in CI) |
Interactions
| Action | TestFlow example |
|---|---|
helpers.typeByTestId(...) | Email, password, table search |
element.clear() | Clear search before new filter |
helpers.clickByTestId(...) | Submit, sidebar, pagination, modals |
helpers.selectByTestId(...) | Role filter, invite role, run suite |
checkbox.click() | Remember me, API toggle |
Key.ESCAPE | Close invite / new run modal |
element.getText() | Extract numeric KPI value |
element.isDisplayed() | Conditional — toast or result msg |
helpers.executeScript(...) | Checkbox covered by custom label |
driver.actions().sendKeys(...) | Keyboard shortcuts in UI |
Assertions — Chai expect()
| Assertion | Example |
|---|---|
expect(x).to.be.true | Submit enabled, checkbox state |
expect(text).to.include(...) | Errors, greeting, row count label |
expect(url).to.include(...) | Post-login redirect |
expect(value).to.equal(...) | Placeholder, password type |
expect(arr).to.have.length(n) | Visible rows, activity items |
expect(text).to.match(/regex/) | Pass rate ^\d+%$ |
expect(res.status).to.equal(200) | API contracts |
expect(duration).to.be.below(ms) | API response SLA |
waitForVisible, getByTestId) with Chai assertions —
Selenium does not auto-retry assertions like Playwright expect(locator).
Shared helpers
| support/helpers.js | |
getByTestId(testId) | Wait + return visible element |
clickByTestId · typeByTestId | Common interactions |
takeScreenshot(name) | Saves PNG under screenshots/ |
readSessionStorage(key) | Auth state after login |
| support/auth.js | |
createAuthSession(helpers) | Programmatic auth via API |
visitWithSession(helpers, path) | Navigate already authenticated |
clearSession(helpers) | Reset before login specs |
| support/api.js | |
publicRequest(path, opts) | HTTP helper for API specs (no browser) |
loadFixture(name) | Load JSON from fixtures/ |
| fixtures/ | |
credentials.json | valid / invalid email and password |
team-member.json | Invite modal payload |
schemas/*.json | AJV schemas for API responses |
Selector strategy
This project uses data-testid exclusively on the TestFlow app — no component tests.
See also docs/selector-strategy.md.
| Context | Preferred locator | Notes |
|---|---|---|
| TestFlow elements | helpers.getByTestId(...) | Primary strategy — stable, defined by the app |
| Modals, tabs (ARIA) | helpers.getByTestId(...) | Complements test IDs where ARIA roles exist |
| Internal state (CSS) | page.locator('.class') | Only when no test ID (.active, .spinner) |
| Shadow DOM / iframes | frameLocator() + test ID | See advanced.test.js |
| Select2, SweetAlert2 | Vendor CSS | .select2-container, .swal2-popup — last resort |
pages/ are the single source of locators — never scatter selectors in specs.
Spec structure
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 — fresh state per test
beforeEach(async function () {
const helpers = getHelpers()
await createAuthSession(helpers)
await helpers.driver.get(`${config.baseUrl}/web/dashboard.html`)
})
// before — shared API response (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 avoids N identical API calls;
beforeEach isolates UI state between scenarios.
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)
})
})
A loop generates N named tests — smoke covers all routes with a single pattern.
Explicit waits — stable UI checks
const { until, By } = require('selenium-webdriver')
// helpers.getByTestId wraps 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
}
// Page Object assertion — wait first, then read state
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}`)
}
}
Always wait for elements before interacting. Implicit wait is set to 0 in
support/driver.js — explicit waits in helpers keep failures diagnosable.
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 — REST contract
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)
Equivalent to Cypress cy.intercept / Playwright page.route. Requires Chrome. Attached in tests/hooks.js as helpers.net.
const { interceptLogin, stubLoginFailure, mockApiGet } = require('./support/network/presets')
const { waitForIntercept, assertResponseSchema } = require('./support/network/assertions')
// Phase 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)
// Phase 2 — Mock empty users
await mockApiGet(helpers.net, 'users/empty-list', /\/api\/users/)
await helpers.clickByTestId('fetch-users-btn')
await waitForIntercept(helpers.net, 'mock_users_empty-list')
// Phase 2 — Mock 500 login
await stubLoginFailure(helpers.net, 500)
// Phase 3 — Ajv contract
assertResponseSchema(call, loadFixture('schemas/auth-login.json'))
UI intercept — Team & Settings (optional)
TestFlow invite/password/rotate are UI-only today. Use optional helpers.net.get(alias) like Cypress if (interception).
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 — fast setup
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()
}
Used in dashboard, team, settings — specs focus on the feature, not login.
Custom fixtures
Replaces global commands — injects token and headers into every 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 — typed status codes
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,
})
}
- Centralized URLs — no scattered strings
- 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 …' }, // masked
response,
})
// Attaches: status, headers, JSON body, reproducible curl
globalSetup — session cache
export default defineConfig({
globalSetup: require.resolve('./globalSetup'),
})
// tests/hooks.js — runs once before all workers
// 1. Reuse token from .selenium/token-cache.json
// 2. Login via API
// 3. Save storageState to .selenium/.auth/user.json
Equivalent to cy.session — heavy login once, reused across N specs.
Enterprise API — overview
API-only suite: no browser, no UI — focus on REST contracts, OAuth, and cross-service sync.
Cypress legacy
Service Objects + cy.request + localStorage token
Current Selenium
request fixture + helpers + JWT cache
Organization
api/gets/ · api/patch/ · JSON fixtures
CI
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
Service Objects — Cypress legacy
POM equivalent for API — encapsulates URLs, auth, and HTTP methods.
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 — per-suite user override
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') })
Required context headers
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 — contract without JSON Schema
export function withoutId(arr) {
return arr.map(({ id, ...rest }) => rest)
}
export function expectSameMembers(expected, received) {
// compare arrays ignoring order — readable diff in 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')
}
Dual write + read verification
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 on Profile API + GET on Lifecycle Platform confirms pending state propagation.
Diff + curl in failure report
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 & conditional skip
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)
Encapsulates selectors, actions, and assertions — the spec describes the scenario, not the DOM. For API-only: use Service Objects or helpers.
Spec (.test.js)
- Given / When / Then
- No scattered selectors
- One scenario per
it()
Page Object
getByTestIdlocators- Actions (
loginWith) - Assertions (
shouldRedirect…)
Anatomy — 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 & actions
| Locators | |
tableRows() | users-table tbody tr |
nameCell(id) | cell-name-{id} |
inviteModal() | Invite modal |
| Actions | |
search(term) | Filter table |
filterByRole(role) | Role select |
openInviteModal() | Open + assert visible |
| Assertions | |
shouldHaveRowCount(n) | Row count |
shouldShowInviteError(text) | Modal validation |
TestFlow Page Objects
Components, Activity, Advanced, and States specs use helpers directly — no dedicated Page Object yet.
WebDriver waits
Selenium WebDriver 4 uses explicit waits in support/helpers.js. Every
getByTestId call waits for the element to exist and be visible before returning.
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 centralized in support/enums/timeouts.js
await driver.manage().setTimeouts({
implicit: 0,
pageLoad: TIMEOUTS.PAGE_LOAD,
script: TIMEOUTS.DEFAULT,
})
Mocha hooks & reporters
tests/hooks.js registers global hooks 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 in CI
}
},
async afterAll() {
await quitDriver()
},
},
}
Local runs: mochawesome
→ reports/selenium-report.html.
CI: allure-mocha
→ allure-results/.
Allure in CI
Workflow .github/workflows/selenium.yml uploads Allure results from
smoke, regression, and full-suite jobs. Job publish-allure merges artifacts
and deploys the report to GitHub Pages.
npm run report:allure # generate locally
npm run report:allure:serve # generate + open
npm run allure:merge-results # merge CI artifacts
npm run pages:prepare-allure # stage for GitHub Pages
API specs + Page Objects
UI suites use Page Objects; API suite uses publicRequest — no browser required.
// 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()
})
See Page Object Model and API testing sections below.
Factories & data fixtures
| fixtures/ (JSON) | |
credentials.json | Valid/invalid email and password — negative login |
team-member.json | Invite modal payload |
wizard.json | Multi-step wizard data |
| support/factories/ | |
index.js | Wizard and invite payload builders |
| support/schemaValidator.js | |
validateWithSchema(obj, schema) | AJV validation for API responses |
| config/env.js | |
resolveConfig() | BASE_URL, demo credentials, headless flag |
Training documentation (block-by-block)
Each spec has a detailed walkthrough with Given/When/Then, tags, and run commands. Choose language: English · Português
| Suite | Doc EN | Doc PT | Command |
|---|---|---|---|
| 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 |
Suite coverage
| Suite | Spec | Tags | Covers |
|---|---|---|---|
smoke | navigation.test.js | @smoke @regression | Pages, sidebar, health API, logout |
auth | login.test.js | @regression @critical | Login UI, sessionStorage, errors, logout |
dashboard | dashboard.test.js | — | KPIs, activity, new run modal |
team | team.test.js | — | Search, filters, pagination, 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, validation, 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
How to run
Local HTML report
npm test
npm run report:open
Headed
npm run test:headed
npm run test:team
npm test # full suite (all 11 specs)
npm run test:smoke # @smoke tag
npm run test:regression # @regression tag
npm run test:critical # @critical tag
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 # Allure report (CI-style)
npm run report:open # open mochawesome HTML
CI — GitHub Actions
CI jobs
smoke, regression, selenium-run (full suite) —
Chrome headless against TestFlow on port 5050
Service container
qaschool/testflow:latest on port 5050
Allure + Pages
publish-allure merges results and deploys Allure Report 3 to GitHub Pages on every push to main
Artifacts
Allure results per job; failure screenshots uploaded when tests fail
Workflow: .github/workflows/selenium.yml · jobs: setup → smoke / regression / selenium-run → publish-pages → publish-allure
Best practices
- Page Objects in
pages/— isolated locators, actions, and assertions helpers.getByTestId+data-testid— stable selectors on TestFlowcreateAuthSessioninbeforeEach— feature specs without repeating UI loginbefore+ shared response — API contracts without N identical requestsvalidateWithSchema— AJV schemas for REST responsespublicRequest— API tests without launching a browser- JSON fixtures — deterministic data (
credentials,team-member) - One scenario per
it()— easy-to-diagnose failures in the report - Tags
@smoke/@regression/@criticalin test titles — filter with npm scripts - Explicit waits in
helpers.js— never rely on implicit wait (set to 0) visitWithSession— navigate authenticated to any route- Failure screenshots attached to Allure in CI via
tests/hooks.js - Factories in
support/factories/— reusable wizard payloads - Read the block-by-block walkthrough in docs/en/ or docs/pt/
npm run test:headedin dev — watch the browser while debugging