merge: NPC 정책 권한 버튼 재검사를 main에 반영한다
This commit is contained in:
@@ -375,19 +375,52 @@ test('physical mobile touch reorders NPC priority across active and inactive lis
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a read-level user sees enabled legacy controls but a forbidden save retains the draft', async ({ page }) => {
|
test('a read-level user may edit drafts but cannot reset, revert, or submit them', async ({ page }) => {
|
||||||
const state: FixtureState = { permissionLevel: 1, failNextMutation: true, mutations: [] };
|
const state: FixtureState = { permissionLevel: 1, mutations: [] };
|
||||||
await installFixture(page, state);
|
await installFixture(page, state);
|
||||||
await gotoPolicy(page);
|
await gotoPolicy(page);
|
||||||
|
|
||||||
const input = page.getByLabel('국가 권장 금');
|
const input = page.getByLabel('국가 권장 금');
|
||||||
await expect(input).toBeEnabled();
|
await expect(input).toBeEnabled();
|
||||||
await input.fill('23456');
|
await input.fill('23456');
|
||||||
page.once('dialog', (dialog) => dialog.accept());
|
|
||||||
await page.locator('#container > .control_bar').getByRole('button', { name: '설정' }).click();
|
const nationPanel = page.locator('.priority-panel').first();
|
||||||
await expect(page.getByRole('alert')).toContainText('권한이 부족합니다.');
|
const activePriority = nationPanel.locator('.priority-column').nth(1).getByText('불가침제의');
|
||||||
|
const inactivePriorityList = nationPanel.locator('.priority-column').first().locator('.priority-list');
|
||||||
|
await activePriority.dragTo(inactivePriorityList);
|
||||||
|
await expect(inactivePriorityList.getByText('불가침제의')).toBeVisible();
|
||||||
|
|
||||||
|
const actionButtons = page.locator('.control_bar button');
|
||||||
|
await expect(actionButtons).toHaveCount(9);
|
||||||
|
for (const button of await actionButtons.all()) {
|
||||||
|
await expect(button).toBeDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabledStyle = await actionButtons.first().evaluate((element) => {
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
return { cursor: style.cursor, opacity: style.opacity, filter: style.filter };
|
||||||
|
});
|
||||||
|
expect(disabledStyle).toEqual({ cursor: 'not-allowed', opacity: '0.55', filter: 'none' });
|
||||||
|
|
||||||
|
// The handler guard is independent of the disabled DOM attribute. This also
|
||||||
|
// protects callers that dispatch a click after permission data has changed.
|
||||||
|
for (const button of await page.locator('#container > .control_bar button').all()) {
|
||||||
|
await button.evaluate((element) => {
|
||||||
|
element.removeAttribute('disabled');
|
||||||
|
(element as HTMLButtonElement).click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const button of await nationPanel.locator('.control_bar button').all()) {
|
||||||
|
await button.evaluate((element) => {
|
||||||
|
element.removeAttribute('disabled');
|
||||||
|
(element as HTMLButtonElement).click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await expect(input).toHaveValue('23456');
|
await expect(input).toHaveValue('23456');
|
||||||
expect(state.mutations).toEqual(['npc.setNationPolicy']);
|
await expect(inactivePriorityList.getByText('불가침제의')).toBeVisible();
|
||||||
|
expect(state.mutations).toEqual([]);
|
||||||
|
await screenshot(page, 'core-npc-policy-read-only-controls.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a user below secret read permission receives a recoverable page error', async ({ page }) => {
|
test('a user below secret read permission receives a recoverable page error', async ({ page }) => {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const generalPriority = ref<PriorityListState | null>(null);
|
|||||||
const lastSavedNationPriority = ref<string[]>([]);
|
const lastSavedNationPriority = ref<string[]>([]);
|
||||||
const lastSavedGeneralPriority = ref<string[]>([]);
|
const lastSavedGeneralPriority = ref<string[]>([]);
|
||||||
const { success: showSuccessToast, error: showErrorToast, info: showInfoToast } = useGameFeedback();
|
const { success: showSuccessToast, error: showErrorToast, info: showInfoToast } = useGameFeedback();
|
||||||
|
const canManagePolicy = computed(() => (data.value?.permissionLevel ?? -1) >= 3);
|
||||||
|
|
||||||
const resolveErrorMessage = (value: unknown): string => {
|
const resolveErrorMessage = (value: unknown): string => {
|
||||||
if (value instanceof Error) return value.message;
|
if (value instanceof Error) return value.message;
|
||||||
@@ -282,19 +283,19 @@ const priorityPanels = computed<PriorityPanel[]>(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resetPolicy = () => {
|
const resetPolicy = () => {
|
||||||
if (!data.value || !window.confirm('초기 설정으로 되돌릴까요?')) return;
|
if (!canManagePolicy.value || !data.value || !window.confirm('초기 설정으로 되돌릴까요?')) return;
|
||||||
policyDraft.value = clonePolicy(data.value.defaultNationPolicy);
|
policyDraft.value = clonePolicy(data.value.defaultNationPolicy);
|
||||||
showInfoToast('서버 초깃값을 적용했습니다. 설정 버튼을 누르면 반영됩니다.');
|
showInfoToast('서버 초깃값을 적용했습니다. 설정 버튼을 누르면 반영됩니다.');
|
||||||
};
|
};
|
||||||
|
|
||||||
const rollbackPolicy = () => {
|
const rollbackPolicy = () => {
|
||||||
if (!lastSavedPolicy.value || !window.confirm('이전 설정으로 되돌릴까요?')) return;
|
if (!canManagePolicy.value || !lastSavedPolicy.value || !window.confirm('이전 설정으로 되돌릴까요?')) return;
|
||||||
policyDraft.value = clonePolicy(lastSavedPolicy.value);
|
policyDraft.value = clonePolicy(lastSavedPolicy.value);
|
||||||
showInfoToast('이전 설정으로 되돌렸습니다.');
|
showInfoToast('이전 설정으로 되돌렸습니다.');
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitPolicy = async () => {
|
const submitPolicy = async () => {
|
||||||
if (!policyDraft.value || !window.confirm('저장할까요?')) return;
|
if (!canManagePolicy.value || !policyDraft.value || !window.confirm('저장할까요?')) return;
|
||||||
try {
|
try {
|
||||||
await trpc.npc.setNationPolicy.mutate(policyDraft.value);
|
await trpc.npc.setNationPolicy.mutate(policyDraft.value);
|
||||||
lastSavedPolicy.value = clonePolicy(policyDraft.value);
|
lastSavedPolicy.value = clonePolicy(policyDraft.value);
|
||||||
@@ -305,7 +306,7 @@ const submitPolicy = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetPriority = (section: PrioritySectionKey) => {
|
const resetPriority = (section: PrioritySectionKey) => {
|
||||||
if (!data.value || !window.confirm('초기 설정으로 되돌릴까요?')) return;
|
if (!canManagePolicy.value || !data.value || !window.confirm('초기 설정으로 되돌릴까요?')) return;
|
||||||
if (section === 'nation') {
|
if (section === 'nation') {
|
||||||
nationPriority.value = assignPriorityState(
|
nationPriority.value = assignPriorityState(
|
||||||
data.value.defaultNationPriority,
|
data.value.defaultNationPriority,
|
||||||
@@ -321,7 +322,7 @@ const resetPriority = (section: PrioritySectionKey) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rollbackPriority = (section: PrioritySectionKey) => {
|
const rollbackPriority = (section: PrioritySectionKey) => {
|
||||||
if (!data.value || !window.confirm('이전 설정으로 되돌릴까요?')) return;
|
if (!canManagePolicy.value || !data.value || !window.confirm('이전 설정으로 되돌릴까요?')) return;
|
||||||
if (section === 'nation') {
|
if (section === 'nation') {
|
||||||
nationPriority.value = assignPriorityState(
|
nationPriority.value = assignPriorityState(
|
||||||
lastSavedNationPriority.value,
|
lastSavedNationPriority.value,
|
||||||
@@ -338,7 +339,7 @@ const rollbackPriority = (section: PrioritySectionKey) => {
|
|||||||
|
|
||||||
const submitPriority = async (section: PrioritySectionKey) => {
|
const submitPriority = async (section: PrioritySectionKey) => {
|
||||||
const state = section === 'nation' ? nationPriority.value : generalPriority.value;
|
const state = section === 'nation' ? nationPriority.value : generalPriority.value;
|
||||||
if (!state || !window.confirm('저장할까요?')) return;
|
if (!canManagePolicy.value || !state || !window.confirm('저장할까요?')) return;
|
||||||
try {
|
try {
|
||||||
if (section === 'nation') {
|
if (section === 'nation') {
|
||||||
await trpc.npc.setNationPriority.mutate(state.active);
|
await trpc.npc.setNationPriority.mutate(state.active);
|
||||||
@@ -416,10 +417,16 @@ const submitPriority = async (section: PrioritySectionKey) => {
|
|||||||
|
|
||||||
<div class="control_bar">
|
<div class="control_bar">
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button class="reset_btn" type="button" @click="resetPolicy">초깃값으로</button>
|
<button class="reset_btn" type="button" :disabled="!canManagePolicy" @click="resetPolicy">
|
||||||
<button class="revert_btn" type="button" @click="rollbackPolicy">이전값으로</button>
|
초깃값으로
|
||||||
|
</button>
|
||||||
|
<button class="revert_btn" type="button" :disabled="!canManagePolicy" @click="rollbackPolicy">
|
||||||
|
이전값으로
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="submit_btn" type="button" @click="submitPolicy">설정</button>
|
<button class="submit_btn" type="button" :disabled="!canManagePolicy" @click="submitPolicy">
|
||||||
|
설정
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="priority-sections">
|
<div class="priority-sections">
|
||||||
@@ -498,14 +505,31 @@ const submitPriority = async (section: PrioritySectionKey) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="control_bar priority-control">
|
<div class="control_bar priority-control">
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button class="reset_btn" type="button" @click="resetPriority(panel.key)">
|
<button
|
||||||
|
class="reset_btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="!canManagePolicy"
|
||||||
|
@click="resetPriority(panel.key)"
|
||||||
|
>
|
||||||
초깃값으로
|
초깃값으로
|
||||||
</button>
|
</button>
|
||||||
<button class="revert_btn" type="button" @click="rollbackPriority(panel.key)">
|
<button
|
||||||
|
class="revert_btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="!canManagePolicy"
|
||||||
|
@click="rollbackPriority(panel.key)"
|
||||||
|
>
|
||||||
이전값으로
|
이전값으로
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="submit_btn" type="button" @click="submitPriority(panel.key)">설정</button>
|
<button
|
||||||
|
class="submit_btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="!canManagePolicy"
|
||||||
|
@click="submitPriority(panel.key)"
|
||||||
|
>
|
||||||
|
설정
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -728,10 +752,16 @@ const submitPriority = async (section: PrioritySectionKey) => {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control_bar button:hover {
|
.control_bar button:not(:disabled):hover {
|
||||||
filter: brightness(1.15);
|
filter: brightness(1.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control_bar button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.55;
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
.control_bar button:focus-visible,
|
.control_bar button:focus-visible,
|
||||||
.help-button:focus-visible {
|
.help-button:focus-visible {
|
||||||
outline: 2px solid #fff;
|
outline: 2px solid #fff;
|
||||||
|
|||||||
Reference in New Issue
Block a user