test(e2e): add Playwright bounty application flow test - #180
Conversation
|
@0xDeon is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds Playwright-based E2E tests and CI integration for the bounty application flow, instruments UI with Changes
Sequence Diagram(s)Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (7)
e2e/bounty-application.spec.ts (4)
374-393: Form-reset test is stable but assumes only one "Cancel" button in the DOM.
getByRole("button", { name: /cancel/i })works today because the non-creator session prevents "Cancel Bounty" from rendering and the mobile CTA is hidden under Desktop Chrome's breakpoint. If either assumption changes (e.g., a future test uses a creator session), the selector will become ambiguous. Targeting the dialog's Cancel by scoping to the dialog or adding adata-testid="application-cancel-btn"inapplication-dialog.tsxwould make it future-proof.♻️ Proposed scoped selector
- await page.getByRole("button", { name: /cancel/i }).click(); + await page + .getByTestId("application-dialog") + .getByRole("button", { name: /^cancel$/i }) + .click();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/bounty-application.spec.ts` around lines 374 - 393, The test selector for the Cancel button is ambiguous; update the test in e2e/bounty-application.spec.ts to scope the cancel click to the application dialog (use the dialog locator like getByTestId("application-dialog") and then locate its Cancel button) or add a stable test id in the component (add data-testid="application-cancel-btn" in application-dialog.tsx on the Cancel button) and use getByTestId("application-cancel-btn") in the test so the Cancel target is unambiguous regardless of other Cancel buttons in the DOM.
189-199: Cookie name is coupled to better-auth config — add a comment or centralize.
boundless_auth.session_tokenencodes thecookieName+.session_tokensuffix from the better-auth server config. Since/api/auth/**is already mocked at the route layer, this cookie is only effective if SSR reads it; if that name ever changes in the auth config the cookie becomes dead weight and auth-dependent tests may silently regress. A brief inline comment or a shared constant imported from the auth config would harden this.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/bounty-application.spec.ts` around lines 189 - 199, The test sets a hard-coded cookie name "boundless_auth.session_token" which mirrors better-auth's cookieName + ".session_token" and can break silently if auth config changes; update the test in e2e/bounty-application.spec.ts to either import a shared constant from the auth configuration (the cookieName or full session cookie constant) and use that when composing the cookie name, or at minimum add a clear inline comment above the addCookies call referencing the better-auth config key (cookieName) and explaining the coupling so future changes update the test accordingly; target the cookie-setting block that calls page.context().addCookies(...) and replace the literal or annotate it.
234-234: Static-analysis ReDoS warning is a false positive here.
BOUNTY_IDis a module-level string constant with no regex metacharacters, sonew RegExp(\/bounty/${BOUNTY_ID}`)is safe. If you want to appease the linter anyway,expect(page).toHaveURL(`/bounty/${BOUNTY_ID}`, { timeout: 10_000 })` (Playwright accepts a string matcher and substring-matches it) works without constructing a regex.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/bounty-application.spec.ts` at line 234, Static-analysis flagged a ReDoS for creating a RegExp with BOUNTY_ID, but BOUNTY_ID is a constant without regex metacharacters; replace the regex-based URL assertion with a string-based matcher to avoid constructing a RegExp. Update the assertion that currently builds new RegExp(`/bounty/${BOUNTY_ID}`) to use expect(page).toHaveURL(`/bounty/${BOUNTY_ID}`, { timeout: 10_000 }) (or the equivalent string form) and keep BOUNTY_ID as the module-level constant referenced in the assertion.
309-352: Consider adding a failing-mutation test to cover the error path.All submission tests exercise the happy path. Given that
bounty-detail-sidebar-cta.tsxcurrently swallows mutation errors intoreturn false(no user-visible error), a test that mocksSubmitToBountyreturning{ errors: [...] }or a 500 would both (a) lock down future error-handling behavior and (b) catch the silent-failure regression flagged in the sidebar CTA review.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/bounty-application.spec.ts` around lines 309 - 352, Add a new E2E test that covers the failed-mutation path by intercepting the submission request used by SubmitToBounty and returning an error (either HTTP 500 or a JSON body like { errors: [...] }); name it something like "shows error and keeps dialog open on failed submission" and model it off the existing "submits application and closes dialog on success" test so you still open the application dialog and fill inputs, then trigger the submit, assert the application dialog remains visible (i.e., not closed by a swallowed error) and assert appropriate failure UI state (e.g., an error toast, an inline error, or that the submit button is re-enabled) to lock down current behavior in bounty-detail-sidebar-cta.tsx which currently swallows mutation errors.components/bounty-detail/bounty-detail-sidebar-cta.tsx (2)
43-43: Use theuseSubmitToBountywrapper so React Query caches are invalidated on success.
hooks/use-submission-mutations.tsalready exposes auseSubmitToBountywrapper that invalidatesbountyKeys.detail(bountyId)andbountyKeys.lists()on success. CallinguseSubmitToBountyMutationdirectly here skips that invalidation, so the bounty detail page and bounty list won't reflect the new submission until a manual reload. The wrapper also normalizes the payload so callers passCreateSubmissionInputdirectly instead of{ input }.♻️ Proposed switch to the wrapper
-import { BountyFieldsFragment, useSubmitToBountyMutation } from "@/lib/graphql/generated"; +import { BountyFieldsFragment } from "@/lib/graphql/generated"; +import { useSubmitToBounty } from "@/hooks/use-submission-mutations"; ... - const submitMutation = useSubmitToBountyMutation(); + const submitMutation = useSubmitToBounty(); ... - await submitMutation.mutateAsync({ - input: { - bountyId: bounty.id, - githubPullRequestUrl: portfolioUrl ?? bounty.githubIssueUrl, - comments: coverLetter, - }, - }); + await submitMutation.mutateAsync({ + bountyId: bounty.id, + githubPullRequestUrl: portfolioUrl ?? bounty.githubIssueUrl, + comments: coverLetter, + });Apply the same change to
MobileCTAat line 289 and 330.Also applies to: 289-289
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` at line 43, Replace direct calls to useSubmitToBountyMutation with the useSubmitToBounty wrapper so React Query cache invalidation and payload normalization occur; specifically, in the component where submitMutation is created (currently using useSubmitToBountyMutation) switch to calling useSubmitToBounty and update callers to pass CreateSubmissionInput directly (not { input }), and make the same replacements in the MobileCTA instances referenced (the spots currently importing useSubmitToBountyMutation at the other locations).
126-152: Extract the sharedonApplyhandler to remove duplication between Sidebar and Mobile CTAs.The entire
onApplyclosure (mutation call, success toast, error handling) is duplicated verbatim. A small helper keyed offbountykeeps both CTAs in sync and prevents the two paths from drifting.♻️ Sketch of a shared helper
function useApplyToBounty(bounty: BountyFieldsFragment) { const submitMutation = useSubmitToBounty(); return async ({ coverLetter, portfolioUrl }: { coverLetter: string; portfolioUrl?: string }) => { await submitMutation.mutateAsync({ bountyId: bounty.id, githubPullRequestUrl: portfolioUrl ?? bounty.githubIssueUrl, comments: coverLetter, }); toast.success("Application submitted successfully!"); return true; }; }Also applies to: 326-352
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 126 - 152, Extract the duplicated onApply closure into a shared hook (e.g., useApplyToBounty) that accepts the bounty and returns the async handler used by ApplicationDialog; inside the hook call useSubmitToBounty to get submitMutation and implement the same logic as the inline closures (await submitMutation.mutateAsync with payload using bounty.id and fallback to bounty.githubIssueUrl, toast.success on success, and preserve the try/catch returning true on success and false on error). Replace the inline onApply in ApplicationDialog (and the duplicate at lines ~326-352) with the handler returned from useApplyToBounty(bounty) so both CTAs share the exact implementation.playwright.config.ts (1)
3-32: Config looks solid – one small operational note.Hermetic mocks +
workers: 1+fullyParallel: falseis a reasonable starting posture for a first E2E suite. Two non-blocking suggestions:
- Add
playwright-report/,test-results/, andblob-report/to.gitignoreif not already ignored, since the HTML reporter writes there locally.- Consider
outputDir: "./test-results"explicitly for clarity and CI artifact upload.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@playwright.config.ts` around lines 3 - 32, Add an explicit outputDir to the Playwright config and ensure local report dirs are git-ignored: in the defineConfig object (where symbols like workers, fullyParallel, and reporter are set) add outputDir: "./test-results" under the top-level config or use block so CI artifact upload is deterministic, and update .gitignore to include playwright-report/, test-results/, and blob-report/ so HTML reports and blobs are not committed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 126-152: The onApply handler in ApplicationDialog wrongly persists
the portfolio URL into githubPullRequestUrl (falling back to
bounty.githubIssueUrl) and swallows errors; update onApply (used in
ApplicationDialog and the duplicate handlers in SidebarCTA/MobileCTA) to pass
the portfolio URL as a distinct field (e.g., portfolioUrl) to
submitMutation.mutateAsync and only populate githubPullRequestUrl when the user
actually supplied a PR URL (don’t fallback to bounty.githubIssueUrl), and change
the catch to re-throw the caught error (or throw a new Error with the mutation
error) instead of returning false so ApplicationDialog can surface the error via
form.setError("root", ...).
In `@e2e/bounty-application.spec.ts`:
- Around line 178-185: The default mock branch currently fulfills with { data:
null } via route.fulfill which can hide missing GraphQL handlers; update the
default branch in the request router to either (a) fail loudly (e.g., call
route.abort() or route.fulfill with a 500 and a descriptive error message so
tests surface unhandled operations) or (b) return a minimal valid shape for the
expected GraphQL response for the operation name, and then remove the test's
console-error filter that suppresses the "Query data cannot be undefined"
warning; locate the default case handling the GraphQL route (the switch/default
that calls route.fulfill) and make one of these changes so unrecognized
operations no longer silently pass.
---
Nitpick comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Line 43: Replace direct calls to useSubmitToBountyMutation with the
useSubmitToBounty wrapper so React Query cache invalidation and payload
normalization occur; specifically, in the component where submitMutation is
created (currently using useSubmitToBountyMutation) switch to calling
useSubmitToBounty and update callers to pass CreateSubmissionInput directly (not
{ input }), and make the same replacements in the MobileCTA instances referenced
(the spots currently importing useSubmitToBountyMutation at the other
locations).
- Around line 126-152: Extract the duplicated onApply closure into a shared hook
(e.g., useApplyToBounty) that accepts the bounty and returns the async handler
used by ApplicationDialog; inside the hook call useSubmitToBounty to get
submitMutation and implement the same logic as the inline closures (await
submitMutation.mutateAsync with payload using bounty.id and fallback to
bounty.githubIssueUrl, toast.success on success, and preserve the try/catch
returning true on success and false on error). Replace the inline onApply in
ApplicationDialog (and the duplicate at lines ~326-352) with the handler
returned from useApplyToBounty(bounty) so both CTAs share the exact
implementation.
In `@e2e/bounty-application.spec.ts`:
- Around line 374-393: The test selector for the Cancel button is ambiguous;
update the test in e2e/bounty-application.spec.ts to scope the cancel click to
the application dialog (use the dialog locator like
getByTestId("application-dialog") and then locate its Cancel button) or add a
stable test id in the component (add data-testid="application-cancel-btn" in
application-dialog.tsx on the Cancel button) and use
getByTestId("application-cancel-btn") in the test so the Cancel target is
unambiguous regardless of other Cancel buttons in the DOM.
- Around line 189-199: The test sets a hard-coded cookie name
"boundless_auth.session_token" which mirrors better-auth's cookieName +
".session_token" and can break silently if auth config changes; update the test
in e2e/bounty-application.spec.ts to either import a shared constant from the
auth configuration (the cookieName or full session cookie constant) and use that
when composing the cookie name, or at minimum add a clear inline comment above
the addCookies call referencing the better-auth config key (cookieName) and
explaining the coupling so future changes update the test accordingly; target
the cookie-setting block that calls page.context().addCookies(...) and replace
the literal or annotate it.
- Line 234: Static-analysis flagged a ReDoS for creating a RegExp with
BOUNTY_ID, but BOUNTY_ID is a constant without regex metacharacters; replace the
regex-based URL assertion with a string-based matcher to avoid constructing a
RegExp. Update the assertion that currently builds new
RegExp(`/bounty/${BOUNTY_ID}`) to use
expect(page).toHaveURL(`/bounty/${BOUNTY_ID}`, { timeout: 10_000 }) (or the
equivalent string form) and keep BOUNTY_ID as the module-level constant
referenced in the assertion.
- Around line 309-352: Add a new E2E test that covers the failed-mutation path
by intercepting the submission request used by SubmitToBounty and returning an
error (either HTTP 500 or a JSON body like { errors: [...] }); name it something
like "shows error and keeps dialog open on failed submission" and model it off
the existing "submits application and closes dialog on success" test so you
still open the application dialog and fill inputs, then trigger the submit,
assert the application dialog remains visible (i.e., not closed by a swallowed
error) and assert appropriate failure UI state (e.g., an error toast, an inline
error, or that the submit button is re-enabled) to lock down current behavior in
bounty-detail-sidebar-cta.tsx which currently swallows mutation errors.
In `@playwright.config.ts`:
- Around line 3-32: Add an explicit outputDir to the Playwright config and
ensure local report dirs are git-ignored: in the defineConfig object (where
symbols like workers, fullyParallel, and reporter are set) add outputDir:
"./test-results" under the top-level config or use block so CI artifact upload
is deterministic, and update .gitignore to include playwright-report/,
test-results/, and blob-report/ so HTML reports and blobs are not committed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: dd1e3232-3e28-4dc5-bced-c4ec92078eb5
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (6)
components/bounty-detail/bounty-detail-sidebar-cta.tsxcomponents/bounty/application-dialog.tsxcomponents/bounty/bounty-card.tsxe2e/bounty-application.spec.tspackage.jsonplaywright.config.ts
|
@0xDeon Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Benjtalkshow
left a comment
There was a problem hiding this comment.
Hey @0xDeon, nice work getting a clean Playwright harness in place. CI is green this time, which is great to see.
One thing that needs addressing before merge: issue #130 explicitly lists "Tests run reliably in CI environment" as acceptance criterion 2, but the workflow (.github/workflows/ci.yml) still only runs pnpm lint and pnpm build. The test:e2e script is in package.json but nothing invokes it, so regressions in the apply flow won't fail a PR. Please add a CI job that runs pnpm exec playwright install chromium && pnpm run test:e2e. Without that, the acceptance criterion isn't actually met and the suite is effectively a local dev tool.
The webServer.command in playwright.config.ts points at npm run dev. Dev server JIT compilation is exactly why you had to bump navigationTimeout to 60s and pin workers: 1. Running against a production build (pnpm build && pnpm start) is faster, closer to what users actually see, and makes the suite less flaky. Worth switching, especially once this runs in CI.
The default route.abort("failed") in setupMocks does surface missing mocks loudly (good), but it also crashes the page with a network error if any unrelated query fires (for example, the bounty card imports useEscrowPool, and other widgets may mount during page load). You get a red test for reasons that aren't the scenario under test. Returning an empty but well-typed shape for low-priority queries, and reserving abort for known-critical ones, would cut the noise.
The auth mock at **/api/auth/** responds with the same session JSON for every endpoint, including sign-out, sign-in/*, etc. It's fine for today's tests but will bite the next person who tests a sign-out flow. Narrow to /api/auth/session or dispatch on URL path.
The success test filters console errors by string (favicon, net::ERR_, chrome-extension). Combined with the abort("failed") default handler, a real network error could easily show up as net::ERR_FAILED and get silently filtered. Either assert on specific behaviors (dialog closed, network request made with the right body) and drop the console-error check, or tighten the filter.
apply-to-bounty-btn-mobile is a new testid but nothing exercises it. The MobileCTA code path is untested, and since the Chromium project uses Desktop Chrome device, the mobile branch never renders. Either add a mobile project to the config or remove the testid.
toHaveURL at line 231 now uses an exact string match. A trailing slash or stray query param (e.g., analytics) will fail the assertion. Consider keeping it loose with a regex here since it's a navigation check, not a value check.
Please also address all of CodeRabbit's corrections (the portfolio URL masquerading as githubPullRequestUrl, and the error-swallowing catch). I see the commit claims these are fixed, so double-check they land cleanly on the final diff.
Rest looks solid. Hermetic network mocking and data-testid discipline are exactly right. Ping me once CI runs the suite.
Also Sync with main branch and fix conflict |
30683ea to
20aeec9
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/bounty-detail/bounty-detail-sidebar-cta.tsx (2)
398-407:⚠️ Potential issue | 🟡 MinorIcon-only cancel button needs an accessible name.
<Button>containing only<XCircle className="size-4" />exposes no accessible name to screen readers. Add anaria-label(e.g.,"Cancel bounty") so the action is announced.🛠️ Suggested fix
<Button + aria-label="Cancel bounty" variant="outline" size="lg" className="h-11 border-red-500/30 text-red-400 hover:bg-red-500/10 shrink-0" onClick={() => setCancelDialogOpen(true)} > <XCircle className="size-4" /> </Button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 398 - 407, The cancel icon button (the <Button> wrapping <XCircle /> in bounty-detail-sidebar-cta) has no accessible name; add an aria-label (for example "Cancel bounty") to the <Button> element so screen readers announce the action. Locate the JSX where canCancel renders the <Button> with onClick={() => setCancelDialogOpen(true)} and add the aria-label attribute to that <Button> (or use aria-labelledby if you prefer a visible label elsewhere) ensuring the XCircle icon remains purely decorative.
379-417:⚠️ Potential issue | 🟠 MajorMobile creator cannot cancel IN_PROGRESS bounty.
canCancelevaluates to true for bothOPENandIN_PROGRESSbounties when the user is the creator (line 362–363). However, in theMobileCTAcomponent, the cancel button is nested inside thecanActbranch (lines 398–407), which is only true when status isOPEN. When the bounty transitions toIN_PROGRESS,canActbecomes false, the component renders the disabled button (lines 409–417), and the cancel button disappears entirely — even thoughcanCancelis still true.The
AlertDialogat line 420 exists but is unreachable because the only call tosetCancelDialogOpen(true)is inside thecanActbranch.Compare this to the desktop
SidebarCTAcomponent (lines 230–243), where the cancel button is correctly positioned outside thecanActternary, rendering independently whenevercanCancelis true.Move the cancel button outside the
canActternary inMobileCTAso it remains reachable for IN_PROGRESS bounties.🛡️ Suggested fix
) : canAct ? ( <div className="flex gap-2"> <ApplicationDialog bountyTitle={bounty.title} onApply={applyToBounty} trigger={ <Button data-testid="apply-to-bounty-btn-mobile" className="flex-1 h-11 font-bold tracking-wide" size="lg" > {label()} </Button> } /> - {canCancel && ( - <Button - variant="outline" - size="lg" - className="h-11 border-red-500/30 text-red-400 hover:bg-red-500/10 shrink-0" - onClick={() => setCancelDialogOpen(true)} - > - <XCircle className="size-4" /> - </Button> - )} </div> ) : ( - <Button - className="w-full h-11 font-bold tracking-wide" - disabled - size="lg" - > - {label()} - </Button> + <div className="flex gap-2"> + <Button + className="flex-1 h-11 font-bold tracking-wide" + disabled + size="lg" + > + {label()} + </Button> + </div> )} + {canCancel && ( + <Button + aria-label="Cancel bounty" + variant="outline" + size="lg" + className="h-11 border-red-500/30 text-red-400 hover:bg-red-500/10 shrink-0 mt-2 w-full" + onClick={() => setCancelDialogOpen(true)} + > + <XCircle className="size-4 mr-2" /> Cancel Bounty + </Button> + )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 379 - 417, The mobile CTA currently nests the cancel Button inside the canAct branch so it disappears when canAct is false; update the MobileCTA JSX (the component rendering FcfsClaimButton / ApplicationDialog / disabled Button) to render the cancel Button based on canCancel outside of the canAct ternary so it shows regardless of canAct (like SidebarCTA). Keep the existing onClick that calls setCancelDialogOpen(true), preserve styling/classes and layout (wrap with the same surrounding container/div and maintain shrink/spacing), and ensure the AlertDialog that reads cancelDialogOpen remains reachable.
♻️ Duplicate comments (1)
components/bounty-detail/bounty-detail-sidebar-cta.tsx (1)
43-60:⚠️ Potential issue | 🟠 MajorPortfolio URL is still being persisted into
githubPullRequestUrl(and as""when blank).Two follow-on concerns with
useApplyToBounty:
- Semantic mismatch (still). The dialog collects a portfolio URL (profile/repo link), but the mutation stores it in
githubPullRequestUrl. For an OPEN COMPETITION bounty, applicants haven't produced a PR yet; this overloads a field whose name implies a real submission PR and will mislead any downstream consumer that treats it as one. Consider passing the portfolio URL as a dedicated input field (e.g.,portfolioUrl) and only settinggithubPullRequestUrlwhen the user actually provides one.- Empty-string fallback.
portfolioUrl ?? ""sends an empty string when the portfolio field is blank. If the GraphQL schema validatesgithubPullRequestUrlas a URL/non-empty string, this will reject server-side and surface as a confusing "Failed to submit application" toast in the dialog. If the field is optional, omit it entirely (undefined) instead of sending"".The previous "swallow error and return false" issue from the prior review is now correctly resolved —
mutateAsyncrejection propagates andApplicationDialogwill surface it viaform.setError("root", ...).🛡️ Suggested change
return async ({ coverLetter, portfolioUrl, }: { coverLetter: string; portfolioUrl?: string; }): Promise<void> => { await mutateAsync({ bountyId: bounty.id, - githubPullRequestUrl: portfolioUrl ?? "", + // TODO: switch to a dedicated portfolioUrl field on the schema + // instead of overloading githubPullRequestUrl. + githubPullRequestUrl: portfolioUrl, comments: coverLetter, }); toast.success("Application submitted successfully!"); };#!/bin/bash # Inspect the GraphQL input shape for SubmitToBounty to confirm whether # githubPullRequestUrl is required / URL-validated and whether a dedicated # portfolio field exists. fd -e graphql -e gql . | xargs rg -nP -C3 'CreateSubmissionInput|submitToBounty|githubPullRequestUrl|portfolioUrl' 2>/dev/null rg -nP -C3 'CreateSubmissionInput|githubPullRequestUrl' --type=ts🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 43 - 60, The useApplyToBounty helper is still mapping the dialog's portfolioUrl into githubPullRequestUrl and sending an empty string when absent; change the payload sent to mutateAsync so it uses a dedicated portfolioUrl property (e.g., pass portfolioUrl only as portfolioUrl: portfolioUrl when present) and only include githubPullRequestUrl when the user has provided an actual PR URL; do not send "" — omit the key or pass undefined to avoid server-side validation errors; update the call site in useApplyToBounty (the mutateAsync invocation) to construct the input object accordingly.
🧹 Nitpick comments (4)
e2e/bounty-application.spec.ts (3)
244-255: Minor: pre-click "not visible" assertion is racy / weak.
expect(...).not.toBeVisible()on line 250 passes whether the element is hidden or not in the DOM, and it's evaluated once with no auto-wait against a pre-render state. Radix's<DialogContent>only mounts whenopen === true, so the more precise (and faster) assertion istoHaveCount(0). Not blocking, just stronger:- await expect(page.getByTestId("application-dialog")).not.toBeVisible(); + await expect(page.getByTestId("application-dialog")).toHaveCount(0);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/bounty-application.spec.ts` around lines 244 - 255, In the "opens application dialog when apply button is clicked" test, replace the pre-click assertion that uses expect(page.getByTestId("application-dialog")).not.toBeVisible() with a count-based check so it asserts the dialog is not mounted (e.g., expect(page.getByTestId("application-dialog")).toHaveCount(0)); this targets the Radix Dialog mounting behavior and removes the racy visibility check before calling page.getByTestId("apply-to-bounty-btn").click().
408-430: Reset test only validates cover letter.Form reset is asserted only on the cover letter. Since
portfolioUrlshares the sameform.reset()path it likely works, but adding a one-line check guards against future regressions where one field is wired up differently than the other:// Cover letter must be empty after reset await expect(page.getByTestId("cover-letter-input")).toHaveValue(""); + await expect(page.getByTestId("portfolio-url-input")).toHaveValue("");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/bounty-application.spec.ts` around lines 408 - 430, The test "resets form when dialog is closed and reopened" currently only asserts the cover letter is cleared; add a one-line assertion to also check the portfolio URL field is reset. After reopening the dialog and before finishing the test, assert that the element with test id "portfolio-url-input" has an empty value (similar to the existing expect on "cover-letter-input") to ensure portfolioUrl is cleared by the same form.reset() path; locate this near the existing expects interacting with "application-dialog", "apply-to-bounty-btn", and "application-cancel-btn".
299-339: Success test asserts dialog close but not the mutation payload.The test confirms the dialog closes after submit, but doesn't assert what was actually sent to
SubmitToBounty. Given the open concern inbounty-detail-sidebar-cta.tsxabout portfolio URL being routed intogithubPullRequestUrl(and""when blank), capturing the request body in this test would catch any regression where the mapping silently changes. Optional:+ let submittedInput: unknown = null; + await page.route("**/api/graphql", async (route) => { + const body = JSON.parse(route.request().postData() ?? "{}"); + if (body.operationName === "SubmitToBounty") { + submittedInput = body.variables?.input; + } + await route.fallback(); + }); ... await expect(page.getByTestId("application-dialog")).not.toBeVisible(); + expect(submittedInput).toMatchObject({ + bountyId: BOUNTY_ID, + comments: expect.stringContaining("zero-knowledge"), + githubPullRequestUrl: "https://github.com/e2e-tester/zkp-demo", + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/bounty-application.spec.ts` around lines 299 - 339, Enhance the test to capture and assert the actual GraphQL mutation payload sent to SubmitToBounty: intercept the network request triggered by clicking submit (watch for the GraphQL operation named SubmitToBounty or the request to the GraphQL endpoint), await that request when clicking getByTestId("submit-application-btn"), parse its JSON body, and assert the variables include the cover letter and the correct field for the portfolio URL (verify portfolio value is sent to the intended variable — e.g., githubPullRequestUrl or portfolioUrl per bounty-detail-sidebar-cta.tsx mapping — and that an empty string is sent when the input is blank) so the test fails on any regression of the field mapping.package.json (1)
11-11: LGTM — Playwright test runner wired up correctly.The
test:e2escript and@playwright/test@^1.59.1devDependency align withplaywright.config.ts(testDir./e2e) and the newe2e/bounty-application.spec.ts. Note for CI:npx playwright install chromium(or a Playwright-provided container image) must run beforenpm run test:e2e, since browser binaries aren't bundled with the npm package. Worth wiring into the CI workflow when that's added.Also applies to: 90-90
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` at line 11, The e2e tests need Playwright browser binaries installed before running the "test:e2e" script; add a CI step that runs "npx playwright install chromium" (or the equivalent Playwright-provided container image) before invoking npm run test:e2e, ensuring the CI workflow executes this install step prior to the job that runs the Playwright test runner and referencing the existing devDependency `@playwright/test` and the "test:e2e" script.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 398-407: The cancel icon button (the <Button> wrapping <XCircle />
in bounty-detail-sidebar-cta) has no accessible name; add an aria-label (for
example "Cancel bounty") to the <Button> element so screen readers announce the
action. Locate the JSX where canCancel renders the <Button> with onClick={() =>
setCancelDialogOpen(true)} and add the aria-label attribute to that <Button> (or
use aria-labelledby if you prefer a visible label elsewhere) ensuring the
XCircle icon remains purely decorative.
- Around line 379-417: The mobile CTA currently nests the cancel Button inside
the canAct branch so it disappears when canAct is false; update the MobileCTA
JSX (the component rendering FcfsClaimButton / ApplicationDialog / disabled
Button) to render the cancel Button based on canCancel outside of the canAct
ternary so it shows regardless of canAct (like SidebarCTA). Keep the existing
onClick that calls setCancelDialogOpen(true), preserve styling/classes and
layout (wrap with the same surrounding container/div and maintain
shrink/spacing), and ensure the AlertDialog that reads cancelDialogOpen remains
reachable.
---
Duplicate comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 43-60: The useApplyToBounty helper is still mapping the dialog's
portfolioUrl into githubPullRequestUrl and sending an empty string when absent;
change the payload sent to mutateAsync so it uses a dedicated portfolioUrl
property (e.g., pass portfolioUrl only as portfolioUrl: portfolioUrl when
present) and only include githubPullRequestUrl when the user has provided an
actual PR URL; do not send "" — omit the key or pass undefined to avoid
server-side validation errors; update the call site in useApplyToBounty (the
mutateAsync invocation) to construct the input object accordingly.
---
Nitpick comments:
In `@e2e/bounty-application.spec.ts`:
- Around line 244-255: In the "opens application dialog when apply button is
clicked" test, replace the pre-click assertion that uses
expect(page.getByTestId("application-dialog")).not.toBeVisible() with a
count-based check so it asserts the dialog is not mounted (e.g.,
expect(page.getByTestId("application-dialog")).toHaveCount(0)); this targets the
Radix Dialog mounting behavior and removes the racy visibility check before
calling page.getByTestId("apply-to-bounty-btn").click().
- Around line 408-430: The test "resets form when dialog is closed and reopened"
currently only asserts the cover letter is cleared; add a one-line assertion to
also check the portfolio URL field is reset. After reopening the dialog and
before finishing the test, assert that the element with test id
"portfolio-url-input" has an empty value (similar to the existing expect on
"cover-letter-input") to ensure portfolioUrl is cleared by the same form.reset()
path; locate this near the existing expects interacting with
"application-dialog", "apply-to-bounty-btn", and "application-cancel-btn".
- Around line 299-339: Enhance the test to capture and assert the actual GraphQL
mutation payload sent to SubmitToBounty: intercept the network request triggered
by clicking submit (watch for the GraphQL operation named SubmitToBounty or the
request to the GraphQL endpoint), await that request when clicking
getByTestId("submit-application-btn"), parse its JSON body, and assert the
variables include the cover letter and the correct field for the portfolio URL
(verify portfolio value is sent to the intended variable — e.g.,
githubPullRequestUrl or portfolioUrl per bounty-detail-sidebar-cta.tsx mapping —
and that an empty string is sent when the input is blank) so the test fails on
any regression of the field mapping.
In `@package.json`:
- Line 11: The e2e tests need Playwright browser binaries installed before
running the "test:e2e" script; add a CI step that runs "npx playwright install
chromium" (or the equivalent Playwright-provided container image) before
invoking npm run test:e2e, ensuring the CI workflow executes this install step
prior to the job that runs the Playwright test runner and referencing the
existing devDependency `@playwright/test` and the "test:e2e" script.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: aa72feaa-0d82-4e17-94b7-5aaaef63627e
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
.gitignorecomponents/bounty-detail/bounty-detail-sidebar-cta.tsxcomponents/bounty/application-dialog.tsxcomponents/bounty/bounty-card.tsxe2e/bounty-application.spec.tspackage.jsonplaywright.config.ts
✅ Files skipped from review due to trivial changes (3)
- .gitignore
- components/bounty/bounty-card.tsx
- playwright.config.ts
|
@Benjtalkshow All done |
Thank you @0xDeon A few things blocking this PR: The branch still has merge conflicts ( The new e2e workflow is failing on The PR is titled as adding e2e tests, but Please address all CodeRabbit findings as well if there is any. |
Implements hermetic, non-flaky E2E tests for the full bounty application
user journey using Playwright:
bounty list → bounty detail → application dialog → fill form → submit
Changes:
- playwright.config.ts: Playwright config (headless Chromium, 1 worker,
60s navigation timeout, webServer auto-start)
- e2e/bounty-application.spec.ts: 10 test cases covering:
• Bounty list renders cards
• Card link navigates to detail page
• Apply button appears for OPEN non-FCFS bounties
• Dialog opens on button click
• Validation: empty cover letter blocked
• Validation: short cover letter blocked
• Validation: invalid portfolio URL rejected
• Full submission closes the dialog (success state)
• Submission works without an optional portfolio URL
• Form resets when dialog is closed and reopened
- components/bounty/application-dialog.tsx: add data-testid attrs
- components/bounty/bounty-card.tsx: add data-testid and data-bounty-id
- components/bounty-detail/bounty-detail-sidebar-cta.tsx: wire the
existing ApplicationDialog component for OPEN non-FCFS bounties so
the application form flow is reachable in the UI and testable E2E
- package.json: add "test:e2e": "playwright test" script
Flakiness prevention:
- All GraphQL and auth network requests intercepted via page.route() —
tests never depend on a live backend
- data-testid selectors throughout — no fragile CSS
- 1 worker prevents dev-server JIT compilation races
- await expect(...) everywhere — zero arbitrary sleeps
… mocks
- Switch sidebar CTA from raw useSubmitToBountyMutation to useSubmitToBounty
wrapper so cache invalidation fires on success
- Extract shared useApplyToBounty(bounty) hook, eliminating duplication
between SidebarCTA and MobileCTA
- onApply now throws on error (Promise<void>) so ApplicationDialog's catch
block sets form.setError("root", ...) instead of silently swallowing failures
- Fix githubPullRequestUrl fallback to "" instead of bounty.githubIssueUrl
- Add data-testid="application-cancel-btn" to Cancel button
- Scope Cancel click in reset test to application-dialog testid
- Replace new RegExp URL assertion with string + timeout: 10_000
- Add failing-mutation test (SubmitToBounty returns GraphQL error, dialog stays
open, application-error visible)
- Default GraphQL mock now aborts unrecognised operations instead of returning
null — surfaces missing handlers immediately
- Remove "Query data cannot be undefined" console filter (no longer needed)
- Add outputDir: "./test-results" to playwright.config.ts
- Add playwright-report/, test-results/, blob-report/ to .gitignore
- Regenerate pnpm-lock.yaml to include @playwright/test and fix
@graphql-codegen/typescript-react-query version mismatch (CI lockfile fix)
- Add test-e2e CI job: build → playwright install chromium → pnpm run test:e2e - Switch webServer.command to pnpm start (CI) / pnpm run dev (local) for prod-build reliability - Narrow auth mock to dispatch on URL path; only /session returns full session JSON - Remove fragile console-error filter block from success test - Remove untested apply-to-bounty-btn-mobile data-testid (no mobile project configured)
…ng of form errors Setting retry: 0 for mutations stops TanStack Query from retrying failed mutations (3x with exponential backoff = ~7s), which was causing the application-error element to appear after the Playwright 5s toBeVisible timeout and breaking the e2e error-handling test.
74d50d9 to
a8a96b7
Compare
|
Done @Benjtalkshow |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
components/bounty-detail/bounty-detail-sidebar-cta.tsx (1)
178-186: Avoid simulated async behavior in the “[Coming soon]” branch.This handler currently logs to console and sleeps for 1.5s without a real side effect. Prefer a disabled/static CTA (or an informational tooltip) until the flow is implemented.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 178 - 186, The onClick handler in the “[Coming soon]” CTA currently uses setIsApplying, console.log with bounty.id, and a 1.5s artificial delay to simulate async work; remove this simulated async behavior and replace the interactive flow with a non-actionable UI: disable the button (or render it as static) and/or attach an informational tooltip explaining the feature is coming soon instead of calling setIsApplying or sleeping; update any references to setIsApplying and the inline async onClick (and remove the console.log for bounty.id) in bounty-detail-sidebar-cta.tsx so the CTA presents a disabled state and no fake side effects.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 53-59: The success toast is shown unconditionally after
mutateAsync; instead inspect the mutation response from mutateAsync (the value
returned by the call in the submit handler in bounty-detail-sidebar-cta.tsx),
verify the expected successful payload (e.g., presence of created application id
or non-empty data field) and throw an error if that payload is missing or
contains GraphQL errors, then only call toast.success("Application submitted
successfully!") when the response is validated; ensure you reference and
validate the mutateAsync result and keep the existing toast and inputs
(bounty.id, portfolioUrl, coverLetter) unchanged.
In `@providers/query-provider.tsx`:
- Around line 18-20: Revert the global change in providers/query-provider.tsx so
defaultOptions.mutations.retry is not set to 0 (restore the resilient global
default or remove the override), and instead disable retries only on the
specific bounty submission mutation by adding { retry: 0 } to that mutation's
useMutation/options (the mutation that handles bounty submission, e.g., the
submitBounty/submitBountyMutation useMutation call); keep the existing
retryDelay logic global and ensure no other mutations are implicitly affected.
---
Nitpick comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 178-186: The onClick handler in the “[Coming soon]” CTA currently
uses setIsApplying, console.log with bounty.id, and a 1.5s artificial delay to
simulate async work; remove this simulated async behavior and replace the
interactive flow with a non-actionable UI: disable the button (or render it as
static) and/or attach an informational tooltip explaining the feature is coming
soon instead of calling setIsApplying or sleeping; update any references to
setIsApplying and the inline async onClick (and remove the console.log for
bounty.id) in bounty-detail-sidebar-cta.tsx so the CTA presents a disabled state
and no fake side effects.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3b52bd39-a132-49fa-9e2f-56a3c3fdb24d
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (9)
.github/workflows/ci.yml.gitignorecomponents/bounty-detail/bounty-detail-sidebar-cta.tsxcomponents/bounty/application-dialog.tsxcomponents/bounty/bounty-card.tsxe2e/bounty-application.spec.tspackage.jsonplaywright.config.tsproviders/query-provider.tsx
✅ Files skipped from review due to trivial changes (3)
- .gitignore
- components/bounty/bounty-card.tsx
- package.json
🚧 Files skipped from review as they are similar to previous changes (1)
- playwright.config.ts
- Restore global mutations.retry:3 (needed for auth, withdrawal, etc.) - Set retry:false only on useSubmitToBountyMutation to fix e2e timeout - Validate submitToBounty result before toasting success, throw on missing payload so GraphQL 200+errors responses surface as errors
|
The rebase resolved the conflict and the failing e2e test passes now — but the way both were achieved introduces serious problems. The rebase wiped the competition flow out of
The PR title says "add Playwright e2e tests" but the diff also rewrites the sidebar CTA and changes global retry config. Once the regression and the retry change are reverted, the scope will match the title. Please address all CodeRabbit findings as well. |
- Restore upstream/main version of bounty-detail-sidebar-cta.tsx in full (CompetitionStatus, CompetitionSubmission, useCompetitionJoinState, Join Competition button, past-deadline helper text all preserved from boundlessfi#178) - Add data-testid='apply-to-bounty-btn' only to the Join Competition buttons (SidebarCTA + MobileCTA) — no other sidebar changes - Rewrite e2e tests to test the actual join-competition flow: - Navigation (bounty list -> detail) - Join Competition button visible and enabled for OPEN bounty - Successful join transitions button to Joined state - Contract failure keeps button and shows toast error - Disabled state for non-OPEN bounty Contract client mocked via page.addInitScript(globalThis.__contestContracts) - Revert providers/query-provider.tsx — no global retry change - Keep retry:false scoped to useSubmitToBountyMutation only
|
@Benjtalkshow Both issues are fixed. The sidebar is fully restored, CompetitionStatus, CompetitionSubmission, useCompetitionJoinState, the Join Competition button, and the past-deadline helper text from #178 are all back in place. The only thing added to that file is data-testid="apply-to-bounty-btn" on the Join Competition button, nothing else changed. For the retry, query-provider.tsx is back to retry: 3 untouched. Instead, retry: false is now scoped only to useSubmitToBountyMutation in use-submission-mutations.ts, which is where it belongs since user-initiated form submissions shouldn't be silently retried anyway. The e2e tests have also been rewritten to match what actually exists in main. Since the Join Competition flow uses the contract client rather than GraphQL, the tests now inject a fake __contestContracts via page.addInitScript and cover navigation, button visibility, successful join, contract failure, and the disabled state for non-OPEN bounties. The ApplicationDialog form tests are gone since that dialog isn't wired into the sidebar CTA in main. Finally the codeRabbit findings have also been addressed too, the global retry regression is reverted, the success toast is guarded against a resolved-but-invalid response, and retry: false is scoped per-mutation rather than applied globally. I think thats all and i'm good to go |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
e2e/bounty-application.spec.ts (1)
58-68: Remove dead and brokeninjectContestClienthelper.This helper is never called — the success path uses the inline
addInitScriptinsetupMocks(lines 72‑76) and the failure path uses another inline script (lines 157‑161). Beyond being unused, the implementation is incorrect: it calls.toString()on theclaimBountyfunction and ships it as a string into the page, then assigns the wrapper object (whoseclaimBountyis now a string, not a function) toglobalThis.__contestContracts. If anyone wires this up later,claimBounty(...)will throw "is not a function".♻️ Either remove or fix to actually serialize the function via the init-script arg
Option A — delete it (preferred, since the inline scripts already cover both paths):
-type ContestContracts = { - claimBounty: (args: { contributor: string; bountyId: bigint }) => Promise<{ txHash: string }>; -}; - -async function injectContestClient(page: Page, impl: Partial<ContestContracts> = {}) { - await page.addInitScript((client) => { - (globalThis as { __contestContracts?: unknown }).__contestContracts = client; - }, { - claimBounty: impl.claimBounty?.toString() ?? (async () => ({ txHash: "0xfake" })).toString(), - }); -}Option B — if you want a reusable helper, pass behavior as a serializable spec and reconstruct the function inside the page context (e.g.
{ mode: 'success' | 'fail', txHash, error }) and buildclaimBountyfrom that inside the init script.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/bounty-application.spec.ts` around lines 58 - 68, Remove the unused and broken helper by deleting the ContestContracts type and the injectContestClient function (which incorrectly serializes claimBounty with .toString()); the test already wires mocks via inline page.addInitScript in setupMocks and the failure path, so remove these dead declarations and any imports/uses, or if you prefer a reusable helper implement it differently by passing a serializable spec (e.g. { mode: 'success'|'fail', txHash?, error? }) and reconstructing a real claimBounty function inside page.addInitScript instead of stringifying it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@e2e/bounty-application.spec.ts`:
- Around line 154-168: The comment about init-script ordering is incorrect:
update the comment near the addInitScript call so it explains that setupMocks
(run in beforeEach) registers the success claimBounty first and this
addInitScript runs second and intentionally overwrites
globalThis.__contestContracts with a failing implementation; reference the
functions/identifiers involved (setupMocks, page.addInitScript,
globalThis.__contestContracts, claimBounty) to make it clear that Playwright
runs init scripts in registration order and the later script replaces the
earlier contract mock.
---
Nitpick comments:
In `@e2e/bounty-application.spec.ts`:
- Around line 58-68: Remove the unused and broken helper by deleting the
ContestContracts type and the injectContestClient function (which incorrectly
serializes claimBounty with .toString()); the test already wires mocks via
inline page.addInitScript in setupMocks and the failure path, so remove these
dead declarations and any imports/uses, or if you prefer a reusable helper
implement it differently by passing a serializable spec (e.g. { mode:
'success'|'fail', txHash?, error? }) and reconstructing a real claimBounty
function inside page.addInitScript instead of stringifying it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 61228775-04ce-4fb5-a490-bbc42297041f
📒 Files selected for processing (3)
components/bounty-detail/bounty-detail-sidebar-cta.tsxe2e/bounty-application.spec.tshooks/use-submission-mutations.ts
✅ Files skipped from review due to trivial changes (1)
- components/bounty-detail/bounty-detail-sidebar-cta.tsx
There was a problem hiding this comment.
Both structural fixes are in. The competition flow is back in bounty-detail-sidebar-cta.tsx, and the retry change is correctly scoped to useSubmitToBounty. Thanks for that.
The e2e suite is still failing though. There have been five fix attempts and it’s still not passing. The current failure is clicking Join Competition transitions button to Joined state at line 143. The test is polling for the __claimBountyCalls counter from your mock contract, but it stays at 0 within the 8 second timeout, which means claimBounty() is never being called.
It’s likely that the mock globalThis.__contestContracts is being injected too late, after React has already read the value, or the click handler isn’t firing.
Please debug this locally using pnpm exec playwright test --ui instead of pushing multiple fix attempts and relying on CI. That loop is slow and the commit history is getting cluttered.
Once that test passes, this should be good to go.
"e2e-comp-bounty-01" stripped of dashes is "e2ecompbounty01" which
contains non-hex chars (o, m, p, u, t, y). toBountyIdBigInt() in
use-competition-bounty.ts throws ContestError("tx_failed") before
claimBounty is ever reached, keeping __claimBountyCalls at 0.
Changed to "e2ec0bcd-dead-beef-cafe-ab01cd02ef03" — a valid UUID whose
stripped form is 32 pure hex chars, so BigInt("0x...") succeeds and
claimBounty() is actually invoked on button click.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@e2e/bounty-application.spec.ts`:
- Around line 247-274: The test "shows error toast and keeps Join button when
contract call fails" currently only asserts the CTA remains visible; update it
to also assert the actual rendered failure message from the failing claimBounty
call is shown (e.g. check for the toast text "Contract: insufficient funds" or
an element with role="alert" containing that message) after clicking the
[data-testid="apply-to-bounty-btn"] so we verify the error flow and toast
rendering, not just that the button didn't switch to Joined.
- Around line 223-243: The test currently only checks that the contract call
occurred (globalThis.__claimBountyCalls) but doesn't verify the UI changed to
the Joined state; update the "clicking Join Competition transitions button to
Joined state" test to assert the post-claim UI by locating the same button
'[data-testid="apply-to-bounty-btn"]:visible' (or a dedicated joined selector if
present) and waiting for its text/attributes to reflect "Joined" (or for a
'[data-testid="joined-bounty-btn"]' element/disabled state) after the claim
completes; keep the poll on __claimBountyCalls for backend confirmation but add
an explicit expect on the button text/state in the test to ensure the UI
transition occurred.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8931d327-ec63-4f63-acf4-c45f68c0168a
📒 Files selected for processing (1)
e2e/bounty-application.spec.ts
| test("shows error toast and keeps Join button when contract call fails", async ({ | ||
| page, | ||
| }) => { | ||
| // Override the success client injected by setupMocks. Playwright runs | ||
| // page.addInitScript scripts in registration order on each navigation, | ||
| // so this test-level script is registered after setupMocks and overwrites | ||
| // globalThis.__contestContracts with a failing claimBounty implementation. | ||
| await page.addInitScript(() => { | ||
| (globalThis as { __contestContracts?: unknown }).__contestContracts = { | ||
| claimBounty: async () => { | ||
| throw new Error("Contract: insufficient funds"); | ||
| }, | ||
| }; | ||
| }); | ||
|
|
||
| await page.goto(`/bounty/${BOUNTY_ID}`); | ||
| await page | ||
| .locator('[data-testid="apply-to-bounty-btn"]:visible') | ||
| .first() | ||
| .click(); | ||
| // On failure the button must NOT transition to "Joined" | ||
| await expect( | ||
| page.locator('[data-testid="apply-to-bounty-btn"]:visible').first(), | ||
| ).toBeVisible({ timeout: 8_000 }); | ||
| await expect( | ||
| page.getByRole("button", { name: /Joined/i }), | ||
| ).not.toBeVisible(); | ||
| }); |
There was a problem hiding this comment.
Check the visible error state, not just that the CTA stays mounted.
As written, this can still pass if the click never reaches the contract layer or if the toast/error handling regresses. Add an assertion for the rendered failure message.
🛠️ Suggested follow-up
await page
.locator('[data-testid="apply-to-bounty-btn"]:visible')
.first()
.click();
+ await expect(page.getByText(/Contract: insufficient funds|Failed to join/i)).toBeVisible();
// On failure the button must NOT transition to "Joined"
await expect(
page.locator('[data-testid="apply-to-bounty-btn"]:visible').first(),
).toBeVisible({ timeout: 8_000 });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@e2e/bounty-application.spec.ts` around lines 247 - 274, The test "shows error
toast and keeps Join button when contract call fails" currently only asserts the
CTA remains visible; update it to also assert the actual rendered failure
message from the failing claimBounty call is shown (e.g. check for the toast
text "Contract: insufficient funds" or an element with role="alert" containing
that message) after clicking the [data-testid="apply-to-bounty-btn"] so we
verify the error flow and toast rendering, not just that the button didn't
switch to Joined.
The Join Competition button's disabled prop didn't depend on session state, so it was clickable before authClient.useSession() resolved. Clicking before walletAddress was populated caused handleJoin() to early-return on the !walletAddress guard, never reaching claimBounty(). This is what made the e2e "transitions button to Joined state" test flaky — Playwright clicked the visible button before the session response arrived, so the contract mock counter never incremented. Fix: gate the disabled prop on !walletAddress in both SidebarCTA and MobileCTA, and have the e2e test await toBeEnabled() before clicking so it deterministically waits for the session to resolve. This also closes a small UX gap — users with no wallet now see a disabled CTA instead of a clickable button that just toasts an error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auth mock used `endsWith("/session")` which doesn't match better-auth's
actual endpoint `/api/auth/get-session` ("get-session" doesn't end with
"/session" — the char before "session" is "-", not "/"). MOCK_SESSION never
got returned, so authClient.useSession() never resolved a user, walletAddress
stayed null, and the Join button stayed disabled past the 5s default toBeEnabled
timeout. Verified the failing test passes locally with this fix (6/6 green).
Also bump toBeEnabled to 10s in all three button-state tests as a safety margin
against slow CI session resolution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Benjtalkshow
left a comment
There was a problem hiding this comment.
Pushed some commits to fix the issue for ya!
|
Thank u @Benjtalkshow |
Summary
ApplicationDialogcomponent into the bounty detail sidebar CTA for non-FCFS OPEN bounties, making the application form reachabledata-testidattributes toBountyCard,ApplicationDialog, and the CTA button for stable, selector-based targetingCloses #130
Flow covered
Test cases (10 total — all pass)
How flakiness was avoided
page.route()— no live backend required. GraphQL mocks dispatch onoperationName; auth mocks respond at**/api/auth/**.data-testidselectors only — no CSS classes or positional selectors.await expect(...)everywhere — zeropage.waitForTimeout()or arbitrary sleeps.workers: 1— prevents Next.js JIT compilation races when multiple tests start simultaneously.navigationTimeout: 60_000— resilient against cold-start server compilation.How to run locally
Or to view the HTML report after a run:
Files changed
playwright.config.tse2e/bounty-application.spec.tspackage.jsontest:e2escriptcomponents/bounty/application-dialog.tsxdata-testidattrscomponents/bounty/bounty-card.tsxdata-testid,data-bounty-idcomponents/bounty-detail/bounty-detail-sidebar-cta.tsxApplicationDialogfor OPEN non-FCFS bountiesSummary by CodeRabbit
Tests
Bug Fixes