Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 37 additions & 16 deletions e2e/bounty-application.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const MOCK_MULTI_WINNER_BOUNTY_FRAGMENT = {
title: "Milestone 1: Design",
description: "Design the UI/UX for the feature.",
isCompleted: false,
}
},
],
contributorProgress: [],
maxSlots: 5,
Expand Down Expand Up @@ -136,11 +136,13 @@ async function setupMocks(page: Page) {
} as ContestContracts;

(globalThis as { __applyForSlotCalls?: number }).__applyForSlotCalls = 0;
(globalThis as { __applicationContracts?: unknown }).__applicationContracts = {
(
globalThis as { __applicationContracts?: unknown }
).__applicationContracts = {
applyForSlot: async () => {
(globalThis as { __applyForSlotCalls?: number }).__applyForSlotCalls =
((globalThis as { __applyForSlotCalls?: number }).__applyForSlotCalls ??
0) + 1;
((globalThis as { __applyForSlotCalls?: number })
.__applyForSlotCalls ?? 0) + 1;
return { txHash: "0xfake-e2e-slot-txhash" };
},
};
Expand Down Expand Up @@ -169,10 +171,14 @@ async function setupMocks(page: Page) {
});

await page.route("**/api/graphql", async (route) => {
let body: { operationName?: string } = {};
let body: {
operationName?: string;
variables?: { id?: string };
} = {};
try {
body = JSON.parse(route.request().postData() ?? "{}") as {
operationName?: string;
variables?: { id?: string };
};
} catch {
/* ignore */
Expand All @@ -186,7 +192,10 @@ async function setupMocks(page: Page) {
body: JSON.stringify({
data: {
bounties: {
bounties: [MOCK_BOUNTY_FRAGMENT, MOCK_MULTI_WINNER_BOUNTY_FRAGMENT],
bounties: [
MOCK_BOUNTY_FRAGMENT,
MOCK_MULTI_WINNER_BOUNTY_FRAGMENT,
],
total: 2,
limit: 20,
offset: 0,
Expand All @@ -195,11 +204,12 @@ async function setupMocks(page: Page) {
}),
});
return;
case "Bounty":
const requestedId = (body as any).variables?.id;
const bountyData = requestedId === BOUNTY_ID_MULTI
? MOCK_MULTI_WINNER_BOUNTY_FRAGMENT
: MOCK_BOUNTY_FRAGMENT;
case "Bounty": {
const requestedId = body.variables?.id;
const bountyData =
requestedId === BOUNTY_ID_MULTI
? MOCK_MULTI_WINNER_BOUNTY_FRAGMENT
: MOCK_BOUNTY_FRAGMENT;
await route.fulfill({
status: 200,
contentType: "application/json",
Expand All @@ -208,6 +218,7 @@ async function setupMocks(page: Page) {
}),
});
return;
}
case "TopContributors":
await route.fulfill({
status: 200,
Expand Down Expand Up @@ -389,7 +400,9 @@ test.describe("Bounty application flow", () => {
await expect(btn).toBeEnabled();
});

test("clicking Apply for Slot calls contract and updates UI", async ({ page }) => {
test("clicking Apply for Slot calls contract and updates UI", async ({
page,
}) => {
await page.goto(`/bounty/${BOUNTY_ID_MULTI}`);
const btn = page.getByRole("button", { name: "Apply for Slot" }).first();
await expect(btn).toBeEnabled();
Expand All @@ -409,14 +422,18 @@ test.describe("Bounty application flow", () => {
.toBeGreaterThan(0);
});

test("shows Slots Full and disables when slot count is at capacity", async ({ page }) => {
test("shows Slots Full and disables when slot count is at capacity", async ({
page,
}) => {
await page.route("**/api/graphql", async (route) => {
let body: { operationName?: string } = {};
try {
body = JSON.parse(route.request().postData() ?? "{}") as {
operationName?: string;
};
} catch { /* ignore */ }
} catch {
/* ignore */
}
if (body.operationName === "Bounty") {
await route.fulfill({
status: 200,
Expand All @@ -443,14 +460,18 @@ test.describe("Bounty application flow", () => {
await expect(btn).toBeDisabled();
});

test("shows Already Joined and disables when user is in contributorProgress", async ({ page }) => {
test("shows Already Joined and disables when user is in contributorProgress", async ({
page,
}) => {
await page.route("**/api/graphql", async (route) => {
let body: { operationName?: string } = {};
try {
body = JSON.parse(route.request().postData() ?? "{}") as {
operationName?: string;
};
} catch { /* ignore */ }
} catch {
/* ignore */
}
if (body.operationName === "Bounty") {
await route.fulfill({
status: 200,
Expand Down
31 changes: 18 additions & 13 deletions hooks/use-bounty-application.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"use client";

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { authClient } from "@/lib/auth-client";
import { bountyKeys } from "@/lib/query/query-keys";
import { MOCK_MODEL4_MILESTONES } from "@/lib/mock/model4";
import type { BountyQuery } from "@/lib/graphql/generated";
import type { ContributorProgress, Bounty } from "@/types/bounty";

// ---------------------------------------------------------------------------
// Contract client shape (resolved from globalThis.__applicationContracts)
Expand Down Expand Up @@ -257,10 +260,6 @@ export function useApproveApplicationSubmission() {
// Hook: apply for slot (BountyRegistry.apply_for_slot)
// ---------------------------------------------------------------------------

import { authClient } from "@/lib/auth-client";
import { MOCK_MODEL4_MILESTONES } from "@/lib/mock/model4";
import type { ContributorProgress, Bounty } from "@/types/bounty";

export function useApplyForSlot() {
const qc = useQueryClient();
const { data: session } = authClient.useSession();
Expand Down Expand Up @@ -290,21 +289,25 @@ export function useApplyForSlot() {
const newProgress: ContributorProgress = {
userId: session?.user?.id ?? "unknown-user",
userName: session?.user?.name ?? "Contributor",
userAvatarUrl: session?.user?.image ?? "https://github.com/shadcn.png",
userAvatarUrl:
session?.user?.image ?? "https://github.com/shadcn.png",
currentMilestoneId: firstMilestoneId,
};
const prevProgress = prev.bounty.contributorProgress ?? [];
const updatedProgress = [...prevProgress, newProgress];
const occupied = (prev.bounty.totalSlotsOccupied ?? 0) + 1;

qc.setQueryData<BountyQuery & { bounty?: Partial<Bounty> }>(bountyKeys.detail(bountyId), {
...prev,
bounty: {
...prev.bounty,
totalSlotsOccupied: occupied,
contributorProgress: updatedProgress,
qc.setQueryData<BountyQuery & { bounty?: Partial<Bounty> }>(
bountyKeys.detail(bountyId),
{
...prev,
bounty: {
...prev.bounty,
totalSlotsOccupied: occupied,
contributorProgress: updatedProgress,
},
},
});
);
}
return { prev, bountyId };
},
Expand All @@ -313,7 +316,9 @@ export function useApplyForSlot() {
},
onSettled: (_r, _e, variables) => {
if (variables?.bountyId) {
qc.invalidateQueries({ queryKey: bountyKeys.detail(variables.bountyId) });
qc.invalidateQueries({
queryKey: bountyKeys.detail(variables.bountyId),
});
}
qc.invalidateQueries({ queryKey: bountyKeys.lists() });
},
Expand Down
Loading