Ivan Petrović open to interesting problems

I find the breaking point first. so production never has to.

Ivan Petrović — Performance Engineer · Senior QA Automation Engineer · QA Lead. 7+ years turning quality into code: frameworks built from scratch at two SaaS companies, performance pipelines that fail builds — not people — and 11+ engineers mentored along the way.

$ now leading QA & performance at Fuel Me — an AI-powered procurement platform with Fortune 500 clients; JMeter as the daily driver, Playwright suite built from zero

$ next pioneering AI/LLM testing workflows — intelligent test generation and suites that heal themselves when the product changes

$ creed performance is a feature, quality gates should fail builds — not people, and a flaky test is a bug with your name on it

// selected work — open source, fully documented, green in CI

01 / performance engineering

Locust Performance Lab

Performance testing as code. A self-contained load testing framework that ships its own misbehaving system under test — one command spins up distributed load generation, live Grafana monitoring, automated memory-leak hunting, and an SLA gate that fails the CI build when latency budgets are breached.

View repo →
PythonLocust 2.xDockerFastAPIPrometheusGrafanaGitHub Actions

SLA gate — perf tests that can fail the build

Latency budgets live in slas.yml, per endpoint. A script parses Locust's CSV, prints a verdict table into the GitHub job summary, and exits non-zero on any breach. “A dashboard needs a human — an exit code scales.”

ScopeMetricActualLimit
POST /checkoutp95320 ms≤ 1500 msPASS
GET /search?q=[term]p95780 ms≤ 1000 msPASS
Aggregatederror rate0.00 %≤ 2.0 %PASS

🧠 Memory-leak hunter

Samples container memory under constant load, discards warm-up, fits a least-squares trend — and fails only when both growth rate and total growth blow their budgets. Demonstrated against a simulated ~50 KB/request leak:

leak on+95.5 MB · +2351 MB/h → exit 1
leak off+2.5 MB · stable → exit 0

Load shapes as ~15 lines of code

Stress, spike and soak profiles are LoadTestShape classes selected by one env var — one reviewed workload model serves every scenario.

class SpikeShape(LoadTestShape):
    """20-user baseline, sudden 10× spike."""
    def tick(self):
        t = self.get_run_time()
        if t < 120: return (20, 5)
        if t < 180: return (200, 50)  # the spike
        if t < 360: return (20, 50)   # recovery
        return None

📉 Regression detection, run over run

One run tells you where you are; comparing runs tells you where you're heading. Nightly CI diffs endpoint-by-endpoint and fails on p95 growth, throughput drops, or worsening error rate:

Endpointp95 base → nowΔ p95
GET /products/[id]67 → 72 ms+7.5 %OK
POST /checkout340 → 320 ms−5.9 %OK
Aggregated310 → 160 ms−48.4 %OK

🐳 Distributed by default

make up WORKERS=8 — Locust master/worker split in plain Docker Compose, FastHttpUser for ~10× RPS per generator. Same images scale across hosts or Kubernetes.

📊 Live observability

Pre-provisioned Grafana dashboard: RPS, p50/p95 overlaid with the user ramp, failures/s, and the leak-hunting memory panel — problems visible the moment they start, not in a PDF afterwards.

👥 Realistic workload model

75% browsers, 25% buyers running the full funnel as a SequentialTaskSet; catch_response marks a 200-without-order_id as a failure — wrong answers delivered quickly are still wrong.

smoke · baseline · stress · spike · soak · leak-test · compare — every scenario is one make target, every gate is an exit code

02 / test automation

Booking.com Automation Framework

E2E that survives a live production site. Playwright + TypeScript framework running green against the real booking.com — anti-bot protection, A/B DOM swaps, white-label variants and all — covering Stays, Flights, Car rentals and Attractions, plus accessibility and API-level checks.

View repo →
Playwright 1.60TypeScript strictPOMaxe-coreESLint 9Sharded CI

🩹 Self-healing locator engine

Every element is an ElementDefinition with ordered fallback candidates. When the primary selector dies mid-day (booking.com A/B tests constantly), candidates are polled round-robin, the winner is logged as a healing event, and non-volatile heals are persisted to a git-committed store so future runs try them first.

destinationInput: {
  key: 'stays.searchbox.destinationInput',
  candidates: [
    'input[name="ss"]',                            // primary
    '[data-testid="destination-container"] input', // fallback 1
    'input[placeholder*="Where are you going"]',   // fallback 2
  ],
}

📡 A real heal, captured in CI

Worker processes append events to their own files; a custom reporter consolidates them after the run. The engine is itself covered by E2E tests.

{
  "key": "flights.results.flightCards",
  "failed": ["[data-testid=…_card]"],
  "healed": "[aria-label^='Result item']",
  "persistPreferred": true
}

🩹 healed → logged → persisted → suite stays green

🌍 Live-site quirks, engineered around

  • Flights served as Kayak white-label in some regions — variant auto-detected, stable deep-link scheme used
  • Sign-in popup at random moments — addLocatorHandler auto-dismisses whenever it blocks
  • /flights/ redirect trap — navigation enters via the header tab, like a real user
  • A/B DOM swaps — absorbed by the healer, variants kept as candidates

🧱 Layered architecture

tests/*.spec.tsintent only — arrange data, call page methods, assert
fixturespage objects + auto fixtures (tracker blocking, console-error capture)
page objectslocators + user actions · never assert
healing enginecandidate resolution + persistence

Plus a data layer with fluent builders — dates always generated relative to “now”, so the suite never rots from hardcoded test data.

Beyond the happy path

axe-core WCAG 2.0 A/AA scan gating on critical violations, toMatchAriaSnapshot structural assertions, and APIRequestContext canaries at the HTTP level.

⚙️ CI built to scale

Static checks fail fast → sharded test matrix → blob reports merged into one HTML report. PR annotations, nightly cron, manual dispatch with tag filter. Scaling = extending the shard matrix.

🏷 14 tests, 9 suites, 3 browsers

Tag-driven: @smoke @stays @flights @cars @attractions @a11y @api @healer compose with --grep regexes. Verified green against the live site, Chromium / Firefox / WebKit.

page objects never assert · specs never touch selectors · selectors heal themselves

03 / visual regression

Visual Comparison

Pixel-perfect UI regression testing. Catches the layout and styling breakage functional tests walk straight past — and proves it by diffing a clean build against an intentionally broken one. A data-driven registry keeps the jump from ~30 snapshots to 1000+ linear: one array row becomes a test across every project.

View repo →
Playwright 1.61TypeScript 6Page Object ModelDockerSharded CI3 browsers · 2 devices

🔍 Proven to catch real bugs

A screenshot test is only convincing if it catches something. The same suite runs against two deploys of the Toolshop app — a clean build and an intentionally broken fork — backed by identical data, so any pixel difference is a planted defect the suite must flag. On the with-bugs build the affected pages light up red — a shifted layout and typos like Contakt and Massage all caught. The trick: there a passing run would be the real bug.

BuildRoleRunVerdict
cleanbaseline sourcepasses✓ match
with-bugssame testsfails✓ caught

🧬 Data-driven registry — add a row, get a test

Coverage lives in one array; a single generated spec turns each entry into a test, multiplied across every project. Adding a screenshot is one object, not a new file — the design that keeps the jump to 1000+ snapshots linear.

// pageRegistry.ts — one entry = one test
{
  category: 'home',
  name: 'home-grid',
  tags: ['@smoke', '@visual'],
  run: async ({ homePage }) => {
    await homePage.open();
  },
}

🧊 Flake frozen from one place

Every visual assertion inherits one tuned config: animations and caret killed, a shared stylesheet that freezes transitions and hides the polling chat widget, and a resolution-independent tolerance that absorbs anti-aliasing noise without hiding real change.

toHaveScreenshot: {
  animations: 'disabled',
  caret: 'hide',
  scale: 'css',
  stylePath: './tests/support/visual-stabilize.css',
  threshold: 0.2,
  maxDiffPixelRatio: 0.01,
}

🤖 The CI proves the suite still bites

A 4-way sharded matrix runs in the pinned Playwright container. Each shard records baselines from the clean build, sanity-checks the clean build against them (no false positives), then runs the with-bugs build and asserts diffs were found — a meta-check that fails CI if zero regressions are flagged, so an over-tolerant threshold can never let the visual suite go silently blind. Blob reports merge into one HTML report with every side-by-side diff.

clean vs baselinesmust match → no false positives
with-bugs vs baselinesmust differ → else CI fails

🖥 Cross-browser & responsive

Six flows run across five projects: Chromium, Firefox and WebKit at one 1366×768 desktop viewport, plus Chromium-based Galaxy Tab S4 and Pixel 7. Selectors target data-test via getByTestId — immune to CSS/text refactors.

🐳 Deterministic pixels in Docker

Fonts and anti-aliasing differ across macOS, Windows and Linux, so baselines drift between machines. The pinned playwright:v1.61.1-noble image renders identically within tolerance on Linux locally and in CI — container baselines match CI exactly. No Percy or Applitools SaaS needed.

🔒 Baselines under control

updateSnapshots: 'none' in CI fails on any drift; baseline:changed rewrites only what genuinely differs for clean diffs. At thousands of PNGs the snapshot tree can move to Git LFS to keep clones fast.

record clean → sanity-check → diff with-bugs → assert caught → merge — CI fails if the suite stops catching bugs

// where this was forged

Experience

Jun 2024 — now

Performance Engineer · Senior QA Automation · Team Lead @ Fuel Me

AI-powered fuel procurement platform, Fortune 500 clients, Series A.

  • Own platform performance end to end: load, stress, spike, endurance & scalability with JMeter against APIs and full user journeys — bottlenecks and memory leaks hunted at production scale, SLAs and baselines set, perf checks wired into CI/CD
  • Built the entire automation framework from scratch (Playwright + Node.js) — 100% of regression automated
  • Pioneered AI/LLM workflows for test-case generation, script creation and auto-healing
  • Leading a QA team of 3 — strategy, mentoring, code reviews
Apr 2020 — Jun 2024

QA Engineer @ SynergySuite

Restaurant management platform for global enterprise chains.

  • Joined a 100% manual team → built the automation framework from scratch (Selenium, Java, TestNG)
  • Mentored 8 QA engineers through the move to automation; by departure, 100% of regression was automated
  • Web + mobile (iOS/Android), API testing, SQL validation, CI/CD with Jenkins & GitHub Actions
Sep 2019 — Apr 2020

Technical Support Specialist @ SynergySuite

L1/L2 for enterprise clients across time zones — logs, SQL, bug reproduction.

$ cat education.txt BSc Applied Computer Science · University of Montenegro · 2010–2013

// tools are interchangeable, judgment isn't

Stack

⚡ performance/

JMeter (daily driver) · Locust · k6 · Gatling — load, stress, spike, soak; bottleneck analysis, memory-leak detection, SLAs & baselines, CI perf gates

🤖 automation/

Playwright · Selenium · TestNG — framework architecture, self-healing strategies, API testing, mobile (iOS/Android), cross-browser

📈 observability/

Grafana · Prometheus · OpenTelemetry · Sentry — dashboards as code, root-cause analysis of latency spikes and throughput degradation

💻 languages/

TypeScript · JavaScript · Node.js · Java · Python · SQL · PHP

🧠 ai-devops/

Custom AI/LLM test workflows · intelligent test generation · GitHub Actions · Jenkins · Docker · Linux

👥 leadership/

Team lead ×2 · 11+ engineers mentored · code review culture · Agile/Scrum · JIRA · Confluence

// ping me

Let's make something
fast and unbreakable.