Taming tRPC & Playwright: A Deep Dive into E2E Testing for Compliance Exports
Join me in a recent development session where we tackled critical E2E tests for compliance exports, wrestled with tRPC mocking, and cleaned up some orphaned data – a true full-stack adventure.
Every now and then, a development session comes along that perfectly encapsulates the daily grind: a mix of feature building, bug squashing, and wrestling with stubborn tools. This past session was one such beast, focused on shoring up our compliance export features with robust end-to-end (E2E) tests, alongside some crucial data integrity fixes.
We pushed 3 code commits and performed a database backfill, ultimately pushing origin/main to a stronger, more reliable state. Here's a breakdown of what went down, the victories we celebrated, and the frustrating "gotchas" that taught us some valuable lessons.
The Mission: Fortifying Compliance and Reports
Our primary goal was to bring E2E testing to our compliance export functionality. This isn't just a nice-to-have; it's critical for trust and accuracy when dealing with sensitive data exports. Beyond that, we needed to iron out a couple of wrinkles in our reporting system.
What We Shipped: A Stack of Solid Features and Fixes
-
E2E Testing for Compliance Exports: This was the big one. We introduced a new test suite,
tests/e2e/compliance-export.spec.ts, boasting 9 comprehensive E2E tests. These tests cover everything from the initial panel rendering and expand/collapse behavior to verifying the export download, checking PR checkbox visibility, and ensuring PR links display correctly. We also built tests for error and loading states, ensuring a resilient user experience.Crucially, we needed a reliable way to authenticate within our Playwright tests. This led to the creation of
tests/e2e/helpers/auth.ts, a reusable fixture that intelligently forges NextAuth v5 JWT cookies. This little helper significantly streamlines authenticated E2E test setup, allowing us to focus on feature testing rather than authentication boilerplate.The result? All 40 E2E tests (18 new, 22 existing) now pass flawlessly across both Chromium and mobile viewports. Sweet validation!
-
Fixing Report Header Rendering: We had an issue where server-generated HTML headers, created by
buildReportHeader(), weren't rendering correctly in the in-app preview. Instead of beautiful, styled headings, users were seeing raw HTML tags. The culprit? OurMarkdownRendererwasn't set up to process raw HTML within Markdown.The fix was straightforward but essential: we added
rehype-rawto ourMarkdownRendererconfiguration. This plugin tells the Markdown processor to interpret raw HTML within the Markdown content, allowing our dynamically generated headers to display as intended. -
Linking Reports to Projects: A subtle but significant data integrity issue surfaced: reports generated from our workflows, auto-fix, and refactor pages were being saved to the database, but without their corresponding
projectIdorprojectName. This meant they were effectively "orphaned" from their source projects.We updated the
ReportGeneratorModalon these three pages to correctly capture and persist theprojectIdandprojectName. This ensures every new report is properly linked. To address the existing data, we ran a targeted backfill script, identifying 12 orphaned reports by looking up theirprojectIdfrom their source workflow/autofix run and updating each record. Data consistency restored!
The "Pain Log" (aka Lessons Learned)
No dev session is complete without hitting a few walls. These moments, while frustrating in real-time, often yield the most valuable lessons.
1. The tRPC Superjson Gotcha
-
The Problem: When mocking tRPC responses in Playwright using
page.routewith plain JSON, our components wouldn't hydrate, and tRPC queries would mysteriously return 500 errors from the real server (despite our mocks being active). -
What We Tried: Intercepting tRPC calls and returning simple
{ data: <my-mock-data> }structures. -
The Root Cause: tRPC, by default, uses the
superjsontransformer for serialization and deserialization. This means its wire format isn't plain JSON. A successful tRPC response needs to be wrapped like this:{ result: { data: { json: <your-data> } } }. Our plain JSON mocks were being rejected by the tRPC client. -
The Solution: We created a
superjsonResult()helper function to correctly format our mock data.typescript// superjsonResult.ts export const superjsonResult = (data: any) => ({ result: { data: { json: data, }, }, }); // In your Playwright test mock: await page.route('**/api/trpc/someProcedure*', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(superjsonResult({ someKey: 'mocked value' })), }); }); -
Takeaway: Always remember tRPC's
superjsontransformer when mocking responses. It's a silent killer if you're not aware of its specific wire format.
2. Environment Variables in Playwright Workers
- The Problem: Our E2E tests failed with "AUTH_SECRET env var must be set" errors, even though
.envfiles were present and working for local development. - What We Tried: Assuming Playwright workers would inherit environment variables from the main process.
- The Root Cause: Playwright's worker processes run in isolated environments and don't automatically load
.envfiles from the project root. - The Solution: In our
auth.tshelper, which runs within the Playwright worker context, we explicitly addedprocess.loadEnvFile()(a Node.js 23+ feature) with a try/catch fallback. This ensures the necessary environment variables are loaded directly within the worker's scope. - Takeaway: Don't assume test runners will automatically load
.envfiles for their child processes. Explicitly load them in the test setup if needed, especially for sensitive variables likeAUTH_SECRET.
3. The Batching tRPC Mock Blind Spot
- The Problem: Even after adding specific tRPC mock routes, some components still failed to hydrate, making us think our mocks weren't working.
- What We Missed: tRPC often batches multiple procedures into a single network request. While we had mocked
compliance.getExportandcompliance.getPanel, the batch request also includeddashboard.activeProcesses, which wasn't in our initial mock map. - The Solution: We expanded our tRPC procedure mock map to include
dashboard.activeProcesses, ensuring all procedures within a batch request were accounted for. - Takeaway: When mocking tRPC, especially with
page.route("**/api/trpc/**"), be mindful of batching. Use your browser's network tab to inspect the full batch request URL and ensure all procedures in the batch are either mocked or allowed to hit the real server.
Looking Ahead
With these critical E2E tests in place and our reporting mechanisms tightened up, we're in a much stronger position. However, the journey continues:
- The code-analysis page still needs
projectIdadded to itsReportGeneratorModal. - Our documentation needs updating with the new compliance report export features.
- We're considering a more robust Playwright
storageStatesetup for our auth fixture, making it even more reusable. - The
nyx-headerCSS classes used bybuildReportHeader()need actual CSS! While it works in PDF exports, the in-app preview is still unstyled.
This session was a great reminder that full-stack development is a constant dance between feature implementation, rigorous testing, and meticulous debugging. Every challenge overcome makes the system (and us!) a little bit stronger.
{"thingsDone":["Created robust E2E tests for compliance export functionality","Implemented reusable NextAuth v5 JWT cookie forging for Playwright authentication","Fixed report header rendering using rehype-raw for server-generated HTML","Corrected ReportGeneratorModal to link reports to projects on workflows, auto-fix, and refactor pages","Backfilled 12 orphaned reports in the database with correct projectId associations","Ensured all 40 E2E tests pass on chromium and mobile viewports"],"pains":["tRPC superjson transformer requirement for mock responses","Playwright workers not inheriting .env variables","Missing tRPC procedure in batch request mock map"],"successes":["Successfully implemented comprehensive E2E test coverage","Resolved critical data integrity issue for reports","Streamlined E2E authentication setup","Improved in-app report header rendering","Gained deeper understanding of tRPC's serialization and Playwright's environment isolation"],"techStack":["Playwright","tRPC","Next.js","NextAuth.js v5","TypeScript","Node.js 23+","rehype-raw","Superjson","PostgreSQL"]}