Files
personal-website/tests/game-tracker.test.ts
Stan Runge c2bc9050c2
Some checks failed
Test and deploy staging / staging (push) Failing after 2m50s
feat: build compact BO2 grind companion with cumulative ranks
2026-10-08 21:06:19 +00:00

1000 lines
37 KiB
TypeScript

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: 'Update my rank' }).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/);
await card.getByRole('button', { name: `Prestige up ${weapon.name}` }).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 + '?/adjustWeaponRank', {
itemId: String(weapon.id),
field: 'level',
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('**/*adjustWeaponRank', (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('**/*adjustWeaponRank');
await card.getByRole('button', { name: `Level up ${weapon.name}` }).click();
await expect(level).toHaveText(/LEVEL6/);
expect(
(
await action(page.request, cod + '?/adjustWeaponRank', {
itemId: String(weapon.id),
field: 'level',
delta: '999'
})
).status
).toBe(400);
expect(
(
await action(page.request, '/projects/game-tracker/call-of-duty/wwii?/adjustWeaponRank', {
itemId: String(weapon.id),
field: 'level',
delta: '1'
})
).status
).toBe(404);
const anonymous = await playwright.request.newContext({ baseURL: origin });
expect(
(
await action(anonymous, cod + '?/adjustWeaponRank', {
itemId: String(weapon.id),
field: 'level',
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();
await expect(card.getByRole('button', { name: 'Edit M27 level and prestige' })).toHaveText(
'LEVEL2/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);
expect(
(
await action(page.request, cod + '?/adjustWeaponRank', {
itemId: String(m27.id),
field: 'prestige',
delta: '1'
})
).status
).toBe(400);
await action(page.request, cod + '?/saveRank', {
itemId: String(m27.id),
level: '17',
prestige: '0'
});
const simultaneous = await Promise.all(
[1, 2].map(() =>
action(page.request, cod + '?/adjustWeaponRank', {
itemId: String(m27.id),
field: 'prestige',
delta: '1'
})
)
);
expect(simultaneous.filter((r) => r.status === 200)).toHaveLength(1);
expect(
(
await sql`select level,prestige from tracker_weapon_ranks where user_id=${userId} and item_id=${m27.id}`
)[0]
).toMatchObject({ level: 1, prestige: 1 });
await page.reload();
await expect(card).toContainText('18/51');
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(card).toContainText('51/51');
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);
});