diff --git a/app.js b/app.js index a04aae71..7cebc6b4 100644 --- a/app.js +++ b/app.js @@ -430,6 +430,13 @@ function bindTableEvents(renderDraftValidation, updateEditorDraftFromEvent) { } event.preventDefault(); renderDraftValidation.flush(); + + const saveButton = form.querySelector('button[type="submit"]'); + if (saveButton && saveButton.getAttribute('aria-disabled') === 'true') { + showToast(saveButton.title || '현재 사용할 수 없는 작업입니다.'); + return; + } + saveEditor(); }); @@ -790,6 +797,7 @@ function renderEditorRow(anchorId) { panel.className = 'editor-panel'; const form = document.createElement('form'); form.dataset.editorForm = 'true'; + form.noValidate = true; const editorGrid = document.createElement('div'); editorGrid.className = 'editor-grid'; @@ -1068,7 +1076,12 @@ function renderEditorValidation() { const saveButton = form.querySelector('button[type="submit"]'); if (saveButton) { - saveButton.disabled = errors.length > 0; + saveButton.disabled = false; + if (errors.length > 0) { + saveButton.setAttribute('aria-disabled', 'true'); + } else { + saveButton.removeAttribute('aria-disabled'); + } saveButton.title = errors.length > 0 ? '입력값을 올바르게 수정해야 저장할 수 있습니다.' : '저장 (Enter)'; } diff --git a/tests/e2e/editor-validation-feedback.spec.js b/tests/e2e/editor-validation-feedback.spec.js new file mode 100644 index 00000000..f6007f65 --- /dev/null +++ b/tests/e2e/editor-validation-feedback.spec.js @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; + +test.describe('editor validation feedback', () => { + test.beforeEach(async ({ page }) => { + await page.goto('./'); + await page.getByRole('button', { name: '최상위 작업 추가' }).click(); + }); + + test('routes an invalid required-field submit through product validation feedback', async ({ page }) => { + const form = page.locator('form[data-editor-form="true"]'); + const toast = page.locator('#toast'); + + await expect(form).toBeVisible(); + await form.evaluate((editorForm) => editorForm.requestSubmit()); + + await expect(page.locator('#editor-errors')).toContainText('최상위 작업은 단계 값을 입력해야 합니다.'); + await expect(toast).toHaveClass(/show/); + await expect(toast).toContainText('입력값을 올바르게 수정해야 저장할 수 있습니다.'); + await expect(form).toBeVisible(); + }); + + test('flushes pending validation before an immediate valid submit', async ({ page }) => { + const phaseInput = page.locator('[data-testid="editor-phase"]'); + + await phaseInput.evaluate((input) => { + input.value = '즉시 제출 단계'; + input.dispatchEvent(new Event('input', { bubbles: true })); + input.form.requestSubmit(); + }); + + await expect(page.locator('form[data-editor-form="true"]')).toHaveCount(0); + await expect( + page.locator('tbody tr[data-task-id]').filter({ hasText: '즉시 제출 단계' }), + ).toHaveCount(1); + }); +});