[POC] #1034 Test A: client-side MSW mock playground - #1049
Conversation
Add Mock Service Worker (msw ^2.15.0) and @mswjs/source ^0.5.0 to the website package and commit the generated public/mockServiceWorker.js worker asset (via `msw init`), so later tickets can intercept Swagger UI "Try it out" requests entirely client-side. The existing js-yaml dependency already provides the spec-parsing path, so no new YAML parser was added. The vendored worker script is excluded from prettier and cspell. Verified by a green `pnpm --filter website run build` (which produces dist/mockServiceWorker.js) and a passing `pnpm --filter website run checks`. Refs #1034 Files changed: - website/package.json - pnpm-lock.yaml - website/.prettierignore - website/.cspell.json - website/public/mockServiceWorker.js
…nd-written handler Adds a throwaway MSW playground that proves client-side interception of Swagger UI "Try it out" requests. A new /protocol/mock-playground route renders the shared OpenApiDocs island with "Try it out" enabled and starts a Mock Service Worker that answers GET /common-grants/opportunities with a schema-valid example body sampled from the rendered OpenAPI spec via openapi-sampler. The shipping /protocol/api-docs page is unchanged (OpenApiDocs gains a backwards-compatible enableTryItOut prop defaulting to false). Refs #1034 Files changed: - website/src/lib/mock/opportunities-handler.ts - website/__tests__/lib/mock/opportunities-handler.spec.ts - website/src/components/MockPlayground.tsx - website/src/pages/protocol/mock-playground.astro - website/src/components/OpenApiDocs.tsx
…all three versions) Replaces T2's single hand-written handler with handlers generated from the rendered OpenAPI spec via @mswjs/source fromOpenApi(), and swaps the active handler set whenever the version dropdown changes so Try-it-out reflects the selected version (0.1.0 / 0.2.0 / 0.3.0). Resolves the spike's central base-URL question: because the specs declare no `servers:` block, @mswjs/source falls back to a `/` base and emits same-origin relative-path handlers matching exactly what Swagger UI targets — no base-URL patching needed. OpenApiDocs gains an optional onVersionChange callback; MockPlayground regenerates handlers via worker.resetHandlers with a latest-wins guard and fetch/parse error handling. The now-superseded T2 opportunities-handler module is removed (its OpenApiSpec type moved into spec-handlers). Refs #1034 Files changed: - website/src/lib/mock/spec-handlers.ts (new) - website/__tests__/lib/mock/spec-handlers.spec.ts (new) - website/src/components/MockPlayground.tsx - website/src/components/OpenApiDocs.tsx - website/src/lib/mock/opportunities-handler.ts (removed) - website/__tests__/lib/mock/opportunities-handler.spec.ts (removed)
|
🚀 Website Preview Deployed! Preview your changes at: https://cg-pr-1049.billy-daly.workers.dev This preview will be automatically deleted when the PR is closed. |
948131f to
31da123
Compare
|
✅ Verified on the Cloudflare preview ( Base-URL finding: specs have no Known caveat (fix before productionizing): the root-scoped worker intercepts the page's own navigation requests; a navigation hitting Astro's trailing-slash 307 logs CI's website |
|
@karinamzalez Thanks for all of your work on this! It's very cool to see it live. While this an excellent proof of concept, there are a few key things I'd love to iron out in this test before we can consider MSW a viable path forward: Must do
Should do
|
Adds a hand-authored, frozen fixture of 10 funding opportunities spanning all four statuses (forecasted/open/closed/custom), varied funding amounts, and spread-out close dates, so filters and sorting will visibly change results in the deterministic handlers built on top of it (#1034-T5/T6). Values are assembled from the TypeSpec @example decorators; the first three records keep their ids/titles from lib/ts-sdk/examples/mock-api-server.ts so cross-repo examples stay recognizable. shapeOpportunityForVersion() projects a full (v0.3, detail) record down to the version/variant a given endpoint should emit (stripping acceptedApplicantTypes and competitions for v0.1, and competitions from any list-variant projection). Refs #1034
Adds the POST /common-grants/opportunities/search handler to buildOpportunityHandlers: applies the status (in/notIn), closeDateRange, totalFundingAvailableRange, minAwardAmountRange, and maxAwardAmountRange filters (currency-aware, partial bounds supported) plus OppSortBy sorting and body pagination over the shared opportunity fixture, and echoes sortInfo/filterInfo per the protocol envelope. Malformed bodies, unknown sort fields/operators, and malformed filter shapes return a 400 with the protocol Error shape instead of throwing. Note: this file (handlers.ts) and its test file also carry the pre-existing list/detail handlers and tests from #1034-T5, which were already present in the working tree uncommitted; they land in this commit since git can't split a single file's hunks into two commits atomically here. Refs #1034
…, 404/400) Rounds out the list/detail opportunity handlers (already landed inside the T6 commit, which couldn't be split from these pre-existing uncommitted changes) with review follow-ups: adds pagination edge-case tests (page past the end, pageSize clamping/defaults) that weren't previously locked in, extracts a shared resolvePagination() helper used by both the list and search handlers (unifying pageSize=0 to clamp to 1 instead of silently defaulting to 100), extracts a successResponse() envelope helper alongside the existing errorResponse() one, and corrects the UUID_PATTERN comment to not overclaim RFC 4122 conformance. Refs #1034
… long-tail Prepends buildOpportunityHandlers ahead of the generated fromOpenApi set in MockPlayground.handleVersionChange so opportunity endpoints override the long tail (first-match-wins). spec-handlers.ts gains a memoization wrapper so every other endpoint replays a byte-identical cached response, keyed on method+path+sorted-query+body, instead of reseeding faker on each call. Refs #1034
Follow-ups from the review of the mock playground implementation. Swagger UI pre-fills the `oppId` box from the specs' `Types.uuid` example (30a12e5e-...), so the first Execute a visitor ran -- field untouched -- answered 404, and the rendered "Example Value" pane described a record the mock had never heard of. Add a fixture record reproducing that published example verbatim, sorted first by lastModifiedAt so the list -> detail round-trip is the obvious next click. Validate filter bound *values*, not just operators: a bound that isn't a date, or isn't a Money object, now returns 400 instead of being silently dropped -- dropping it answered a filtered request with unfiltered results. Report unapplied customFilters through filterInfo.errors, the spec's channel for non-fatal filtering errors. Reject page/pageSize below the spec's `minimum: 1` rather than clamping, and drop the MAX_PAGE_SIZE cap -- 100 is the declared default, not a maximum. Derive totalPages the same way on list and search, so an empty result set reports zero pages on both. Break sort ties on `id` so `desc` is the exact reverse of `asc`; without it, records sharing a sort value kept their incoming order under a stable sort. Surface the reserved error inputs on the playground page itself, built from the exported id constants so the docs can't drift from the handlers. Also: evict a failed resolver's cache key instead of replaying the rejection forever; derive Version from SUPPORTED_VERSIONS and guard the dropdown value so an unsupported version degrades visibly rather than being cast through; move personal AI-tooling ignores out of the shared .gitignore. Files changed: - website/src/lib/mock/opportunities/fixtures.ts - website/src/lib/mock/opportunities/handlers.ts - website/src/lib/mock/spec-handlers.ts - website/src/components/MockPlayground.tsx - website/src/pages/protocol/mock-playground.astro - website/__tests__/lib/mock/*.spec.ts - .gitignore
Restore .gitignore to match main. The AI-tooling entries added earlier in this branch are personal workflow artifacts and now live in a local .git/info/exclude; the leftover trailing-whitespace tweak is unrelated to this spike. Net effect: .gitignore no longer appears in this PR's diff.
|
@widal001 Thanks for the review! All five items are addressed on this branch-- verifiable on the preview at
Why it took a fixture: Bigger implication: your must-dos plus the browser / curl / SDK continuity point shifted the ADR's recommendation to a small standalone Worker serving one real URL for all three consumers (docs site stays static on GH Pages), reusing this PR's fixture/handlers. Are you open to taking the architecture discussion to the ADR? I'm still making small updates but here is the link: https://drive.google.com/file/d/1EqvksgT5zesQelSGvv0XrujDagxDJA4U/view?usp=drive_link |



Summary
Important
This PR's role has changed since it was opened. The review feedback here, plus the onboarding/quickstart discussion, shifted the spike ADR's recommendation away from client-side MSW (this PR) toward a small standalone Cloudflare Worker serving the mock: one real URL that answers the browser,
curl, and the SDK identically, while the docs site stays pure static on GH Pages, untouched. This PR now serves two purposes:handlers.ts's ~580 lines, MSW touches six (the import, twoHttpResponse.jsonwrappers, three route registrations)-- the filter/sort/error logic is pure request->response code that ports into a Worker nearly as-is.The browser-vs-CLI/SDK trade-off is deliberately not resolved in this PR -- that's the ADR's central question, so please put architecture-level thoughts there. Review here is scoped to: are the fidelity must-dos/should-dos met?
Expected end state: if the team approves the standalone-Worker direction, this PR closes unmerged with a pointer to the ADR and the successor PR; the fixture/handler modules carry forward.
Changes proposed
@mswjs/sourceand initialize themockServiceWorker.js; add a throwaway/protocol/mock-playgroundroute rendering the sharedOpenApiDocsisland with "Try it out" enabled, generating handlers from the rendered OpenAPI specs viafromOpenApi()and swapping the active set across v0.1.0/v0.2.0/v0.3.0 (the shipped/protocol/api-docspage still ships with Try-it-out disabled).opportunities/fixtures.ts: 11 semantic, version-aware opportunity records built from the TypeSpec@exampledecorators instead of faker output. One reproduces the spec's published example under the id Swagger UI pre-fills, so the default Execute returns that record rather than a 404, and it sorts first in the list response.opportunities/handlers.ts: hand-authored list/detail/search reading from that fixture: deterministic, mutually consistent, with realstatus/date/money-range filters andOppSortBysorting. Bad filter bounds and out-of-range pagination are rejected rather than silently ignored.sortBy/operator, a malformed filter bound or JSON body, andpage/pageSizebelow the spec'sminimum: 1-- all in the protocol's{ status, message, errors[] }envelope.MockPlaygroundprepends the opportunity handlers ahead of the generated set (MSW resolves first-match-wins);spec-handlers.tsmemoizes the rest, so repeat calls are identical across the whole surface for the session.Context for reviewers
Root cause of the fidelity gap.
fromOpenApi()falls back toseedSchema(), which calls@faker-js/fakerwith nofaker.seed()-- fresh data every call and ignores OpenAPI's singularexample, the keyword our@exampledecorators compile to. Structural, not configuration: schema-seeding can't resolve a path param, keep two endpoints consistent, apply filters, or emit errors. (This applies to any schema-sampling mock, including Prism's dynamic mode, which is why the fixture layer is needed under every option, not just MSW.)Fix: a hybrid. Fixture-backed handlers for the three opportunity endpoints, layered ahead of the generated set that still covers every other path. Rejected: seeding faker (repeatable, but nothing else); adding response-level examples to the TypeSpec (edits the published protocol to serve a docs feature); hand-authoring everything (loses the breadth that makes the playground worth visiting).
How to verify -- open
/protocol/mock-playground; the intro block lists the error inputs.GET /opportunities-> Execute twice -> same body. First item isSmall business grant program.GET /opportunities/{oppId}-> Execute without touching the pre-filled id → 200, matching the "Example Value" pane above it. Anyidfrom the list resolves to that same record.status in [open]-> onlyopenrecords; flipsortOrder→ exact reverse.competitionsandacceptedApplicantTypes.Verified locally: 146 tests pass,
pnpm --filter website run checksclean, plus the flow above against a dev server.Trade-offs. The fixture is hand-maintained, so it can drift from the TypeSpec models -- tests catch gross shape drift, not full conformance. Two of the 400s aren't declared responses on their operations, so Swagger UI labels them "Undocumented"; either the mock is over-strict or the protocol under-specifies param validation. Long-tail memoization is session-scoped and resets on reload.
Additional information