Files
personal-website/tests/game-tracker.test.ts
Stan Runge 0ee9ed0abb
All checks were successful
Test and deploy staging / staging (push) Successful in 2m29s
feat: roomier BO2 companion with +/- rank stepping
- Scale the COD page to the screen: larger gun art, text and progress
  ribbon, sized from one viewport unit so it fills a MacBook Pro 16"
  and big monitors and still fits 1280x720 without scrolling.
- Five columns on wide screens and four below, picked by CSS, so the
  server-rendered page already has the final layout.
- Replace the tabs/filters/overview rows with one toolbar of matching
  controls.
- Player and weapon ranks step with + and -: level 55 + 1 is the next
  prestige at level 1, up to max prestige. Presses show at once, batch
  while a save is running, and stepRank does the arithmetic in SQL.
- Clicking a rank still opens exact entry for big jumps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:50:19 +00:00

1007 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect, type APIRequestContext } from '@playwright/test';
import postgres from 'postgres';
test.skip(
!process.env.TRACKER_E2E,
'Opt-in: npm run test:tracker uses the migrated local development database.'
);
const origin = 'http://127.0.0.1:4174';
const users: string[] = [];
let sql: ReturnType<typeof postgres>;
test.beforeAll(() => {
if (!process.env.DB_URL) throw new Error('DB_URL required');
sql = postgres(process.env.DB_URL, { max: 1 });
});
test.afterAll(async () => {
if (!sql) return;
for (const id of users) await sql`delete from "user" where id=${id}`;
await sql.end();
});
async function register(request: APIRequestContext, attempt = 0): Promise<string> {
const response = await request.post('/api/auth/sign-up/email', {
headers: { Origin: origin },
data: {
name: 'Tracker Tester',
email: `tracker-${crypto.randomUUID()}@example.com`,
password: 'Tracker-test-password-2026!'
}
});
if (response.status() === 429 && attempt < 2) {
await new Promise((resolve) => setTimeout(resolve, 11000));
return register(request, attempt + 1);
}
expect(response.ok(), await response.text()).toBe(true);
const body = await response.json();
users.push(body.user.id);
return body.user.id as string;
}
async function action(
request: APIRequestContext,
path: string,
values: Record<string, string | string[]>
) {
const body = new URLSearchParams();
for (const [k, v] of Object.entries(values))
for (const value of Array.isArray(v) ? v : [v]) body.append(k, value);
const response = await request.post(path, {
headers: {
Origin: origin,
Accept: 'application/json',
'x-sveltekit-action': 'true',
'content-type': 'application/x-www-form-urlencoded'
},
data: body.toString()
});
const result = await response.json();
return { status: result.status ?? response.status(), type: result.type, result };
}
test('Platinum route, regional dex, catch journal, duplicate policy, team slots and ownership', async ({
page,
playwright
}) => {
const id = await register(page.request);
const [{ id: gameId }] = await sql`select id from games where slug='platinum'`;
const [{ id: pokemonId }] = await sql`select id from pokemon where number=387`;
await page.goto('/projects/game-tracker/pokemon?game=platinum');
await expect(page.getByRole('heading', { name: 'Pokémon journal' })).toBeVisible();
await expect(page.getByRole('button', { name: 'set rules', exact: true })).toBeEnabled({
timeout: 15000
});
await expect(page.getByRole('button', { name: /Turtwig Grass/ })).toBeVisible();
await expect(page.getByRole('button', { name: /Nidoran/ })).toHaveCount(0);
await page.getByRole('button', { name: 'set rules', exact: true }).click();
await expect(page.getByRole('dialog', { name: 'Set rules' })).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toHaveCount(0);
await page.getByRole('tab', { name: 'Pokédex', exact: true }).click();
await expect(page.getByLabel('Pokédex order')).toHaveValue('extended-sinnoh');
await expect(page.locator('article').first()).toContainText('Turtwig');
await page.getByLabel('Pokédex order').selectOption('national');
await expect(page.locator('article').first()).toContainText('Bulbasaur');
const catchValues = {
gameId: String(gameId),
pokemonId: String(pokemonId),
nickname: 'Sprout',
level: '5',
moves: ['Tackle', 'Withdraw'],
stage: 'story',
obtained: 'starter',
partySlot: '1',
caughtOn: '2026-09-25',
location: 'Route 201'
};
expect(
(await action(page.request, '/projects/game-tracker/pokemon?/saveCatch', catchValues)).status
).toBe(200);
const [first] = await sql`select * from pokemon_catches where user_id=${id}`;
expect(
(
await action(page.request, '/projects/game-tracker/pokemon?/saveCatch', {
...catchValues,
nickname: 'Second Sprout',
partySlot: '2'
})
).status
).toBe(200);
const [second] = await sql`select * from pokemon_catches where user_id=${id} and id<>${first.id}`;
await page.reload();
await page.getByRole('tab', { name: /My PC/ }).click();
await expect(page.getByRole('tabpanel').getByRole('button', { name: /#387/ })).toContainText(
'Sprout'
);
await expect(page.getByRole('tabpanel').getByRole('button', { name: /#387/ })).not.toContainText(
'Second Sprout'
);
await page.getByRole('tabpanel').getByRole('button', { name: /#387/ }).click();
await expect(page.getByRole('dialog')).toContainText('Meet Sprout');
await page.getByLabel('Level', { exact: true }).fill('12');
await page.getByRole('button', { name: 'Save Pokémon', exact: true }).click();
await expect(page.getByRole('dialog')).toHaveCount(0);
expect((await sql`select level from pokemon_catches where id=${first.id}`)[0].level).toBe(12);
expect(
(
await action(page.request, '/projects/game-tracker/pokemon?/saveCatch', {
...catchValues,
id: String(second.id),
nickname: 'Second Sprout',
partySlot: '1'
})
).status
).toBe(200);
expect(
(await sql`select party_slot from pokemon_catches where id=${first.id}`)[0].party_slot
).toBeNull();
expect(
(
await action(page.request, '/projects/game-tracker/pokemon?/saveCatch', {
...catchValues,
moves: ['a', 'b', 'c', 'd', 'e']
})
).status
).toBe(400);
const other = await playwright.request.newContext({ baseURL: origin });
await register(other);
expect(
(
await action(other, '/projects/game-tracker/pokemon?/saveCatch', {
...catchValues,
id: String(first.id)
})
).status
).toBe(404);
expect(
(await action(other, '/projects/game-tracker/pokemon?/deleteCatch', { id: String(first.id) }))
.status
).toBe(404);
await other.dispose();
expect(
(
await action(page.request, '/projects/game-tracker/pokemon?/saveJourney', {
gameId: String(gameId),
stage: 'postgame',
location: 'stark-mountain'
})
).status
).toBe(200);
expect(
(
await action(page.request, '/projects/game-tracker/pokemon?/saveMilestone', {
gameId: String(gameId),
stage: 'story',
key: 'league:champion-cynthia',
title: 'Champion · Cynthia',
category: 'league',
done: 'on'
})
).status
).toBe(200);
await page.reload();
await expect(page.getByLabel('My playthrough')).toHaveValue('postgame');
await expect(page.getByLabel('Encounter location')).toHaveValue('stark-mountain');
await page.screenshot({ path: '/tmp/tracker-pokemon-saved.png', fullPage: true });
const [entry] =
await sql`select id from pokemon_game_entries where game_id=${gameId} and pokemon_id=${pokemonId}`;
expect(
(
await action(page.request, '/projects/game-tracker/pokemon?/togglePokemonCaught', {
entryId: String(entry.id)
})
).status
).toBe(400);
for (const row of [first, second])
expect(
(
await action(page.request, '/projects/game-tracker/pokemon?/deleteCatch', {
id: String(row.id)
})
).status
).toBe(200);
expect(
(
await sql`select * from user_entries where user_id=${id} and pokemon_game_entry_id=${entry.id}`
).length
).toBe(1);
expect(
(
await action(page.request, '/projects/game-tracker/pokemon?/togglePokemonCaught', {
entryId: String(entry.id)
})
).status
).toBe(200);
});
test('weapon/player ranks, challenge progress and osu goals/scores/training persist independently', async ({
page,
playwright
}) => {
const id = await register(page.request);
const [weapon] =
await sql`select i.id, i.name from tracker_items i join tracker_games g on i.game_id=g.id where g.slug='black-ops-3' and i.kind='weapon' order by i.id limit 1`;
const [challenge] =
await sql`select c.id,c.target from tracker_challenges c join tracker_games g on c.game_id=g.id where g.slug='black-ops-3' and c.target>1 limit 1`;
const cod = '/projects/game-tracker/call-of-duty/black-ops-3';
await page.goto(cod);
await page.getByRole('button', { name: 'Player level and prestige' }).click();
await page.getByLabel('Current level', { exact: true }).fill('42');
await page.getByLabel('Prestige', { exact: true }).fill('3');
await page.getByRole('button', { name: 'Save rank' }).click();
await expect(page.getByRole('dialog')).toHaveCount(0);
await expect(page.getByLabel('Player level and prestige')).toContainText('LEVEL42');
await expect(page.getByLabel('Player level and prestige')).toContainText('PRESTIGE3');
expect(
(
await action(page.request, cod + '?/saveRank', {
itemId: String(weapon.id),
level: '17',
prestige: '2'
})
).status
).toBe(200);
expect(
(
await action(page.request, cod + '?/saveChallenge', {
challengeId: String(challenge.id),
current: String(challenge.target)
})
).status
).toBe(200);
await page.reload();
const weaponCard = page.getByRole('article', { name: weapon.name, exact: true });
await expect(
weaponCard.getByRole('button', { name: `Edit ${weapon.name} level and prestige` })
).toContainText('17');
await weaponCard.getByRole('button', { name: `${weapon.name} camos and milestones` }).click();
await expect(page.getByRole('dialog')).toContainText('Max level reached');
await page.keyboard.press('Escape');
await page.getByRole('tab', { name: 'Challenges', exact: true }).click();
await expect(page.locator('article')).toHaveCount(36);
const goal = {
kind: 'goal',
title: 'First 200 BPM FC',
mode: 'osu',
goal: 'fc',
skill: 'stamina',
playedOn: '2026-09-25',
rate: '1.00',
mods: 'NM',
beatmapUrl: 'https://osu.ppy.sh/beatmaps/123'
};
expect((await action(page.request, '/projects/game-tracker/osu?/save', goal)).status).toBe(200);
expect(
(
await action(page.request, '/projects/game-tracker/osu?/save', {
...goal,
kind: 'score',
title: 'My best play',
accuracy: '99.25',
combo: '1234',
misses: '0',
pp: '250.3',
scoreUrl: 'https://osu.ppy.sh/scores/123'
})
).status
).toBe(200);
expect(
(
await action(page.request, '/projects/game-tracker/osu?/save', {
...goal,
kind: 'training',
title: 'Stream practice',
bpm: '180',
duration: '60',
accuracy: '98.5'
})
).status
).toBe(200);
expect(
(
await action(page.request, '/projects/game-tracker/osu?/save', {
...goal,
kind: 'training',
title: 'Faster streams',
bpm: '140',
duration: '45',
rate: '1.50',
mods: 'DT',
accuracy: '97.5'
})
).status
).toBe(200);
expect(
(
await action(page.request, '/projects/game-tracker/osu?/save', {
...goal,
beatmapUrl: 'https://osu.ppy.sh.evil.test/beatmaps/123'
})
).status
).toBe(400);
expect(
(
await action(page.request, '/projects/game-tracker/osu?/save', {
...goal,
kind: 'training',
duration: '0',
bpm: '180'
})
).status
).toBe(400);
await page.goto('/projects/game-tracker/osu');
await expect(page.getByRole('heading', { name: 'First 200 BPM FC' })).toBeVisible();
await page.getByRole('button', { name: 'Achieved!', exact: true }).click();
await expect(page.getByText('✓ achieved')).toBeVisible();
await page.getByRole('tab', { name: 'Training log' }).click();
await expect(page.getByText('210 effective BPM')).toBeVisible();
await expect(page.getByRole('img', { name: /Last 2 qualifying/ })).toBeVisible();
await page.screenshot({ path: '/tmp/tracker-osu-saved.png', fullPage: true });
const [row] = await sql`select id from osu_entries where user_id=${id} limit 1`;
const other = await playwright.request.newContext({ baseURL: origin });
await register(other);
expect(
(await action(other, '/projects/game-tracker/osu?/save', { ...goal, id: String(row.id) }))
.status
).toBe(404);
expect(
(await action(other, '/projects/game-tracker/osu?/delete', { id: String(row.id) })).status
).toBe(404);
await other.dispose();
});
test('shared dialogs have focus, Escape dismissal and mobile layouts without overflow', async ({
page
}) => {
const id = await register(page.request);
await sql`update "user" set role='admin' where id=${id}`;
for (const [path, label, title] of [
['/blog', 'new article', 'new article'],
['/lists', 'new list', 'new list']
]) {
await page.goto(path);
await page.getByRole('button', { name: label, exact: true }).click();
const dialog = page.getByRole('dialog', { name: title });
await expect(dialog).toBeVisible();
await expect(dialog.getByRole('textbox').first()).toBeFocused();
await page.keyboard.press('Escape');
await expect(dialog).toHaveCount(0);
await expect(page.getByRole('button', { name: label, exact: true })).toBeFocused();
}
await page.goto('/projects/money-tracker');
await page.getByRole('heading', { name: 'Accounts', exact: true }).waitFor();
await page.locator('#accounts').getByRole('button', { name: 'Add', exact: true }).click();
const moneyDialog = page.getByRole('dialog');
await expect(moneyDialog).toBeVisible();
await moneyDialog.getByLabel('Account name', { exact: true }).fill('Tracker integration account');
await moneyDialog.getByLabel(/^Opening balance/).fill('123.45');
await moneyDialog.getByRole('button', { name: 'Save', exact: true }).click();
await expect(moneyDialog).toHaveCount(0);
expect(
(await sql`select opening_balance_cents from money_accounts where user_id=${id}`)[0]
.opening_balance_cents
).toBe(12345);
await page.setViewportSize({ width: 390, height: 844 });
for (const path of [
'/projects/game-tracker',
'/projects/game-tracker/pokemon?game=platinum',
'/projects/game-tracker/osu',
'/projects/game-tracker/call-of-duty/black-ops-3'
]) {
await page.goto(path);
await expect(page.locator('h1')).not.toHaveText('500');
expect(
await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true);
}
await page.goto('/projects/game-tracker/pokemon?game=platinum');
await expect(page.getByRole('button', { name: /Turtwig Grass/ })).toBeVisible();
await page.screenshot({ path: '/tmp/tracker-pokemon-mobile.png', fullPage: true });
});
test('illustrated library uses two Pokémon columns, follows selected games, and picture buttons select one game', async ({
page
}) => {
await register(page.request);
const games = await sql<{ id: number; slug: string }[]>`select id, slug from games`;
const selected = ['platinum', 'red', 'white', 'emerald', 'legends-arceus'];
expect(
(
await action(page.request, '/projects/game-tracker/pokemon?/setGames', {
gameIds: games.filter((g) => selected.includes(g.slug)).map((g) => String(g.id))
})
).status
).toBe(200);
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('/projects/game-tracker');
await expect(page.locator('.collection-grid .game-row')).toHaveCount(5);
await expect(page.locator('.collection-grid .game-name')).toHaveText([
'Red',
'Emerald',
'Platinum',
'White',
'Legends: Arceus'
]);
await expect(page.locator('.cod-rows .game-row')).toHaveCount(7);
await expect
.poll(() =>
page
.locator('main img')
.evaluateAll((images) =>
images.every(
(i) => (i as HTMLImageElement).complete && (i as HTMLImageElement).naturalWidth > 0
)
)
)
.toBe(true);
await page.screenshot({ path: '/tmp/tracker-library-pictures.png', fullPage: true });
await page.getByRole('link', { name: /Platinum artwork Platinum/ }).click();
await expect(page.getByRole('button', { name: 'Play Platinum', exact: true })).toHaveAttribute(
'aria-pressed',
'true'
);
await expect(page.getByRole('combobox', { name: 'Current game' })).toHaveCount(0);
await expect(page.getByRole('dialog')).toHaveCount(0);
const map = page.getByRole('img', { name: 'Sinnoh region map from the Pokémon games' });
await expect(map).toBeVisible();
await expect
.poll(() => map.evaluate((i) => (i as HTMLImageElement).naturalWidth))
.toBeGreaterThan(0);
await page.getByRole('button', { name: 'Explore Route 201', exact: true }).click();
await expect(page.getByRole('button', { name: /Turtwig Grass/ })).toBeVisible();
await map.scrollIntoViewIfNeeded();
await page.screenshot({ path: '/tmp/tracker-real-map.png' });
await page.getByRole('button', { name: 'Play White', exact: true }).click();
await expect(page).toHaveURL(/game=white/);
await expect(page.getByRole('button', { name: 'Play White', exact: true })).toHaveAttribute(
'aria-pressed',
'true'
);
await expect(page.getByRole('button', { name: 'Play Platinum', exact: true })).toHaveAttribute(
'aria-pressed',
'false'
);
// Large collections stay readable in exactly two columns; vertical page scrolling is allowed.
expect(
(
await action(page.request, '/projects/game-tracker/pokemon?/setGames', {
gameIds: games.map((g) => String(g.id))
})
).status
).toBe(200);
for (const [width, height] of [
[1440, 900],
[1280, 720]
]) {
await page.setViewportSize({ width, height });
await page.goto('/projects/game-tracker');
await expect(page.locator('.collection-grid .game-row')).toHaveCount(games.length);
const size = await page.evaluate(() => ({
height: document.documentElement.scrollHeight,
width: document.documentElement.scrollWidth,
viewportHeight: innerHeight,
viewportWidth: innerWidth
}));
expect(
await page
.locator('.collection-grid')
.evaluate((el) => getComputedStyle(el).gridTemplateColumns.split(' ').length)
).toBe(2);
expect(size.width).toBeLessThanOrEqual(size.viewportWidth);
const clipped = await page.locator('.game-row').evaluateAll(
(rows) =>
rows.filter((r) => {
const b = r.getBoundingClientRect();
return b.right > innerWidth || b.height < 56;
}).length
);
expect(clipped).toBe(0);
}
await page.screenshot({ path: '/tmp/tracker-library-all-games.png', fullPage: true });
await page.goto('/projects/game-tracker/pokemon?game=platinum');
await expect(
page.getByRole('group', { name: 'Choose Pokémon game' }).getByRole('button')
).toHaveCount(games.length);
await expect
.poll(() =>
page
.locator('.game-cover-button')
.evaluateAll(
(buttons) => new Set(buttons.map((b) => Math.round(b.getBoundingClientRect().top))).size
)
)
.toBe(2);
await expect(page.getByRole('button', { name: 'Switch game', exact: true })).toHaveCount(0);
const covers = await page.locator('.game-cover-button').evaluateAll((buttons) =>
buttons.every((b) => {
const r = b.getBoundingClientRect();
return r.left >= 0 && r.right <= innerWidth;
})
);
expect(covers).toBe(true);
});
test('live weapon controls save immediately, infer max level from prestige, and handle concurrent updates', async ({
page,
playwright
}) => {
const userId = await register(page.request);
const [weapon] =
await sql`select i.id,i.name from tracker_items i join tracker_games g on i.game_id=g.id where g.slug='black-ops-3' and i.kind='weapon' order by i.sort_order limit 1`;
const cod = '/projects/game-tracker/call-of-duty/black-ops-3';
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto(cod);
await page.getByRole('button', { name: `${weapon.name} camos and milestones` }).click();
const card = page.getByRole('dialog', { name: weapon.name, exact: true });
const level = card.getByRole('button', { name: `Edit ${weapon.name} level and prestige` });
await expect(card.getByRole('button', { name: `Level down ${weapon.name}` })).toBeDisabled();
await card.getByRole('button', { name: `Level up ${weapon.name}` }).click();
await expect(level).toHaveText(/LEVEL2/);
await card.getByRole('button', { name: `Level down ${weapon.name}` }).click();
await expect(level).toHaveText(/LEVEL1/);
// Without known caps + / − only move the level; prestige is typed into the exact-rank dialog.
await level.click();
await page.getByLabel('Prestige', { exact: true }).fill('1');
await page.getByRole('button', { name: 'Save rank' }).click();
await expect(page.getByRole('dialog')).toHaveCount(0);
await page.getByRole('button', { name: `${weapon.name} camos and milestones` }).click();
await expect(card).toContainText('Max level reached');
await expect(card.getByRole('checkbox', { name: /max level/ })).toHaveCount(0);
expect(
(
await sql`select level,prestige from tracker_weapon_ranks where user_id=${userId} and item_id=${weapon.id}`
)[0]
).toMatchObject({ level: 1, prestige: 1 });
const concurrent = await Promise.all(
Array.from({ length: 4 }, () =>
action(page.request, cod + '?/stepRank', { itemId: String(weapon.id), delta: '1' })
)
);
expect(concurrent.every((r) => r.status === 200)).toBe(true);
await page.reload();
await page.getByRole('button', { name: `${weapon.name} camos and milestones` }).click();
await expect(level).toHaveText(/LEVEL5/);
await page.route('**/*stepRank', (route) => route.abort());
await card.getByRole('button', { name: `Level up ${weapon.name}` }).click();
await expect(card.getByRole('status')).toContainText(/fetch|save|network/i);
await expect(level).toHaveText(/LEVEL5/);
await page.unroute('**/*stepRank');
await card.getByRole('button', { name: `Level up ${weapon.name}` }).click();
await expect(level).toHaveText(/LEVEL6/);
for (const delta of ['999', '0', '1.5', ''])
expect(
(await action(page.request, cod + '?/stepRank', { itemId: String(weapon.id), delta })).status
).toBe(400);
expect(
(
await action(page.request, '/projects/game-tracker/call-of-duty/wwii?/stepRank', {
itemId: String(weapon.id),
delta: '1'
})
).status
).toBe(404);
const anonymous = await playwright.request.newContext({ baseURL: origin });
expect(
(
await action(anonymous, cod + '?/stepRank', {
itemId: String(weapon.id),
delta: '1'
})
).status
).toBe(401);
await anonymous.dispose();
await page.keyboard.press('Escape');
const cards = await page
.locator('.weapon-card')
.evaluateAll((cards) => cards.map((c) => c.getBoundingClientRect().bottom));
expect(Math.max(...cards)).toBeLessThanOrEqual(800);
await page.screenshot({ path: '/tmp/tracker-live-cod.png' });
});
test('crowded Pokémon areas show every species without a scrolling list and allow a catch from the map', async ({
page
}) => {
await register(page.request);
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('/projects/game-tracker/pokemon?game=platinum');
await expect(page.getByRole('button', { name: /Turtwig Grass/ })).toBeVisible();
const [{ id: locationId }] =
await sql`select id from pokemon_locations where slug='sinnoh-route-210'`;
const response = page.waitForResponse((r) => {
const url = new URL(r.url());
return (
url.pathname.endsWith('/api/encounters') &&
url.searchParams.get('location') === String(locationId)
);
});
await page.getByLabel('Encounter location', { exact: true }).selectOption('sinnoh-route-210');
const rows = await (await response).json();
const special = page.getByRole('checkbox', { name: /Include special conditions/ });
if (await special.count()) await special.check();
const expected = new Set(rows.map((r: { number: number }) => r.number)).size;
const overview = page.getByRole('group', { name: 'All Pokémon in this area' });
await expect(overview.getByRole('button')).toHaveCount(expected);
expect(expected).toBeGreaterThan(20);
const bounds = await overview.evaluate((el) => {
const parent = el.getBoundingClientRect();
return {
height: el.clientHeight,
scroll: el.scrollHeight,
fits: [...el.querySelectorAll('button')].every((b) => {
const r = b.getBoundingClientRect();
return (
r.bottom <= parent.bottom + 1 &&
r.right <= parent.right + 1 &&
r.width >= 20 &&
r.height >= 20
);
})
};
});
expect(bounds.scroll).toBeLessThanOrEqual(bounds.height + 1);
expect(bounds.fits).toBe(true);
// The overview itself always fits without a scrolling list. A large cover library
// can move the workspace below the fold, so bring that panel into view first.
await overview.scrollIntoViewIfNeeded();
await expect
.poll(() => overview.evaluate((el) => el.getBoundingClientRect().bottom <= innerHeight))
.toBe(true);
await page.screenshot({ path: '/tmp/tracker-compact-pokemon.png' });
await page.getByRole('button', { name: 'Expand encounters' }).click();
const expanded = page.getByRole('dialog');
await expect(expanded.getByRole('group').getByRole('button')).toHaveCount(expected);
await expanded.getByRole('group').getByRole('button').first().click();
const details = page.getByRole('dialog').last();
await expect(details).toContainText('Lv.');
await details.getByRole('button', { name: 'Mark as seen', exact: true }).click();
await expect(details.getByRole('button', { name: '✓ Seen · undo', exact: true })).toBeEnabled();
await details.getByRole('button', { name: 'Register a catch', exact: true }).click();
const editor = page.getByRole('dialog', { name: /^Register / });
await expect(editor).toContainText('Add to my collection');
await expect(page.locator('[role="dialog"][data-state="closed"]')).toHaveCount(0);
await editor.getByLabel('Nickname', { exact: true }).fill('Route companion');
await editor.getByRole('button', { name: 'Add to my collection', exact: true }).click();
await expect(page.getByRole('dialog')).toHaveCount(0);
await expect(overview.getByRole('button').first()).toHaveAttribute('aria-label', /caught/);
for (const [width, height] of [
[1280, 720],
[768, 1024]
]) {
await page.setViewportSize({ width, height });
await expect(overview.getByRole('button')).toHaveCount(expected);
await overview.scrollIntoViewIfNeeded();
await expect
.poll(() => overview.evaluate((el) => el.getBoundingClientRect().bottom <= innerHeight))
.toBe(true);
}
await page.setViewportSize({ width: 390, height: 844 });
await expect(page.getByRole('tab', { name: 'Area Pokémon', exact: true })).toBeVisible();
await expect(overview.getByRole('button')).toHaveCount(expected);
await overview.scrollIntoViewIfNeeded();
await expect
.poll(() => overview.evaluate((el) => el.getBoundingClientRect().bottom <= innerHeight))
.toBe(true);
await page.screenshot({ path: '/tmp/tracker-crowded-mobile.png' });
await expect(
page.getByRole('img', { name: 'Sinnoh region map from the Pokémon games' })
).not.toBeVisible();
await page.getByRole('tab', { name: 'Region map', exact: true }).click();
await page.getByRole('button', { name: 'Explore Route 201', exact: true }).click();
await expect(page.getByRole('button', { name: /Turtwig Grass/ })).toBeVisible();
await page.screenshot({ path: '/tmp/tracker-live-mobile.png' });
});
test('visible progress reacts to sightings, catches and completed stories without double-counting species', async ({
page,
playwright
}) => {
const userId = await register(page.request);
const games = await sql<
{ id: number; slug: string }[]
>`select id,slug from games where slug in ('platinum','diamond')`;
const platinum = games.find((g) => g.slug === 'platinum')!;
const diamond = games.find((g) => g.slug === 'diamond')!;
const [species] = await sql`select id from pokemon where number=387`;
await action(page.request, '/projects/game-tracker/pokemon?/setGames', {
gameIds: games.map((g) => String(g.id))
});
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('/projects/game-tracker/pokemon?game=platinum');
const collection = page.getByRole('progressbar', { name: 'National collection', exact: true });
const sightings = page.getByRole('progressbar', {
name: 'Species seen at least once',
exact: true
});
const completed = page.getByRole('progressbar', { name: 'Games completed', exact: true });
const gameProgress = page.getByRole('progressbar', { name: 'Platinum completion', exact: true });
await expect(collection).toHaveAttribute('aria-valuenow', '0');
await page.getByRole('tab', { name: 'Pokédex', exact: true }).click();
await page.getByRole('checkbox', { name: 'Seen Turtwig in Platinum', exact: true }).check();
await expect
.poll(async () => Number(await sightings.getAttribute('aria-valuenow')))
.toBeGreaterThan(0);
await expect(collection).toHaveAttribute('aria-valuenow', '0');
await page.reload();
await expect
.poll(async () => Number(await sightings.getAttribute('aria-valuenow')))
.toBeGreaterThan(0);
await page.getByRole('tab', { name: 'Pokédex', exact: true }).click();
await page.getByRole('checkbox', { name: 'Caught Turtwig in Platinum', exact: true }).check();
await expect
.poll(async () => Number(await collection.getAttribute('aria-valuenow')))
.toBeGreaterThan(0);
const first = await collection.getAttribute('aria-valuenow');
const [entry] =
await sql`select id from pokemon_game_entries where game_id=${diamond.id} and pokemon_id=${species.id}`;
await action(page.request, '/projects/game-tracker/pokemon?/togglePokemonCaught', {
entryId: String(entry.id),
caught: 'on'
});
await page.reload();
await expect(collection).toHaveAttribute('aria-valuenow', first!);
const before = Number(await gameProgress.getAttribute('aria-valuenow'));
await page.getByRole('button', { name: 'Mark story complete', exact: true }).click();
await expect(completed).toHaveAttribute('aria-valuenow', '50');
await expect
.poll(async () => Number(await gameProgress.getAttribute('aria-valuenow')))
.toBeGreaterThan(before);
await page.getByRole('tab', { name: 'Badges & milestones', exact: true }).click();
await page.getByRole('button', { name: 'Complete Coal · Roark', exact: true }).click();
await expect(
page.getByRole('progressbar', { name: 'Badges & trials', exact: true })
).toHaveAttribute('aria-valuenow', '12.5');
await page.getByRole('button', { name: 'Full breakdown', exact: true }).click();
await expect(
page.getByRole('dialog', { name: 'Your Pokémon progress' }).getByRole('progressbar')
).toHaveCount(16);
await page.screenshot({ path: '/tmp/tracker-progress-pokemon.png' });
await page.keyboard.press('Escape');
await action(page.request, '/projects/game-tracker/pokemon?/saveJourney', {
gameId: String(platinum.id),
stage: 'postgame',
location: 'sinnoh-route-201'
});
expect(
(
await sql`select completed from pokemon_journeys where user_id=${userId} and game_id=${platinum.id}`
)[0].completed
).toBe(true);
const unseen = await action(page.request, '/projects/game-tracker/pokemon?/setSeen', {
gameId: String(platinum.id),
pokemonId: String(species.id),
seen: 'off'
});
expect(unseen.status).toBe(400);
const anonymous = await playwright.request.newContext({ baseURL: origin });
expect(
(
await action(anonymous, '/projects/game-tracker/pokemon?/setSeen', {
gameId: String(platinum.id),
pokemonId: String(species.id),
seen: 'on'
})
).status
).toBe(401);
await anonymous.dispose();
await page.goto('/projects/game-tracker');
await expect(
page.getByRole('progressbar', { name: 'National collection', exact: true })
).toHaveAttribute('aria-valuenow', first!);
await expect(
page.getByRole('progressbar', { name: 'Pokémon games completed', exact: true })
).toHaveAttribute('aria-valuenow', '50');
await page.screenshot({ path: '/tmp/tracker-progress-index.png', fullPage: true });
});
test('partial COD challenges and osu goals visibly advance completion meters', async ({ page }) => {
await register(page.request);
const cod = '/projects/game-tracker/call-of-duty/black-ops-3';
const [challenge] =
await sql`select c.* from tracker_challenges c join tracker_games g on g.id=c.game_id where g.slug='black-ops-3' and c.target>=10 order by c.id limit 1`;
await page.goto(cod);
const overall = page.getByRole('progressbar', { name: 'Game completion', exact: true });
await expect(overall).toHaveAttribute('aria-valuenow', '0');
await page.getByRole('tab', { name: 'Challenges', exact: true }).click();
await page.getByLabel('Search challenges').fill(challenge.name);
const card = page
.getByRole('article')
.filter({ has: page.getByRole('heading', { name: challenge.name, exact: true }) });
await card.getByRole('spinbutton').fill(String(Math.floor(challenge.target / 2)));
await card.getByRole('button', { name: 'Save', exact: true }).click();
await expect
.poll(async () => Number(await overall.getAttribute('aria-valuenow')))
.toBeGreaterThan(0);
const partial = Number(await overall.getAttribute('aria-valuenow'));
await card.getByRole('checkbox', { name: 'Unlocked' }).check();
await card.getByRole('button', { name: 'Save', exact: true }).click();
await expect
.poll(async () => Number(await overall.getAttribute('aria-valuenow')))
.toBeGreaterThan(partial);
await page.getByRole('tab', { name: 'Weapons', exact: true }).click();
await page.screenshot({ path: '/tmp/tracker-progress-cod.png' });
await action(page.request, '/projects/game-tracker/osu?/save', {
kind: 'goal',
title: 'Progress test FC',
mode: 'osu',
goal: 'fc',
skill: 'stamina',
playedOn: '2026-09-25',
rate: '1.00',
mods: 'NM'
});
await page.goto('/projects/game-tracker/osu');
await expect(
page.getByRole('progressbar', { name: 'All map goals', exact: true })
).toHaveAttribute('aria-valuenow', '0');
await page.getByRole('button', { name: 'Achieved!', exact: true }).click();
await expect(
page.getByRole('progressbar', { name: 'All map goals', exact: true })
).toHaveAttribute('aria-valuenow', '100');
await expect(
page.getByRole('progressbar', { name: 'Full combo goals', exact: true })
).toHaveAttribute('aria-valuenow', '100');
});
test('BO2 armoury fits every gun, enforces caps and advances all rank progress with each level', async ({
page
}) => {
const userId = await register(page.request);
const cod = '/projects/game-tracker/call-of-duty/black-ops-2';
const [m27] =
await sql`select i.id from tracker_items i join tracker_games g on g.id=i.game_id where g.slug='black-ops-2' and i.name='M27'`;
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto(cod);
await expect(page.locator('.weapon-card')).toHaveCount(40);
for (const [width, height] of [
[1280, 720],
[1920, 1080]
]) {
await page.setViewportSize({ width, height });
const bounds = await page.locator('.weapon-card').evaluateAll((cards) =>
cards.map((c) => {
const b = c.getBoundingClientRect();
const section = c.closest('section')!.getBoundingClientRect();
return {
name: c.getAttribute('aria-label'),
fits:
b.bottom <= section.bottom + 1 &&
b.bottom <= innerHeight &&
b.right <= innerWidth &&
b.height >= 29
};
})
);
expect(bounds.filter((b) => !b.fits)).toEqual([]);
expect(await page.evaluate(() => document.documentElement.scrollHeight)).toBeLessThanOrEqual(
height
);
await expect
.poll(() =>
page
.locator('.weapon-card img')
.evaluateAll((images) =>
images.every(
(i) => (i as HTMLImageElement).complete && (i as HTMLImageElement).naturalWidth > 0
)
)
)
.toBe(true);
}
for (const name of [
'M27',
'AN-94',
'DSR 50',
'Peacekeeper',
'MP7',
'Vector K10',
'Skorpion EVO'
]) {
await expect(
page
.getByRole('article', { name, exact: true })
.getByRole('button', { name: `View ${name} artwork` })
).toBeVisible();
}
const card = page.getByRole('article', { name: 'M27', exact: true });
const levels = page.getByRole('progressbar', { name: 'Weapon levels', exact: true });
const overall = page.getByRole('progressbar', { name: 'Game completion', exact: true });
await expect(levels).toHaveAttribute('aria-valuenow', '0');
await card.getByRole('button', { name: 'Level up M27', exact: true }).click();
const m27Rank = card.getByRole('button', { name: 'Edit M27 level and prestige' });
await expect(m27Rank).toHaveText('P0 2/17');
const first = Number(await levels.getAttribute('aria-valuenow'));
expect(first).toBeGreaterThan(0);
const before = Number(await overall.getAttribute('aria-valuenow'));
await card.getByRole('button', { name: 'Level up M27', exact: true }).click();
await expect
.poll(async () => Number(await levels.getAttribute('aria-valuenow')))
.toBeGreaterThan(first);
await expect
.poll(async () => Number(await overall.getAttribute('aria-valuenow')))
.toBeGreaterThan(before);
expect(
(
await action(page.request, cod + '?/saveRank', {
itemId: String(m27.id),
level: '18',
prestige: '2'
})
).status
).toBe(400);
expect(
(
await action(page.request, cod + '?/saveRank', {
itemId: String(m27.id),
level: '17',
prestige: '3'
})
).status
).toBe(400);
await action(page.request, cod + '?/saveRank', {
itemId: String(m27.id),
level: '17',
prestige: '0'
});
// Presses from two devices at max level both count: level 17 → prestige 1 level 2.
const simultaneous = await Promise.all(
[1, 2].map(() =>
action(page.request, cod + '?/stepRank', { itemId: String(m27.id), delta: '1' })
)
);
expect(simultaneous.every((r) => r.status === 200)).toBe(true);
expect(
(
await sql`select level,prestige from tracker_weapon_ranks where user_id=${userId} and item_id=${m27.id}`
)[0]
).toMatchObject({ level: 2, prestige: 1 });
await page.reload();
await expect(m27Rank).toHaveText('P1 2/17');
await card.getByRole('button', { name: 'Level down M27', exact: true }).click();
await card.getByRole('button', { name: 'Level down M27', exact: true }).click();
await expect(m27Rank).toHaveText('P0 17/17');
await expect
.poll(
async () =>
(
await sql`select level,prestige from tracker_weapon_ranks where user_id=${userId} and item_id=${m27.id}`
)[0]
)
.toMatchObject({ level: 17, prestige: 0 });
await card.getByRole('button', { name: 'Level up M27', exact: true }).click();
await expect(m27Rank).toHaveText('P1 1/17');
await action(page.request, cod + '?/saveRank', { level: '55', prestige: '0' });
await page.reload();
const badge = page.getByLabel('Player level and prestige');
await page.getByRole('button', { name: 'Level up player rank', exact: true }).click();
await expect(badge).toContainText('PRESTIGE1/10');
await expect(badge).toContainText('LEVEL1/55');
await page.getByRole('button', { name: 'Level down player rank', exact: true }).click();
await expect(badge).toContainText('LEVEL55/55');
await expect
.poll(
async () => (await sql`select level,prestige from tracker_ranks where user_id=${userId}`)[0]
)
.toMatchObject({ level: 55, prestige: 0 });
await action(page.request, cod + '?/saveRank', {
itemId: String(m27.id),
level: '17',
prestige: '2'
});
await action(page.request, cod + '?/saveRank', { level: '42', prestige: '3' });
await page.reload();
await expect(m27Rank).toHaveText('P2 17/17');
await expect(card.getByRole('button', { name: 'Level up M27' })).toBeDisabled();
await expect(page.getByRole('img', { name: 'Prestige 3 emblem' })).toBeVisible();
const progress = await overall.getAttribute('aria-valuenow');
await page.goto('/projects/game-tracker');
await expect(
page.getByRole('progressbar', { name: 'Black Ops II progress', exact: true })
).toHaveAttribute('aria-valuenow', progress!);
await page.goto(cod);
await page.setViewportSize({ width: 1280, height: 720 });
await page.screenshot({ path: '/tmp/bo2-verified-desktop.png' });
await page.setViewportSize({ width: 390, height: 844 });
await page.screenshot({ path: '/tmp/bo2-verified-mobile.png', fullPage: true });
await expect
.poll(() => page.evaluate(() => document.documentElement.scrollWidth))
.toBeLessThanOrEqual(390);
});