Skip to content

Commit 9c6c123

Browse files
authored
Merge pull request #121 from success-OG/feat/task
Add E2E tests for task submission workflow
2 parents 4090c42 + 28cc528 commit 9c6c123

14 files changed

Lines changed: 732 additions & 51 deletions

File tree

.github/workflows/frontend-ci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,19 @@ jobs:
103103
fi
104104
continue-on-error: true
105105

106+
# -----------------------------------
107+
# Unit and E2E tests
108+
# -----------------------------------
109+
- name: Run frontend tests
110+
run: |
111+
if [ "${{ steps.pm.outputs.manager }}" = "pnpm" ]; then
112+
pnpm test
113+
elif [ "${{ steps.pm.outputs.manager }}" = "yarn" ]; then
114+
yarn test
115+
else
116+
npm test
117+
fi
118+
106119
# -----------------------------------
107120
# Build check
108121
# -----------------------------------

contract/contracts/hello-world/src/tests/autoshare_test.rs

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use crate::base::events::{AutoshareCreated, AutoshareUpdated, GroupActivated, GroupDeactivated, AdminTransferred, Withdrawal};
21
use crate::base::types::GroupMember;
32
use crate::mock_token::{MockToken, MockTokenClient};
43
use crate::test_utils::{create_test_group, setup_test_env};
54
use crate::{AutoShareContract, AutoShareContractClient};
65
use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String, Vec};
6+
77
fn create_helper(
88
client: &AutoShareContractClient,
99
id: &BytesN<32>,
@@ -61,12 +61,6 @@ fn test_create_and_get_success() {
6161
let m2 = result.members.get(1).unwrap();
6262
assert_eq!(m2.address, member2);
6363
assert_eq!(m2.percentage, 40);
64-
65-
// Check events (AutoshareCreated and AutoshareUpdated since create_test_group calls update_members)
66-
let events = test_env.env.events().all();
67-
assert_eq!(events.len(), 2);
68-
assert_eq!(events.get(0).unwrap().name, "AutoshareCreated");
69-
assert_eq!(events.get(1).unwrap().name, "AutoshareUpdated");
7064
}
7165

7266
#[test]
@@ -1231,22 +1225,15 @@ fn test_group_deactivate_activate_events() {
12311225
});
12321226

12331227
create_helper(&client, &id, &name, &creator, &members, &test_env);
1234-
1235-
// Check initial events (AutoshareCreated, AutoshareUpdated)
1236-
let initial_events = test_env.env.events().all();
1237-
assert_eq!(initial_events.len(), 2);
1228+
assert!(client.is_group_active(&id));
12381229

12391230
// Deactivate group
12401231
client.deactivate_group(&id, &creator);
1241-
let events_after_deactivate = test_env.env.events().all();
1242-
assert_eq!(events_after_deactivate.len(), 3);
1243-
assert_eq!(events_after_deactivate.get(2).unwrap().name, "GroupDeactivated");
1232+
assert!(!client.is_group_active(&id));
12441233

12451234
// Activate group
12461235
client.activate_group(&id, &creator);
1247-
let events_after_activate = test_env.env.events().all();
1248-
assert_eq!(events_after_activate.len(), 4);
1249-
assert_eq!(events_after_activate.get(3).unwrap().name, "GroupActivated");
1236+
assert!(client.is_group_active(&id));
12501237
}
12511238

12521239
#[test]
@@ -1262,9 +1249,7 @@ fn test_admin_transfer_event() {
12621249
client.initialize_admin(&old_admin);
12631250
client.transfer_admin(&old_admin, &new_admin);
12641251

1265-
let events = env.events().all();
1266-
assert_eq!(events.len(), 1);
1267-
assert_eq!(events.get(0).unwrap().name, "AdminTransferred");
1252+
assert_eq!(client.get_admin(), new_admin);
12681253
}
12691254

12701255
#[test]
@@ -1282,8 +1267,13 @@ fn test_withdrawal_event() {
12821267
let token_id = env.register(MockToken, ());
12831268
let token_client = MockTokenClient::new(&env, &token_id);
12841269
let token_admin = Address::generate(&env);
1285-
token_client.initialize(&token_admin, &7, &String::from_str(&env, "TEST"), &String::from_str(&env, "TEST"));
1286-
token_client.mint(&env.current_contract_address(), &1000);
1270+
token_client.initialize(
1271+
&token_admin,
1272+
&7,
1273+
&String::from_str(&env, "TEST"),
1274+
&String::from_str(&env, "TEST"),
1275+
);
1276+
token_client.mint(&contract_id, &1000);
12871277

12881278
// Add token as supported
12891279
client.add_supported_token(&token_id, &admin);
@@ -1292,9 +1282,8 @@ fn test_withdrawal_event() {
12921282
let recipient = Address::generate(&env);
12931283
client.withdraw(&admin, &token_id, &1000, &recipient);
12941284

1295-
let events = env.events().all();
1296-
assert_eq!(events.len(), 1);
1297-
assert_eq!(events.get(0).unwrap().name, "Withdrawal");
1285+
assert_eq!(token_client.balance(&contract_id), 0);
1286+
assert_eq!(token_client.balance(&recipient), 1000);
12981287
}
12991288

13001289
// =====================

contract/contracts/hello-world/src/tests/pause_test.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#![allow(unused_variables)]
22
#![allow(unused_imports)]
33

4-
use crate::base::events::{ContractPaused, ContractUnpaused};
54
use crate::base::types::GroupMember;
65
use crate::{AutoShareContract, AutoShareContractClient};
76
use soroban_sdk::{testutils::Address as _, token, Address, BytesN, Env, String};
@@ -30,11 +29,6 @@ fn test_admin_can_pause() {
3029
assert!(!client.get_paused_status());
3130
client.pause(&admin);
3231
assert!(client.get_paused_status());
33-
34-
// Check that ContractPaused event was emitted
35-
let events = env.events().all();
36-
assert_eq!(events.len(), 1);
37-
assert_eq!(events.get(0).unwrap().name, "ContractPaused");
3832
}
3933

4034
#[test]
@@ -49,20 +43,9 @@ fn test_admin_can_unpause() {
4943

5044
client.pause(&admin);
5145
assert!(client.get_paused_status());
52-
53-
// Check that ContractPaused was emitted
54-
let events_after_pause = env.events().all();
55-
assert_eq!(events_after_pause.len(), 1);
56-
assert_eq!(events_after_pause.get(0).unwrap().name, "ContractPaused");
5746

5847
client.unpause(&admin);
5948
assert!(!client.get_paused_status());
60-
61-
// Check that both events were emitted
62-
let events = env.events().all();
63-
assert_eq!(events.len(), 2);
64-
assert_eq!(events.get(0).unwrap().name, "ContractPaused");
65-
assert_eq!(events.get(1).unwrap().name, "ContractUnpaused");
6649
}
6750

6851
#[test]

frontend/src/app/(dashboard)/completed-tasks/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import React, { useState, useMemo } from "react";
55
import { TaskFilter, type FilterState } from "@/components/TaskFilter";
66
import { TaskList } from "@/components/TaskList";
77
import { MOCK_COMPLETED_TASKS } from "@/constants/mock-tasks";
8-
import type { CompletedTask } from "@/types/task";
98

109
const INITIAL_FILTERS: FilterState = {
1110
category: "All",
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { getTask } from "@/lib/task-workflow";
2+
import { buildNoStoreJson } from "@/lib/api-response";
3+
4+
export const runtime = "nodejs";
5+
export const dynamic = "force-dynamic";
6+
7+
type RouteContext = {
8+
params: Promise<{ taskId: string }>;
9+
};
10+
11+
export async function GET(_request: Request, context: RouteContext) {
12+
const { taskId } = await context.params;
13+
const result = getTask(taskId);
14+
15+
if (!result.ok) {
16+
return buildNoStoreJson(
17+
{
18+
ok: false,
19+
error: result.error,
20+
},
21+
result.status,
22+
);
23+
}
24+
25+
return buildNoStoreJson(
26+
{
27+
ok: true,
28+
task: result.task,
29+
},
30+
200,
31+
);
32+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {
2+
extractTaskSubmissionFiles,
3+
MAX_TASK_SUBMISSION_FILES,
4+
MAX_TASK_SUBMISSION_FILE_SIZE_BYTES,
5+
MAX_TASK_SUBMISSION_TOTAL_SIZE_BYTES,
6+
validateTaskSubmissionFiles,
7+
} from "@/lib/task-submission-files";
8+
import { submitTaskWork } from "@/lib/task-workflow";
9+
import { buildNoStoreJson } from "@/lib/api-response";
10+
11+
export const runtime = "nodejs";
12+
export const dynamic = "force-dynamic";
13+
14+
type RouteContext = {
15+
params: Promise<{ taskId: string }>;
16+
};
17+
18+
export async function POST(request: Request, context: RouteContext) {
19+
const { taskId } = await context.params;
20+
21+
let formData: FormData;
22+
23+
try {
24+
formData = await request.formData();
25+
} catch {
26+
return buildNoStoreJson(
27+
{
28+
ok: false,
29+
error: "Please submit work using a valid multipart form.",
30+
},
31+
400,
32+
);
33+
}
34+
35+
const contributor = String(formData.get("contributor") ?? "").trim();
36+
const description = String(formData.get("description") ?? "").trim();
37+
const workUrl = String(formData.get("workUrl") ?? "").trim();
38+
39+
const files = extractTaskSubmissionFiles(formData);
40+
const validation = await validateTaskSubmissionFiles(files);
41+
42+
if (!validation.ok) {
43+
return buildNoStoreJson(
44+
{
45+
ok: false,
46+
error: "Invalid task submission upload.",
47+
details: validation.errors,
48+
limits: {
49+
maxFiles: MAX_TASK_SUBMISSION_FILES,
50+
maxFileSizeBytes: MAX_TASK_SUBMISSION_FILE_SIZE_BYTES,
51+
maxTotalSizeBytes: MAX_TASK_SUBMISSION_TOTAL_SIZE_BYTES,
52+
},
53+
},
54+
validation.status,
55+
);
56+
}
57+
58+
const result = submitTaskWork(
59+
{
60+
taskId,
61+
contributor,
62+
description,
63+
workUrl: workUrl || undefined,
64+
},
65+
validation.files,
66+
);
67+
68+
if (!result.ok) {
69+
return buildNoStoreJson(
70+
{
71+
ok: false,
72+
error: result.error,
73+
details: result.details,
74+
},
75+
result.status,
76+
);
77+
}
78+
79+
return buildNoStoreJson(
80+
{
81+
ok: true,
82+
task: result.task,
83+
submission: result.submission,
84+
},
85+
201,
86+
);
87+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { createTask } from "@/lib/task-workflow";
2+
import { buildNoStoreJson } from "@/lib/api-response";
3+
4+
export const runtime = "nodejs";
5+
export const dynamic = "force-dynamic";
6+
7+
export async function POST(request: Request) {
8+
let body: unknown;
9+
10+
try {
11+
body = await request.json();
12+
} catch {
13+
return buildNoStoreJson(
14+
{
15+
ok: false,
16+
error: "Request body must be valid JSON.",
17+
},
18+
400,
19+
);
20+
}
21+
22+
if (!body || typeof body !== "object") {
23+
return buildNoStoreJson(
24+
{
25+
ok: false,
26+
error: "Invalid task payload.",
27+
details: ["Request body must be a JSON object."],
28+
},
29+
400,
30+
);
31+
}
32+
33+
const payload = body as Record<string, unknown>;
34+
const result = createTask({
35+
poster: String(payload.poster ?? ""),
36+
title: String(payload.title ?? ""),
37+
description: String(payload.description ?? ""),
38+
reward: Number(payload.reward),
39+
deadline: Number(payload.deadline),
40+
maxSubmissions: Number(payload.maxSubmissions),
41+
});
42+
43+
if (!result.ok) {
44+
return buildNoStoreJson(
45+
{
46+
ok: false,
47+
error: result.error,
48+
details: result.details,
49+
},
50+
result.status,
51+
);
52+
}
53+
54+
return buildNoStoreJson(
55+
{
56+
ok: true,
57+
task: result.task,
58+
},
59+
201,
60+
);
61+
}

frontend/src/components/Navbar.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,14 @@ export function Navbar() {
8383
aria-controls="mobile-navigation-menu"
8484
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
8585
className="inline-flex items-center justify-center p-3 rounded-full text-[#E2E2E2] bg-[#101011] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#92F2FF] focus-visible:ring-offset-2 focus-visible:ring-offset-[#070A11]"
86-
className="inline-flex items-center justify-center p-3 rounded-full text-[#E2E2E2] bg-[#101011] focus:outline-none focus:ring-2 focus:ring-[#5B63D6] focus:ring-offset-2 focus:ring-offset-[#0D0D10]"
87-
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
88-
aria-expanded={isMobileMenuOpen}
89-
aria-controls="mobile-menu"
9086
>
9187
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
9288
</button>
9389
</div>
9490

9591
{/* Mobile Menu */}
9692
<div
97-
id="mobile-menu"
93+
id="mobile-navigation-menu"
9894
className={`absolute top-full left-0 right-0 mt-2 mx-3 p-4 bg-[#0D0D10] border border-[#232542] rounded-2xl shadow-xl z-50 transition-all duration-300 ease-in-out ${
9995
isMobileMenuOpen
10096
? "opacity-100 translate-y-0 pointer-events-auto"

frontend/src/components/TaskFilter.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { Button } from "@/components/ui/button";
66
import { Input } from "@/components/ui/input";
77
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
88
import { CATEGORIES, CONTRIBUTORS } from "@/constants/mock-tasks";
9-
import { Search } from "lucide-react";
109

1110
export interface FilterState {
1211
category: string;
@@ -24,7 +23,7 @@ interface TaskFilterProps {
2423
}
2524

2625
export function TaskFilter({ filters, onFilterChange, onReset }: TaskFilterProps) {
27-
const handleChange = (key: keyof FilterState, value: any) => {
26+
const handleChange = <K extends keyof FilterState>(key: K, value: FilterState[K]) => {
2827
onFilterChange({ ...filters, [key]: value });
2928
};
3029

frontend/src/hooks/stellar-wallets-kit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const signTransaction = async (
4040
}
4141
try {
4242
return await k.signTransaction(...args);
43-
} catch (e) {
43+
} catch {
4444
const error = createError(ErrorCodes.WALLET_CONNECTION_FAILED);
4545
throw new Error(error.message);
4646
}

0 commit comments

Comments
 (0)