fix(game-ui): 턴 선택기 버튼 눌림 깊이를 복원
카테고리와 각 명령 버튼을 공통 Lumen 하단면과 hover, pointer-down 이동 계약에 연결합니다.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { mkdir, writeFile } from 'node:fs/promises';
|
import { mkdir, writeFile } from 'node:fs/promises';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
import { expect, test, type Locator, type Page, type Route } from '@playwright/test';
|
||||||
|
|
||||||
const response = (data: unknown) => ({ result: { data } });
|
const response = (data: unknown) => ({ result: { data } });
|
||||||
const errorResponse = (path: string, message: string) => ({
|
const errorResponse = (path: string, message: string) => ({
|
||||||
@@ -619,6 +619,97 @@ const gridColumnCount = async (page: Page, selector: string) =>
|
|||||||
.first()
|
.first()
|
||||||
.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length);
|
.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length);
|
||||||
|
|
||||||
|
const raisedButtonState = async (target: Locator) =>
|
||||||
|
target.evaluate((element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
return {
|
||||||
|
top: rect.top,
|
||||||
|
bottom: rect.bottom,
|
||||||
|
height: rect.height,
|
||||||
|
backgroundColor: style.backgroundColor,
|
||||||
|
borderTopWidth: style.borderTopWidth,
|
||||||
|
borderLeftWidth: style.borderLeftWidth,
|
||||||
|
borderBottomWidth: style.borderBottomWidth,
|
||||||
|
borderBottomColor: style.borderBottomColor,
|
||||||
|
borderRadius: style.borderRadius,
|
||||||
|
marginTop: style.marginTop,
|
||||||
|
paddingTop: style.paddingTop,
|
||||||
|
paddingBottom: style.paddingBottom,
|
||||||
|
classNames: [...element.classList],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const pointerDownButtonState = async (page: Page, target: Locator) => {
|
||||||
|
const box = await target.boundingBox();
|
||||||
|
if (!box) throw new Error('raised button is not measurable');
|
||||||
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||||
|
await page.mouse.down();
|
||||||
|
const state = await raisedButtonState(target);
|
||||||
|
await page.mouse.move(1, 1);
|
||||||
|
await page.mouse.up();
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistEnlargedRaisedButtonProbe = async (page: Page, source: Locator, name: string) => {
|
||||||
|
if (!artifactRoot) return;
|
||||||
|
const target = resolve(artifactRoot);
|
||||||
|
await mkdir(target, { recursive: true });
|
||||||
|
const probeId = `raised-button-probe-${name}`;
|
||||||
|
const hostId = `${probeId}-host`;
|
||||||
|
await source.evaluate(
|
||||||
|
(element, ids) => {
|
||||||
|
const host = document.createElement('div');
|
||||||
|
host.id = ids.hostId;
|
||||||
|
Object.assign(host.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
inset: '20px auto auto 20px',
|
||||||
|
width: '390px',
|
||||||
|
height: '170px',
|
||||||
|
padding: '10px',
|
||||||
|
background: '#000',
|
||||||
|
zIndex: '2147483647',
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
const stage = document.createElement('div');
|
||||||
|
Object.assign(stage.style, {
|
||||||
|
display: 'flow-root',
|
||||||
|
width: '90px',
|
||||||
|
transform: 'scale(4)',
|
||||||
|
transformOrigin: 'top left',
|
||||||
|
});
|
||||||
|
const probe = element.cloneNode(true) as HTMLElement;
|
||||||
|
probe.id = ids.probeId;
|
||||||
|
probe.classList.remove('active');
|
||||||
|
probe.removeAttribute('disabled');
|
||||||
|
probe.style.width = '90px';
|
||||||
|
stage.append(probe);
|
||||||
|
host.append(stage);
|
||||||
|
document.body.append(host);
|
||||||
|
},
|
||||||
|
{ hostId, probeId }
|
||||||
|
);
|
||||||
|
|
||||||
|
const host = page.locator(`#${hostId}`);
|
||||||
|
const probe = page.locator(`#${probeId}`);
|
||||||
|
const states: Record<string, Awaited<ReturnType<typeof raisedButtonState>>> = {};
|
||||||
|
states.default = await raisedButtonState(probe);
|
||||||
|
await host.screenshot({ path: resolve(target, `${name}-large-default.png`) });
|
||||||
|
await probe.hover();
|
||||||
|
states.hover = await raisedButtonState(probe);
|
||||||
|
await host.screenshot({ path: resolve(target, `${name}-large-hover.png`) });
|
||||||
|
const box = await probe.boundingBox();
|
||||||
|
if (!box) throw new Error('enlarged raised button is not measurable');
|
||||||
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||||
|
await page.mouse.down();
|
||||||
|
states.pointerDown = await raisedButtonState(probe);
|
||||||
|
await host.screenshot({ path: resolve(target, `${name}-large-pointer-down.png`) });
|
||||||
|
await page.mouse.move(1, 1);
|
||||||
|
await page.mouse.up();
|
||||||
|
await writeFile(resolve(target, `${name}-large-states.json`), `${JSON.stringify(states, null, 2)}\n`);
|
||||||
|
await host.evaluate((element) => element.remove());
|
||||||
|
};
|
||||||
|
|
||||||
const persistArtifact = async (page: Page, name: string) => {
|
const persistArtifact = async (page: Page, name: string) => {
|
||||||
if (!artifactRoot) return;
|
if (!artifactRoot) return;
|
||||||
const target = resolve(artifactRoot);
|
const target = resolve(artifactRoot);
|
||||||
@@ -1033,7 +1124,7 @@ test('pure NPC message senders are not rendered as reply targets', async ({ page
|
|||||||
await persistArtifact(page, `${basePath.slice(1)}-npc-reply-targets-desktop-1200`);
|
await persistArtifact(page, `${basePath.slice(1)}-npc-reply-targets-desktop-1200`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('main reserved-turn picker renders the Ref general category order', async ({ page }) => {
|
test('main reserved-turn picker renders the Ref category order and raised button depth', async ({ page }) => {
|
||||||
const state: NavigationFixture = {
|
const state: NavigationFixture = {
|
||||||
officerLevel: 1,
|
officerLevel: 1,
|
||||||
permission: 0,
|
permission: 0,
|
||||||
@@ -1081,16 +1172,68 @@ test('main reserved-turn picker renders the Ref general category order', async (
|
|||||||
expect(desktopGeometry.columns.split(' ')).toHaveLength(3);
|
expect(desktopGeometry.columns.split(' ')).toHaveLength(3);
|
||||||
expect(desktopGeometry.rows).toBe(2);
|
expect(desktopGeometry.rows).toBe(2);
|
||||||
expect(desktopGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
|
expect(desktopGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
|
||||||
expect(desktopGeometry.categoryButton).toEqual({ height: 32, paddingTop: '6px', paddingBottom: '6px' });
|
expect(desktopGeometry.categoryButton).toEqual({ height: 35.5, paddingTop: '5.25px', paddingBottom: '5.25px' });
|
||||||
expect(desktopGeometry.commandButton).toEqual(desktopGeometry.categoryButton);
|
expect(desktopGeometry.commandButton).toEqual(desktopGeometry.categoryButton);
|
||||||
|
|
||||||
const strategyCategory = picker.getByRole('button', { name: '계략', exact: true });
|
const strategyCategory = picker.getByRole('button', { name: '계략', exact: true });
|
||||||
|
await page.mouse.move(1, 1);
|
||||||
|
const categoryDefault = await raisedButtonState(strategyCategory);
|
||||||
|
expect(categoryDefault).toMatchObject({
|
||||||
|
height: 35.5,
|
||||||
|
backgroundColor: 'rgb(23, 61, 39)',
|
||||||
|
borderTopWidth: '0px',
|
||||||
|
borderLeftWidth: '1px',
|
||||||
|
borderBottomWidth: '4px',
|
||||||
|
borderBottomColor: 'rgb(21, 55, 35)',
|
||||||
|
borderRadius: '5.25px',
|
||||||
|
marginTop: '0px',
|
||||||
|
classNames: expect.arrayContaining(['legacy-button', 'legacy-button--lumen']),
|
||||||
|
});
|
||||||
await strategyCategory.hover();
|
await strategyCategory.hover();
|
||||||
|
const categoryHover = await raisedButtonState(strategyCategory);
|
||||||
|
expect(categoryHover).toMatchObject({ height: 34.5, borderBottomWidth: '3px', marginTop: '1px' });
|
||||||
|
expect(categoryHover.top).toBe(categoryDefault.top + 1);
|
||||||
|
expect(categoryHover.bottom).toBe(categoryDefault.bottom);
|
||||||
|
const categoryPointerDown = await pointerDownButtonState(page, strategyCategory);
|
||||||
|
expect(categoryPointerDown).toMatchObject({ height: 33.5, borderBottomWidth: '2px', marginTop: '2px' });
|
||||||
|
expect(categoryPointerDown.top).toBe(categoryDefault.top + 2);
|
||||||
|
expect(categoryPointerDown.bottom).toBe(categoryDefault.bottom);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
await strategyCategory.focus();
|
await strategyCategory.focus();
|
||||||
await expect(strategyCategory).toBeFocused();
|
await expect(strategyCategory).toBeFocused();
|
||||||
|
await expect.poll(() => strategyCategory.evaluate((element) => element.matches(':focus-visible'))).toBe(true);
|
||||||
await strategyCategory.click();
|
await strategyCategory.click();
|
||||||
await expect(strategyCategory).toHaveClass(/active/);
|
await expect(strategyCategory).toHaveClass(/active/);
|
||||||
await expect(picker.locator('.command-item')).toHaveText(['화계']);
|
await expect(picker.locator('.command-item')).toHaveText(['화계']);
|
||||||
|
await page.mouse.move(1, 1);
|
||||||
|
const commandButton = picker.locator('.command-item').first();
|
||||||
|
const commandDefault = await raisedButtonState(commandButton);
|
||||||
|
expect(commandDefault).toMatchObject({
|
||||||
|
height: 35.5,
|
||||||
|
backgroundColor: 'rgb(48, 32, 22)',
|
||||||
|
borderTopWidth: '0px',
|
||||||
|
borderLeftWidth: '1px',
|
||||||
|
borderBottomWidth: '4px',
|
||||||
|
borderBottomColor: 'rgb(43, 29, 20)',
|
||||||
|
borderRadius: '5.25px',
|
||||||
|
marginTop: '0px',
|
||||||
|
classNames: expect.arrayContaining(['legacy-button', 'legacy-button--lumen']),
|
||||||
|
});
|
||||||
|
await commandButton.hover();
|
||||||
|
const commandHover = await raisedButtonState(commandButton);
|
||||||
|
expect(commandHover).toMatchObject({ height: 34.5, borderBottomWidth: '3px', marginTop: '1px' });
|
||||||
|
expect(commandHover.top).toBe(commandDefault.top + 1);
|
||||||
|
expect(commandHover.bottom).toBe(commandDefault.bottom);
|
||||||
|
const commandPointerDown = await pointerDownButtonState(page, commandButton);
|
||||||
|
expect(commandPointerDown).toMatchObject({ height: 33.5, borderBottomWidth: '2px', marginTop: '2px' });
|
||||||
|
expect(commandPointerDown.top).toBe(commandDefault.top + 2);
|
||||||
|
expect(commandPointerDown.bottom).toBe(commandDefault.bottom);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await commandButton.focus();
|
||||||
|
await expect(commandButton).toBeFocused();
|
||||||
|
await expect.poll(() => commandButton.evaluate((element) => element.matches(':focus-visible'))).toBe(true);
|
||||||
|
await persistEnlargedRaisedButtonProbe(page, strategyCategory, 'turn-selector-category');
|
||||||
|
await persistEnlargedRaisedButtonProbe(page, commandButton, 'turn-selector-command');
|
||||||
await persistArtifact(page, `${basePath.slice(1)}-main-reserved-ref-categories-desktop-1200`);
|
await persistArtifact(page, `${basePath.slice(1)}-main-reserved-ref-categories-desktop-1200`);
|
||||||
|
|
||||||
await page.setViewportSize({ width: 500, height: 900 });
|
await page.setViewportSize({ width: 500, height: 900 });
|
||||||
@@ -1118,7 +1261,11 @@ test('main reserved-turn picker renders the Ref general category order', async (
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
expect(mobileGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
|
expect(mobileGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
|
||||||
expect(mobileGeometry.categoryButton).toEqual({ height: 32, paddingTop: '6px', paddingBottom: '6px' });
|
expect(mobileGeometry.categoryButton).toEqual({
|
||||||
|
height: 35.5,
|
||||||
|
paddingTop: '5.25px',
|
||||||
|
paddingBottom: '5.25px',
|
||||||
|
});
|
||||||
expect(mobileGeometry.commandButton).toEqual(mobileGeometry.categoryButton);
|
expect(mobileGeometry.commandButton).toEqual(mobileGeometry.categoryButton);
|
||||||
await persistArtifact(page, `${basePath.slice(1)}-main-reserved-ref-categories-mobile-500`);
|
await persistArtifact(page, `${basePath.slice(1)}-main-reserved-ref-categories-mobile-500`);
|
||||||
});
|
});
|
||||||
@@ -1327,6 +1474,9 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn
|
|||||||
await page.locator('[data-main-target="commands"] .select-command').click();
|
await page.locator('[data-main-target="commands"] .select-command').click();
|
||||||
const picker = page.getByTestId('command-picker');
|
const picker = page.getByTestId('command-picker');
|
||||||
await expect(picker).toBeVisible();
|
await expect(picker).toBeVisible();
|
||||||
|
// The trigger can end up directly above a newly opened category button.
|
||||||
|
// Measure the default grid after leaving the intentional Lumen hover state.
|
||||||
|
await page.mouse.move(1, 1);
|
||||||
const pickerGeometry = await picker.evaluate((element) => {
|
const pickerGeometry = await picker.evaluate((element) => {
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
const editor = element.closest('.reserved-command-editor')?.getBoundingClientRect();
|
const editor = element.closest('.reserved-command-editor')?.getBoundingClientRect();
|
||||||
|
|||||||
@@ -130,7 +130,12 @@ const commandTitle = (command: CommandAvailability) =>
|
|||||||
<button
|
<button
|
||||||
v-for="category in categories"
|
v-for="category in categories"
|
||||||
:key="category.id"
|
:key="category.id"
|
||||||
:class="['category-btn', { active: selectedCategory === category.id }]"
|
:class="[
|
||||||
|
'category-btn',
|
||||||
|
'legacy-button',
|
||||||
|
'legacy-button--lumen',
|
||||||
|
{ active: selectedCategory === category.id },
|
||||||
|
]"
|
||||||
@click="selectedCategory = category.id"
|
@click="selectedCategory = category.id"
|
||||||
>
|
>
|
||||||
{{ category.label }}
|
{{ category.label }}
|
||||||
@@ -143,6 +148,8 @@ const commandTitle = (command: CommandAvailability) =>
|
|||||||
:key="command.key"
|
:key="command.key"
|
||||||
:class="[
|
:class="[
|
||||||
'command-item',
|
'command-item',
|
||||||
|
'legacy-button',
|
||||||
|
'legacy-button--lumen',
|
||||||
command.status === 'available' ? 'ok' : '',
|
command.status === 'available' ? 'ok' : '',
|
||||||
command.status === 'blocked' ? 'blocked' : '',
|
command.status === 'blocked' ? 'blocked' : '',
|
||||||
command.status === 'blocked' && props.allowBlocked ? 'reservable' : '',
|
command.status === 'blocked' && props.allowBlocked ? 'reservable' : '',
|
||||||
@@ -173,26 +180,19 @@ const commandTitle = (command: CommandAvailability) =>
|
|||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-btn,
|
.category-btn.legacy-button--lumen {
|
||||||
.command-item {
|
--legacy-button-bg: #173d27;
|
||||||
min-height: 32px;
|
--legacy-button-border: #153723;
|
||||||
padding-block: 6px;
|
--legacy-button-color: #fff;
|
||||||
}
|
min-height: 0;
|
||||||
|
|
||||||
.category-btn {
|
|
||||||
border: 0;
|
|
||||||
border-right: 1px solid #666;
|
|
||||||
border-bottom: 1px solid #666;
|
|
||||||
padding-inline: 4px;
|
padding-inline: 4px;
|
||||||
background: #173d27;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-btn.active {
|
.category-btn.legacy-button--lumen.active {
|
||||||
background: #28633f;
|
--legacy-button-bg: #28633f;
|
||||||
color: #ffe38a;
|
--legacy-button-border: #245939;
|
||||||
|
--legacy-button-color: #ffe38a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-grid {
|
.command-grid {
|
||||||
@@ -201,19 +201,17 @@ const commandTitle = (command: CommandAvailability) =>
|
|||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-item {
|
.command-item.legacy-button--lumen {
|
||||||
border: 0;
|
--legacy-button-bg: #302016 var(--sammo-texture-walnut);
|
||||||
border-right: 1px solid #666;
|
--legacy-button-border: #2b1d14;
|
||||||
border-bottom: 1px solid #666;
|
--legacy-button-color: #fff;
|
||||||
|
min-height: 0;
|
||||||
padding-inline: 5px;
|
padding-inline: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #302016 var(--sammo-texture-walnut);
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-item.ok {
|
.command-item.ok {
|
||||||
|
|||||||
Reference in New Issue
Block a user