feat(game-ui): port Ref progress bars

This commit is contained in:
2026-08-11 00:40:38 +00:00
parent 3b5ae0594f
commit 322a8533fb
18 changed files with 1161 additions and 89 deletions
+50 -2
View File
@@ -72,6 +72,17 @@ const myGeneral = (state: FixtureState) => ({
injury: 0,
experience: 100,
dedication: 200,
age: 30,
turnTime: '2026-01-01 00:10:00',
crewTypeId: 1,
traits: { personal: 'None', specialDomestic: 'None', specialWar: 'None' },
progression: {
experienceLevel: 1,
dedicationLevel: 2,
statExperience: { leadership: 7, strength: 8, intelligence: 9 },
statUpgradeLimit: 20,
dex: [350, 1_375, 3_500, 7_125, 1_275_975],
},
items: { horse: 'che_명마', weapon: null, book: null, item: null },
},
city: { id: 1, name: '업', level: 8, nationId: 1 },
@@ -118,6 +129,17 @@ const battleCenter = (state: FixtureState) => ({
crew: 300,
train: 80,
atmos: 90,
age: 30,
crewTypeId: 1,
equipment: { weapon: 'None', book: 'None', horse: 'None', item: 'None' },
traits: { personal: 'None', specialDomestic: 'None', specialWar: 'None' },
progression: {
experienceLevel: 1,
statExperience: { leadership: 7, strength: 8, intelligence: 9 },
statUpgradeLimit: 20,
dex: [350, 1_375, 3_500, 7_125, 1_275_975],
},
battleStats: { kills: 1, deaths: 2, fire: 0, killCrew: 300, deathCrew: 100, dex: [] },
},
{
id: 8,
@@ -137,6 +159,17 @@ const battleCenter = (state: FixtureState) => ({
crew: 100,
train: 60,
atmos: 60,
age: 20,
crewTypeId: 1,
equipment: { weapon: 'None', book: 'None', horse: 'None', item: 'None' },
traits: { personal: 'None', specialDomestic: 'None', specialWar: 'None' },
progression: {
experienceLevel: 0,
statExperience: { leadership: 0, strength: 0, intelligence: 0 },
statUpgradeLimit: 20,
dex: [0, 0, 0, 0, 0],
},
battleStats: { kills: 0, deaths: 0, fire: 0, killCrew: 0, deathCrew: 0, dex: [] },
},
],
});
@@ -153,10 +186,15 @@ const install = async (page: Page, state: FixtureState) => {
);
await page.route('**/image/game/**', async (route) => {
const filename = basename(new URL(route.request().url()).pathname);
if (legacyImageRoot && ['back_walnut.jpg', 'back_green.jpg', 'back_blue.jpg'].includes(filename)) {
if (
legacyImageRoot &&
['back_walnut.jpg', 'back_green.jpg', 'back_blue.jpg', 'pr5.gif', 'pb5.gif', 'pr8.gif', 'pb8.gif'].includes(
filename
)
) {
await route.fulfill({
status: 200,
contentType: 'image/jpeg',
contentType: filename.endsWith('.gif') ? 'image/gif' : 'image/jpeg',
body: await readFile(resolve(legacyImageRoot, filename)),
});
return;
@@ -455,6 +493,8 @@ test('내 정보&설정 keeps the legacy 1000px/500px geometry and saves in plac
await page.goto('my-page');
await expect(page.locator('.title-row')).toContainText('내 정 보');
await expect(page.locator('#set_my_setting')).toBeVisible();
await expect(page.locator('.general-column [role="progressbar"]')).toHaveCount(14);
await expect(page.locator('.general-column [aria-label*="1,275,975 (EX+)"]')).toHaveCount(5);
await expect.poll(() => state.generalMeQueries).toBeGreaterThan(0);
expect(state.accessPages).not.toContain('my-page');
const noDefenceOption = page.locator('option[value="999"]');
@@ -911,6 +951,14 @@ test('감찰부 keeps the selector interaction and shows the permission error pa
await expect(page.locator('.selector-row select').nth(1)).toHaveValue('8');
await page.getByRole('button', { name: '다음 ▶' }).click();
await expect(page.locator('.selector-row select').nth(1)).toHaveValue('7');
await expect(page.locator('.battle-general-card [role="progressbar"]')).toHaveCount(14);
await expect(page.locator('.battle-general-card [aria-label*="1,275,975 (EX+)"]')).toHaveCount(5);
expect(
await page
.locator('.battle-general-card [role="progressbar"]')
.first()
.evaluate((bar) => getComputedStyle(bar).backgroundImage)
).toContain('/game/pr8.gif');
const geometry = await page.locator('.battle-page').evaluate((element) => {
const selector = element.querySelector<HTMLElement>('.selector-row')!;
const controls = [...selector.children].map((child) => (child as HTMLElement).getBoundingClientRect());
@@ -54,6 +54,13 @@ const generalContext = (state: NavigationFixture) => ({
injury: 0,
experience: 100,
dedication: 200,
progression: {
experienceLevel: 1,
dedicationLevel: 2,
statExperience: { leadership: 5, strength: 10, intelligence: 15 },
statUpgradeLimit: 20,
dex: [350, 100_000, 500_000, 1_000_000, 1_275_975],
},
items: { horse: null, weapon: null, book: null, item: null },
},
city: {
@@ -385,6 +392,94 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`);
});
test('main city and general cards render every ref bar plus dual dexterity progress at ref heights', async ({
page,
}) => {
const state: NavigationFixture = {
officerLevel: 1,
permission: 0,
nationLevel: 1,
stage: 0,
npcMode: 1,
generalMeCalls: 0,
operations: [],
};
await installFixture(page, state);
await page.setViewportSize({ width: 1200, height: 900 });
await waitForMain(page);
const cityBars = page.locator('[data-main-target="city"] [role="progressbar"]');
const statBars = page.locator('[data-stat-progress] [role="progressbar"]');
const experienceBar = page.locator('[data-experience-progress] [role="progressbar"]');
const dexRows = page.locator('[data-dex-progress]');
await expect(cityBars).toHaveCount(8);
await expect(statBars).toHaveCount(3);
await expect(experienceBar).toHaveCount(1);
await expect(dexRows).toHaveCount(5);
await expect(dexRows.locator('[role="progressbar"]')).toHaveCount(10);
expect(await cityBars.first().evaluate((element) => element.getBoundingClientRect().height)).toBe(9);
expect(await statBars.first().evaluate((element) => element.getBoundingClientRect().height)).toBe(12);
expect(await experienceBar.evaluate((element) => element.getBoundingClientRect().height)).toBe(12);
const firstDexBars = dexRows.first().locator('[role="progressbar"]');
expect(await firstDexBars.nth(0).evaluate((element) => element.getBoundingClientRect().height)).toBe(12);
expect(await firstDexBars.nth(1).evaluate((element) => element.getBoundingClientRect().height)).toBe(9);
await expect(dexRows.nth(3).locator('[role="progressbar"]').nth(0)).toHaveAttribute('aria-valuemax', '100');
await expect(dexRows.nth(3).locator('[role="progressbar"]').nth(0)).toHaveAttribute(
'aria-label',
/1,000,000 \/ 1,275,975 \(EX\+\)/
);
await expect(dexRows.nth(3).locator('[role="progressbar"]').nth(1)).toHaveAttribute('aria-label', /까지 .* 남음/);
await expect(dexRows.nth(4).locator('[role="progressbar"]').nth(1)).toHaveAttribute('aria-label', /EX\+ 달성/);
const texture = await cityBars.first().evaluate((element) => getComputedStyle(element).backgroundImage);
const fillTexture = await cityBars
.first()
.locator('.legacy-progress__fill')
.evaluate((element) => getComputedStyle(element).backgroundImage);
expect(texture).toContain('/game/pr5.gif');
expect(fillTexture).toContain('/game/pb5.gif');
const captureProgress = async (name: string) => {
if (!artifactRoot) return;
await mkdir(artifactRoot, { recursive: true });
const measurement = await page.locator('.legacy-progress').evaluateAll((elements) =>
elements.map((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
const fill = element.querySelector<HTMLElement>('.legacy-progress__fill');
return {
label: element.getAttribute('aria-label'),
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
borderTop: style.borderTop,
borderBottom: style.borderBottom,
backgroundImage: style.backgroundImage,
fillWidth: fill?.getBoundingClientRect().width ?? 0,
fillBackgroundImage: fill ? getComputedStyle(fill).backgroundImage : '',
};
})
);
await Promise.all([
page.screenshot({ path: resolve(artifactRoot, `progress-bars-${name}.png`), fullPage: true }),
writeFile(resolve(artifactRoot, `progress-bars-${name}.json`), `${JSON.stringify(measurement, null, 2)}\n`),
]);
};
await captureProgress('desktop-1200');
await page.setViewportSize({ width: 500, height: 900 });
await expect(page.locator('.layout-mobile')).toBeVisible();
await expect(page.locator('[data-main-target="city"] [role="progressbar"]')).toHaveCount(8);
await expect(page.locator('[data-main-target="general"] [role="progressbar"]')).toHaveCount(14);
expect(
await page
.locator('[data-main-target="city"] [role="progressbar"]')
.first()
.evaluate((element) => element.getBoundingClientRect().height)
).toBe(9);
await captureProgress('mobile-500');
});
test('the 939/940 boundary switches to the Ref-style 502px single document', async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 5,
+43 -10
View File
@@ -22,7 +22,35 @@ const readReferenceImage = async (filename: string): Promise<Buffer> => {
throw new Error(`Reference image not found: ${filename}`);
};
type Member = { id: number; name: string; cityId: number; cityName: string };
type Member = {
id: number;
name: string;
cityId: number;
cityName: string;
stats: { leadership: number; strength: number; intelligence: number };
experience: number;
progression: {
experienceLevel: number;
statExperience: { leadership: number; strength: number; intelligence: number };
statUpgradeLimit: number;
dex: number[];
};
};
const member = (id: number, name: string, cityId: number, cityName: string): Member => ({
id,
name,
cityId,
cityName,
stats: { leadership: 70, strength: 60, intelligence: 50 },
experience: 450,
progression: {
experienceLevel: 4,
statExperience: { leadership: 7, strength: 8, intelligence: 9 },
statUpgradeLimit: 20,
dex: [350, 1_375, 3_500, 7_125, 1_275_975],
},
});
type TroopFixture = {
id: number;
name: string;
@@ -61,11 +89,7 @@ const baseTroops = (): TroopFixture[] => [
picture: 'default.jpg',
imageServer: 0,
},
members: [
{ id: 1, name: '공손찬', cityId: 1, cityName: '북평' },
{ id: 3, name: '조운', cityId: 1, cityName: '북평' },
{ id: 4, name: '전예', cityId: 2, cityName: '계' },
],
members: [member(1, '공손찬', 1, '북평'), member(3, '조운', 1, '북평'), member(4, '전예', 2, '계')],
},
{
id: 2,
@@ -81,7 +105,7 @@ const baseTroops = (): TroopFixture[] => [
picture: 'default.jpg',
imageServer: 0,
},
members: [{ id: 2, name: '관우', cityId: 2, cityName: '계' }],
members: [member(2, '관우', 2, '계')],
},
];
@@ -124,11 +148,11 @@ const installApiFixture = async (page: Page, state: FixtureState) => {
window.localStorage.setItem('sammo-game-token', 'ga_playwright');
window.localStorage.setItem('sammo-game-profile', profile);
}, gameProfile);
for (const filename of ['back_walnut.jpg', 'back_green.jpg']) {
for (const filename of ['back_walnut.jpg', 'back_green.jpg', 'pr5.gif', 'pb5.gif', 'pr8.gif', 'pb8.gif']) {
await page.route(`**/image/game/${filename}`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'image/jpeg',
contentType: filename.endsWith('.gif') ? 'image/gif' : 'image/jpeg',
body: await readReferenceImage(filename),
});
});
@@ -182,7 +206,7 @@ const installApiFixture = async (page: Page, state: FixtureState) => {
picture: 'default.jpg',
imageServer: 0,
},
members: [{ id: createdId, name: '유비', cityId: 1, cityName: '북평' }],
members: [member(createdId, '유비', 1, '북평')],
});
return response({ ok: true, troopId: createdId, troopName: '신규대' });
}
@@ -270,6 +294,15 @@ test('renders the legacy desktop grid with matching computed geometry and states
await page.locator('.troopMember').nth(1).hover();
await expect(page.getByRole('tooltip')).toContainText('조운');
await expect(page.getByRole('tooltip').locator('[role="progressbar"]')).toHaveCount(14);
await expect(page.getByRole('tooltip').locator('[aria-label*="1,275,975 (EX+)"]')).toHaveCount(5);
expect(
await page
.getByRole('tooltip')
.locator('[role="progressbar"]')
.first()
.evaluate((bar) => getComputedStyle(bar).backgroundImage)
).toContain('/game/pr8.gif');
expect(await page.getByRole('tooltip').evaluate((tooltip) => tooltip.getBoundingClientRect().width)).toBeCloseTo(
500,
0