- {CODE} tokens in description/actions/flavor render as inline symbol
icons (RichText), including inside cost pips
- custom frames gain an optional background texture (upload/replace/
remove via /api/custom-frames/[id]/texture); renders behind panels
- custom games can be shared to the community (is_public): toggle in the
game space, public listing at /community/games, read-only game view,
/api/public/games endpoints (no auth, public rows only)
- /designer/print: multi-card print sheets on US Letter at 300dpi
(63x88mm cards, 3x3 or 2x2, dashed cut guides, full-sheet PNG export)
- migration 1787711511000
104 lines
2.6 KiB
JavaScript
104 lines
2.6 KiB
JavaScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('../../lib/sql.js', () => ({ sql: vi.fn() }));
|
|
|
|
import { sql } from '../../lib/sql.js';
|
|
import listHandler from '../../pages/api/public/games/index.js';
|
|
import itemHandler from '../../pages/api/public/games/[id].js';
|
|
|
|
function createRes() {
|
|
const res = {
|
|
statusCode: 200,
|
|
body: null,
|
|
status(code) {
|
|
res.statusCode = code;
|
|
return res;
|
|
},
|
|
json(data) {
|
|
res.body = data;
|
|
return res;
|
|
},
|
|
};
|
|
return res;
|
|
}
|
|
|
|
describe('/api/public/games', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
sql.mockResolvedValue({ rows: [] });
|
|
});
|
|
|
|
it('rejects non-GET methods', async () => {
|
|
const res = createRes();
|
|
|
|
await listHandler({ method: 'POST' }, res);
|
|
|
|
expect(res.statusCode).toBe(405);
|
|
expect(sql).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('lists public games with author and card counts', async () => {
|
|
sql.mockResolvedValueOnce({
|
|
rows: [{ id: 9, name: 'Aetherfall', author: 'rstillw', card_count: 4 }],
|
|
});
|
|
const res = createRes();
|
|
|
|
await listHandler({ method: 'GET' }, res);
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.games).toHaveLength(1);
|
|
const listed = sql.mock.calls[0][0].join('');
|
|
expect(listed).toContain('is_public = true');
|
|
});
|
|
});
|
|
|
|
describe('/api/public/games/[id]', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
sql.mockResolvedValue({ rows: [] });
|
|
});
|
|
|
|
it('rejects invalid ids', async () => {
|
|
const res = createRes();
|
|
|
|
await itemHandler({ method: 'GET', query: { id: 'abc' } }, res);
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
});
|
|
|
|
it('404s private or missing games', async () => {
|
|
sql.mockResolvedValueOnce({ rows: [] }); // public filter misses
|
|
const res = createRes();
|
|
|
|
await itemHandler({ method: 'GET', query: { id: '9' } }, res);
|
|
|
|
expect(res.statusCode).toBe(404);
|
|
});
|
|
|
|
it('returns the game and its designs with resolved frames', async () => {
|
|
sql
|
|
.mockResolvedValueOnce({
|
|
rows: [{ id: 9, name: 'Aetherfall', author: 'rstillw' }],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
rows: [
|
|
{
|
|
id: 3,
|
|
name: 'Stormsage',
|
|
frame_pk: 2,
|
|
frame_name: 'Molten',
|
|
frame_palette: { outer: '#111111' },
|
|
frame_texture: null,
|
|
},
|
|
],
|
|
});
|
|
const res = createRes();
|
|
|
|
await itemHandler({ method: 'GET', query: { id: '9' } }, res);
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.game.name).toBe('Aetherfall');
|
|
expect(res.body.designs[0].custom_frame.name).toBe('Molten');
|
|
expect(res.body.designs[0].frame_pk).toBeUndefined();
|
|
});
|
|
});
|