feat(game-ui): port Ref progress bars
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user