← Back home

🔍 QA & Testing Portfolio

I don't just build software — I try to break it, on purpose, before someone else does by accident. Below is a real test plan and a set of bug reports (some found and fixed, some found and filed) from testing Pros & Cons, SaiKirthan, Amazon Order Tracker, and this portfolio site itself, written up the way I'd document them for a team: a standard bug report (environment, repro steps, expected vs. actual, evidence, severity/priority) followed by a separate Gherkin-format regression test for each one.

Test Plan

Objective
Validate functional correctness and production-readiness of both apps across their full build & deploy pipeline, not just their code.
Scope
Web builds of both apps: core user flows, auth/sync, CI/CD pipeline correctness, cross-build-mode parity (debug/profile/release), and a targeted accessibility & data-integrity pass.
Approach
Automated (static analysis, unit/widget tests, headless-browser smoke tests across build modes) plus manual/exploratory testing (live-URL verification post-deploy, CI log review, targeted code review for edge cases).
Tools
flutter analyze, flutter test, Playwright (headless Chromium), GitHub Actions CI, manual cross-browser checks.
Environments
Headless Chromium, GitHub Actions (ubuntu-latest), and live production URLs on GitHub Pages + custom domain.
Exit criteria
Live production URL returns 200 with zero console errors, and the core flow (record → transcribe → save → view) is verified end-to-end.

Microphone button fails with zero feedback

Pros & Cons · found via user-reported issue during real-world usage
Environment
Pros & Cons web build, desktop browser, real-world usage (user-reported).
Steps to Reproduce
  1. Open the app in a browser with no microphone permission granted yet.
  2. Tap the microphone button.
  3. Deny the browser's permission prompt (or use a browser without Web Speech API support).
Expected Result
A clear message explains what went wrong (permission denied, unsupported browser, etc.) and the button returns to its idle state.
Actual Result
Nothing happens. Every speech-recognition failure path only called print(), which is invisible in a production build - the user has no way to know whether to retry, grant permission, or switch browsers.
Happening in ProdEnvironmentOccurrence / Reproducibility
YesProdAlways (5/5)
Evidence
Browser dev-tools console showed only print() output on failure, with no UI change at all - confirmed across a denied-permission case and an unsupported-browser case.
Impact
This is a voice-note app - the mic button failing invisibly means the app's entire core feature appears completely broken, with no diagnostic path for the user or for whoever triages their bug report next.
Severity & Priority
High severity - core feature appears fully broken with zero feedback. Priority: fix immediately.
Fixed: added error tracking and a live onError callback so every failure now surfaces a specific SnackBar message instead of failing silently.

Production release build renders a blank white screen

Pros & Cons · found via cross-build-mode parity testing
Environment
Pros & Cons, flutter build web --release artifact, any browser, with Firebase Auth + Firestore configured.
Steps to Reproduce
  1. Build the app with flutter build web --release (using the existing --no-web-resources-cdn flag).
  2. Serve the build output and open it in a browser.
  3. Compare against the same app run via flutter run (debug) or a profile build.
Expected Result
The app UI renders within 5 seconds with no uncaught JavaScript errors, identically across debug, profile, and release builds.
Actual Result
The release build shows a completely blank white screen. Debug and profile builds render correctly - the break only exists in the release artifact.
Happening in ProdEnvironmentOccurrence / Reproducibility
YesProd (release build)Always (5/5)
Evidence
Loading the release build in a real browser showed zero DOM content painted; isolating build flags one at a time traced it to --no-web-resources-cdn specifically, independent of dart2js optimization level (confirmed at O2/O3/O4).
Impact
The production site would have been fully down for every visitor, with the break invisible to the standard dev workflow - it only showed up by explicitly testing the actual release artifact in a real browser.
Severity & Priority
Critical severity - full production outage for every visitor. Priority: fix immediately, block release.
Fixed: removed --no-web-resources-cdn from the deploy workflow and documented the root cause inline as a guardrail against reintroducing it.

App deployed to a broken, double-nested URL

SaiKirthan · found via post-deployment smoke testing
Environment
SaiKirthan deploy workflow (GitHub Actions), published output on the shared portfolio hub (GitHub Pages).
Steps to Reproduce
  1. Trigger the SaiKirthan deploy workflow and wait for it to report success.
  2. Navigate to the app's advertised hub URL.
  3. Compare the actual published path against the intended /SaiKirthan/ path.
Expected Result
The advertised hub URL returns a 200 response with the app's content.
Actual Result
The build was pushed into a double-nested /SaiKirthan/ subfolder of a Pages site whose root was already effectively "SaiKirthan," so nothing was ever actually published to the shared portfolio site at the advertised URL.
Happening in ProdEnvironmentOccurrence / Reproducibility
YesProdAlways (5/5)
Evidence
CI reported green on every run; loading the actual published hub URL returned the wrong content/404, while the double-nested path returned the app - confirming the workflow's destination path, not the build itself, was wrong.
Impact
A textbook "green CI, red reality" gap - the app was unreachable at its advertised address for an unknown period despite every automated check passing.
Severity & Priority
High severity - app fully unreachable at its published URL. Priority: fix immediately.
Fixed: rewrote the workflow to publish into the shared hub repo with the correct destination path, and removed a conflicting custom-domain entry it had also been writing.

Deploy blocked by a stale environment protection rule

SaiKirthan · found via CI log inspection
Environment
SaiKirthan repo, GitHub Actions, github-pages deployment environment, immediately after migrating from a legacy branch-based Pages deploy to the Actions-based method.
Steps to Reproduce
  1. Push to main after migrating the repo to Actions-based Pages deployment.
  2. Let the deploy workflow attempt to deploy to the github-pages environment.
  3. Inspect the run's log output.
Expected Result
The workflow run on main is permitted to deploy and completes successfully.
Actual Result
Every deploy was silently rejected with "Branch main is not allowed to deploy to github-pages due to environment protection rules" - a leftover restriction from the old setup that only allowlisted the legacy branch.
Happening in ProdEnvironmentOccurrence / Reproducibility
YesProd (CI/CD)Always (5/5)
Evidence
GitHub Actions run log showed the exact environment-protection rejection message on every run following the migration.
Impact
A completely correct workflow would fail indefinitely on a permissions issue unrelated to its own code - an easy trap for anyone migrating deployment methods without auditing environment settings first.
Severity & Priority
Medium severity - blocks all deploys, but has a known, one-time configuration fix. Priority: fix immediately to unblock releases.
Fixed: added "main" to the environment's allowed deployment branches and re-ran the deploy to confirm.

Rapid double-tap on Submit can create duplicate entries

Pros & Cons · found via exploratory / rapid-input testing
Environment
Pros & Cons, mobile and web builds, any device - rapid-input exploratory testing.
Steps to Reproduce
  1. Enter a valid transcript and select a type.
  2. Tap "Submit" twice in quick succession, before the field clears.
  3. Check the results list for the entry.
Expected Result
Only one entry is saved; the second tap is a no-op while the first save is in flight.
Actual Result
Two entries are created with identical content and different IDs. The Submit button has no in-flight guard - it stays enabled and the save handler has no "already saving" state check.
Happening in ProdEnvironmentOccurrence / Reproducibility
YesProdIntermittent (3/5)
Evidence
Results list showed two entries with identical transcript text and type, each with a distinct auto-generated ID, immediately after a fast double-tap.
Product Framing
Low severity, but a real data-quality cost - duplicates clutter the results list, and once full bi-directional cloud sync is live, would double-write to Firestore too. Cheap fix (one boolean guard); I'd bundle this into the next small polish pass rather than ship a dedicated release for it.
Severity & Priority
Low severity, low priority - real but cosmetic data-quality issue with a cheap fix; bundle into next polish pass.

Mic button has no accessible name for screen readers

Pros & Cons · found via accessibility review (WCAG 4.1.2)
Environment
Pros & Cons, VoiceOver/TalkBack, accessibility review pass (WCAG 4.1.2).
Steps to Reproduce
  1. Turn on a screen reader (VoiceOver or TalkBack).
  2. Navigate through the app to the microphone button.
  3. Listen to what, if anything, is announced.
Expected Result
The screen reader announces a clear label, e.g. "Start recording, button," and conveys its recording/idle state accessibly.
Actual Result
Nothing meaningful is announced. The control is a bare tap target wrapping an icon, with no semantic label, tooltip, or button role.
Happening in ProdEnvironmentOccurrence / Reproducibility
YesProdAlways (5/5)
Evidence
VoiceOver/TalkBack announced only a generic "button" (or nothing) when focus landed on the mic control - no label, no state.
Product Framing
This fully excludes screen-reader users from the app's core feature, not just a UX rough edge - I'd prioritize this above BUG-005 since it blocks a whole class of users rather than degrading data quality for everyone.
Severity & Priority
Medium severity, high priority - fully blocks screen-reader users from the app's primary interaction.

"dislike" gets partially misclassified as a "pros" signal

Pros & Cons · found via exploratory testing of the NLP extraction
Environment
Pros & Cons, transcript-parsing/NLP extraction logic, any platform.
Steps to Reproduce
  1. Dictate a note containing the word "dislike," e.g. "I dislike the noise levels."
  2. Let the transcript be parsed into a structured entry.
  3. Check whether the statement was filed under pros or cons.
Expected Result
"noise levels" is recorded under cons, and does not appear under pros.
Actual Result
The statement is occasionally filed under pros instead. The pros-extraction pattern matches the unanchored substring "like" with no word boundary, so "dislike" partially matches the pros pattern.
Happening in ProdEnvironmentOccurrence / Reproducibility
YesProdIntermittent (3/5)
Evidence
Parsed-entry output for the dictated phrase "I dislike the noise levels" showed "noise levels" filed under pros in the structured entry.
Product Framing
Low frequency, but high trust cost when it happens: in a voice-note app, transcription accuracy is the product. A user won't notice their sentiment was flipped until they read the note back later - I'd flag this as worth fixing before any push to market the extraction logic as "smart."
Severity & Priority
Low severity, medium priority - infrequent, but silently corrupts data the product's value proposition depends on.

Refunded orders are counted as a duplicate purchase

Amazon Order Tracker · found via validation testing against real order history
Environment
Amazon Order Tracker, order-history import flow, validated against a real exported Amazon order history containing a refund.
Steps to Reproduce
  1. Purchase an item, then have Amazon issue a refund for that order.
  2. Export the Amazon order history, which lists the refund as its own related order entry.
  3. Import the order history into Amazon Order Tracker and check the item's stats.
Expected Result
The item's "times purchased" count reflects one purchase, not two, and the refunded amount is not double-counted in total spend.
Actual Result
The item shows up as two separate buys, inflating both its "times purchased" count and total spend. Every order ID is treated as an independent purchase event, with no refund/return flag the importer can key off of.
Happening in ProdEnvironmentOccurrence / Reproducibility
YesProdIntermittent (3/5)
Evidence
Cross-referencing a real imported order history against Amazon's own order page showed the same item counted twice in the dashboard - once as the original purchase, once as the refund entry.
Impact
This distorts the dashboard's core value proposition - "what you buy most" and "total spent" become unreliable exactly where a refund exists, which undermines trust in the numbers for anyone auditing their own spending.
Product Framing
Doesn't block the MVP's core "import and see spending patterns" flow, and Amazon's order-history page has no clean, consistently-labeled refund flag to key off of - a real fix needs heuristic detection (e.g. matching a reversed order total or a "Refund"/"Return" status string) rather than a quick patch. Filing as P2, post-MVP: worth fixing before this is positioned as an accurate financial-tracking tool, not before initial release.
Severity & Priority
Medium severity, P2/post-MVP priority - undermines data accuracy but doesn't block initial release.

Projects carousel row 1 never scrolls; row 2 scrolls but jitters near the loop point

catalogentry-hub (this site)
Environment
catalogentry.com homepage, "Projects" section, desktop Chrome, ~1400px window; two-row carousel build from PR #14.
Steps to Reproduce
  1. Open catalogentry.com and scroll to the Projects section.
  2. Watch the first row of project cards for 10+ seconds.
  3. Watch the second row of project cards for 10+ seconds, including the moment it completes a full loop.
Expected Result
Both rows continuously auto-scroll at their own pace, with no stopping and no visible stutter at the loop point.
Actual Result
Row 1 does not move at all. Row 2 moves, but visibly jitters/stutters at the point where it wraps back to the start.
Happening in ProdEnvironmentOccurrence / Reproducibility
YesProdAlways (5/5)
Evidence
Headless-browser (Puppeteer) inspection: row 1's track.scrollWidth did not exceed its clientWidth, so setting scrollLeft had no visible effect; row 2's reverse-direction wrap logic reset scrollLeft in a way that fought the browser's own scroll clamping near the boundary.
Severity & Priority
High severity - breaks the first content section below the fold. Priority: fix immediately.
Fixed: replaced the scroll-position approach with a transform: translateX() loop driven by an internal JS position value, which doesn't depend on the row happening to overflow its container. See Projects.

Marquee loop shows an empty gap in production, not reproducible in a narrower local test window

catalogentry-hub (this site)
Environment
catalogentry.com homepage, "Projects" section, production custom domain (~832px content width). Not reproducible in a local browser window narrowed to ~700px.
Steps to Reproduce
  1. Open catalogentry.com on a standard desktop browser window (production width).
  2. Watch either project row scroll through one complete loop cycle.
  3. Repeat with the same page open in a narrower local browser window and compare.
Expected Result
The row stays fully populated with cards at every point in its scroll loop, regardless of viewport width.
Actual Result
At the real production width, the row briefly showed an empty gap partway through its loop. The identical code showed no gap at all in a narrower local test window.
Happening in ProdEnvironmentOccurrence / Reproducibility
YesProdAlways (5/5)
Evidence
Headless-browser measurement: the row's duplicated content measured ~860px wide, against a required ~1664px (2x the 832px container width) for the translateX(-50%) technique to never run out of content mid-loop.
Severity & Priority
High severity - visible defect in production that a narrower local test window fully masked. Priority: fix immediately.
Fixed: replaced the fixed "-50%" assumption with JS that clones each row's cards at runtime until the content safely exceeds 2x the row's width, and computes the exact loop distance from real layout (offsetLeft) instead of an assumed percentage.

Testimonials autoplay jitters at the loop point and freezes entirely at low speed

catalogentry-hub (this site)
Environment
catalogentry.com homepage, "What people say" section, all browsers; regression surfaced while lowering the configured autoplay speed.
Steps to Reproduce
  1. Open catalogentry.com and scroll to the "What people say" section.
  2. Watch the carousel autoplay through one full cycle and observe the loop point.
  3. Lower the configured autoplay speed to below 0.5px/frame and reload the page.
Expected Result
The carousel loops seamlessly with no visible jump, and continues animating smoothly at any configured speed.
Actual Result
The carousel jumped abruptly back to the first card at the end of every cycle. After lowering the speed, it stopped animating entirely, with no console error.
Happening in ProdEnvironmentOccurrence / Reproducibility
YesProdAlways (5/5)
Evidence
Headless-browser measurement: at a 0.25px/frame increment, track.scrollLeft stayed at 0 for 3+ seconds, confirming the browser was rounding away the sub-pixel increment each frame before it could accumulate.
Severity & Priority
Medium severity - decorative element, but a silent full freeze is an easy regression to ship unnoticed. Priority: fix before next release.
Fixed: duplicated the testimonial cards once and subtract one set's width on wrap instead of resetting to 0, and switched to tracking scroll position in a plain JS variable - written to scrollLeft only after rounding - so it never gets stuck re-reading its own rounded value. See What people say.