Skip to content

Commit 1e0d4ba

Browse files
committed
feat: add Export GIF link to finished game pages (BS-c23cac20c43944fe)
Links to exporter.battlesnake.com with engine_url={BASE_URL}/api so the exporter pulls frames from the engine-compatible endpoint shipped in PR #127. Rendered only for finished games, as the last action in the theater row, for anonymous and authenticated viewers alike.
1 parent 26afc12 commit 1e0d4ba

2 files changed

Lines changed: 57 additions & 3 deletions

File tree

e2e/tests/game-public-view.spec.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import { query } from '../fixtures/db';
33

44
test.describe('Public Game Viewing', () => {
55
// Helper: create a game via the API using an authenticated page, return game_id
6-
async function createGameViaDb(): Promise<string> {
6+
async function createGameViaDb(
7+
status: 'waiting' | 'running' | 'finished' | 'failed' = 'finished'
8+
): Promise<string> {
79
// Insert a minimal game directly in the DB for testing view access
810
const result = await query<{ game_id: string }>(
911
`INSERT INTO games (board_size, game_type, status, created_at, updated_at)
10-
VALUES ('small', 'standard', 'finished', NOW(), NOW())
11-
RETURNING game_id::text AS game_id`
12+
VALUES ('small', 'standard', $1, NOW(), NOW())
13+
RETURNING game_id::text AS game_id`,
14+
[status]
1215
);
1316
return result[0].game_id;
1417
}
@@ -140,6 +143,33 @@ test.describe('Public Game Viewing', () => {
140143
await expect(meta.getByText('Survived to the 5,000-turn limit')).toBeVisible();
141144
await expect(meta.getByText('Cause of Death', { exact: true })).not.toBeVisible();
142145
});
146+
147+
test('finished game page shows an Export GIF link pointing at the exporter', async ({ page }) => {
148+
const gameId = await createGameViaDb();
149+
150+
await page.goto(`/games/${gameId}`);
151+
152+
// The e2e server env does not set BASE_URL, so the app falls back to its
153+
// default origin — the exporter href embeds that default.
154+
const link = page.getByRole('link', { name: 'Export GIF' });
155+
await expect(link).toHaveCount(1);
156+
await expect(link).toHaveAttribute(
157+
'href',
158+
`https://exporter.battlesnake.com/games/${gameId}/gif?engine_url=http://localhost:3000/api`
159+
);
160+
await expect(link).toHaveAttribute('target', '_blank');
161+
await expect(link).toHaveAttribute('rel', 'noopener');
162+
});
163+
164+
test('unfinished game pages show no Export GIF link', async ({ page }) => {
165+
for (const status of ['waiting', 'running', 'failed'] as const) {
166+
const gameId = await createGameViaDb(status);
167+
168+
await page.goto(`/games/${gameId}`);
169+
170+
await expect(page.getByRole('link', { name: 'Export GIF' })).toHaveCount(0);
171+
}
172+
});
143173
});
144174

145175
test.describe('Homepage Leaderboard Link for Unauthenticated Users', () => {

server/src/routes/game/view.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,13 @@ pub async fn view_game(
197197
a href="/leaderboards" class="btn" { "View Leaderboards" }
198198
a href="/" class="btn" { "Back to Home" }
199199
}
200+
@if finished {
201+
a
202+
href=(export_gif_url(&state.config.base_url, game_id))
203+
class="btn"
204+
target="_blank"
205+
rel="noopener" { "Export GIF" }
206+
}
200207
}
201208
}
202209

@@ -383,6 +390,14 @@ fn append_show_spoilers(url: String, show_spoilers: bool) -> String {
383390
format!("{url}{sep}showSpoilers=true")
384391
}
385392

393+
/// Build the exporter.battlesnake.com GIF URL for a game. The exporter
394+
/// fetches frame history from `{base_url}/api/games/{game_id}/frames`, so the
395+
/// `/api` suffix is load-bearing: dropping it silently sends the exporter to
396+
/// its default engine, which contains no Arena games.
397+
fn export_gif_url(base_url: &str, game_id: Uuid) -> String {
398+
format!("https://exporter.battlesnake.com/games/{game_id}/gif?engine_url={base_url}/api")
399+
}
400+
386401
fn ordinal_place(n: i32) -> String {
387402
let suffix = match (n % 10, n % 100) {
388403
(_, 11..=13) => "th",
@@ -528,6 +543,15 @@ mod tests {
528543
);
529544
}
530545

546+
#[test]
547+
fn export_gif_url_is_full_exporter_url_with_api_engine() {
548+
let url = export_gif_url("https://arena.example.com", game_id());
549+
assert_eq!(
550+
url,
551+
"https://exporter.battlesnake.com/games/6f9422eb-cd95-4a17-b0a2-a3fefe4f47b1/gif?engine_url=https://arena.example.com/api"
552+
);
553+
}
554+
531555
fn params(value: Option<&str>) -> ViewGameParams {
532556
ViewGameParams {
533557
show_spoilers: value.map(str::to_string),

0 commit comments

Comments
 (0)