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
12 changes: 11 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,17 @@ npm run test:e2e

### Frontend Tests

Currently, the frontend does not have a dedicated test suite configured. Ensure any UI changes are manually verified in the browser before submitting a pull request.
The frontend uses [Vitest](https://vitest.dev/) with [Testing Library](https://testing-library.com/react) for unit tests.

```bash
# Run the test suite
cd frontend && npm test

# Watch mode (re-runs on file changes)
cd frontend && npm run test:watch
```

Tests live in `frontend/tests/` and use `.test.js` (or `.test.jsx`) extensions.

### Onchain Tests

Expand Down
47 changes: 27 additions & 20 deletions backend/src/multiplayer-queue/multiplayer-queue.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Injectable, NotFoundException, BadRequestException, Logger } from "@nestjs/common"
import { type Repository, MoreThan, DataSource } from "typeorm"
import { type Repository, LessThan, MoreThan } from "typeorm"
import { Cron, CronExpression } from "@nestjs/schedule"
import { type Queue, QueueStatus, SkillLevel } from "./entities/queue.entity"
import type { Match } from "./entities/match.entity"
import { Match as MatchEntity } from "./entities/match.entity"
import type { JoinQueueDto } from "./dto/join-queue.dto"
import type { QueueStatusDto } from "./dto/queue-status.dto"
import type { MatchResultDto } from "./dto/match-result.dto"
Expand All @@ -15,6 +17,7 @@ export class MultiplayerQueueService {
constructor(
private readonly queueRepository: Repository<Queue>,
private readonly matchRepository: Repository<Match>,
private readonly dataSource: DataSource,
) {}

/**
Expand Down Expand Up @@ -232,7 +235,9 @@ export class MultiplayerQueueService {
}

/**
* Create a match between players
* Create a match between players inside a transactional boundary so that
* an interrupt (crash / network error) after saving the match but before
* updating queue entries does not leave partial state.
*/
private async createMatch(players: Queue[]): Promise<Match> {
if (players.length < 2) {
Expand All @@ -245,28 +250,30 @@ export class MultiplayerQueueService {
return null
}

const match = this.matchRepository.create({
playerIds: players.map((p) => p.userId),
playerUsernames: players.map((p) => p.username),
gameMode: players[0].gameMode,
skillLevel: players[0].skillLevel,
averageWaitTime: Math.floor(players.reduce((sum, p) => sum + p.waitTime, 0) / players.length),
})

const savedMatch = await this.matchRepository.save(match)

// Update queue entries
for (const player of players) {
player.status = QueueStatus.MATCHED
player.matchId = savedMatch.id
player.matchedAt = new Date()
}
return this.dataSource.transaction(async (manager) => {
const match = manager.create(MatchEntity, {
playerIds: players.map((p) => p.userId),
playerUsernames: players.map((p) => p.username),
gameMode: players[0].gameMode,
skillLevel: players[0].skillLevel,
averageWaitTime: Math.floor(players.reduce((sum, p) => sum + p.waitTime, 0) / players.length),
})

const savedMatch = await manager.save(match)

// Update queue entries within the same transaction
for (const player of players) {
player.status = QueueStatus.MATCHED
player.matchId = savedMatch.id
player.matchedAt = new Date()
}

await this.queueRepository.save(players)
await manager.save(players)

this.logger.log(`Created match ${savedMatch.id} with players: ${players.map((p) => p.username).join(", ")}`)
this.logger.log(`Created match ${savedMatch.id} with players: ${players.map((p) => p.username).join(", ")}`)

return savedMatch
return savedMatch
})
}

/**
Expand Down
13 changes: 10 additions & 3 deletions frontend/components/ShareButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import React from "react";
import { Button } from "@/components/ui/button";
import { Share2 } from "lucide-react";

// Always share the canonical home/referral URL to prevent
// leaking puzzle IDs or internal routes via window.location.href.
const SHARE_URL =
process.env.NEXT_PUBLIC_SHARE_URL ||
process.env.NEXT_PUBLIC_APP_URL ||
"https://stellarhunts.com";

const ShareButton = () => {
const handleShare = async () => {
const shareMessage =
Expand All @@ -13,16 +20,16 @@ const ShareButton = () => {
await navigator.share({
title: "StellarHunts",
text: shareMessage,
url: window.location.href,
url: SHARE_URL,
});
} catch (err) {
navigator.clipboard.writeText(
`${shareMessage}\n${window.location.href}`
`${shareMessage}\n${SHARE_URL}`
);
alert("Share link copied to clipboard!");
}
} else {
navigator.clipboard.writeText(`${shareMessage}\n${window.location.href}`);
navigator.clipboard.writeText(`${shareMessage}\n${SHARE_URL}`);
alert("Share link copied to clipboard!");
}
};
Expand Down
10 changes: 8 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.3",
Expand All @@ -33,10 +35,14 @@
"zustand": "^5.0.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
"@types/node": "^20",
"eslint": "^8",
"eslint-config-next": "14.2.23",
"jsdom": "^29.1.1",
"postcss": "^8",
"tailwindcss": "^3.4.1"
"tailwindcss": "^3.4.1",
"vitest": "^4.1.10"
}
}
Loading