Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d803f02
🎨 Palette: 툴팁을 가진 요소를 키보드로 접근할 수 있도록 개선
seonghobae Aug 2, 2026
dba7dc4
🎨 Palette: 툴팁을 가진 요소를 키보드로 접근할 수 있도록 개선
seonghobae Aug 2, 2026
f6cb064
test(a11y): cover keyboard-accessible summary tooltips
seonghobae Aug 3, 2026
d9014b3
test(a11y): require explicit descriptions for summary cards
seonghobae Aug 3, 2026
5b8d40b
test(a11y): enforce summary-card descriptions
seonghobae Aug 3, 2026
0ab7383
test(a11y): run summary-card contract in unit suite
seonghobae Aug 3, 2026
4b19d11
fix(a11y): expose summary-card tooltip descriptions
seonghobae Aug 3, 2026
ea1bba0
test(a11y): compare screen-reader copy literally
seonghobae Aug 3, 2026
34d2af9
chore(a11y): align test manifest with security base
seonghobae Aug 3, 2026
a68c1eb
test(a11y): bind each assertion to its own summary card
seonghobae Aug 3, 2026
75577a9
docs(a11y): record summary-card verification
seonghobae Aug 3, 2026
eba0a06
🎨 Palette: 키보드 접근성을 위해 폼 제출 버튼 비활성화 상태 표현을 aria-disabled로 변경
seonghobae Aug 3, 2026
4bc7332
test(a11y): cover focusable disabled save-button contract
seonghobae Aug 3, 2026
445c7a9
test(a11y): cover focusable aria-disabled editor save
seonghobae Aug 3, 2026
b19ffab
ci: synchronize security base and supported runtimes
seonghobae Aug 3, 2026
9048a1d
test(a11y): require fresh validation before submit guard
seonghobae Aug 3, 2026
3ae6712
test(a11y): run editor submit accessibility contract
seonghobae Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '20'
node-version: '22.13.0'
cache: 'npm'

- name: Install dependencies
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/server-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node 22
- name: Setup Node 22.13
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 22
node-version: 22.13.0
- name: Install
run: npm ci
- name: Unit tests (EVM · CPM · baseline · workload)
Expand All @@ -45,10 +45,10 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node 22
- name: Setup Node 22.13
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 22
node-version: 22.13.0
- name: Install
run: npm ci
- name: Install Playwright (chromium)
Expand Down
3 changes: 3 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,6 @@
## $(date +%Y-%m-%d) - Prevent accidental data loss in inline editors
**Learning:** Forms that take a long time to fill out (like a WBS editor) are prone to accidental closure by users pressing `Escape` or clicking cancel. This causes immediate data loss without any warning, resulting in frustration.
**Action:** When working on editors that can be dismissed, track whether the user has modified any fields compared to their initial state. If there are changes, intercept the close action and present a confirmation dialog (`window.confirm`) to ensure they really want to discard their edits. Bypass this for intentional saves or explicit data overrides.
## 2026-08-03 - Replace native disabled with aria-disabled for form submit buttons
**Learning:** Using the native `disabled` attribute on form submission buttons prevents them from receiving focus. This completely hides the button's `title` tooltip (which often explains *why* the form cannot be submitted) from keyboard-only and screen-reader users, leading to a confusing UX when validation fails.
**Action:** Use `aria-disabled="true"` instead of `disabled` for submit buttons when form validation fails. This ensures the button remains in the tab order so the `title` tooltip can be read. Since the browser no longer blocks the form submission natively, ensure the form's `submit` event listener explicitly checks `getAttribute('aria-disabled') === 'true'` and aborts if necessary.
Comment on lines +118 to +120

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

필수 오류 설명을 title만으로 제공하지 마세요.

title은 키보드 및 보조 기술에서 일관되게 노출되지 않습니다. 필수 안내는 명시적 접근성 설명으로 연결해야 합니다. (developer.mozilla.org)

  • .jules/palette.md#L118-L120: title이 스크린 리더에서 읽힌다는 설명을 제거하고, 필수 안내에는 aria-describedby 같은 명시적 연결을 사용하도록 수정하세요.
  • app.js#L1056-L1061: 오류가 있으면 저장 버튼에 aria-describedby="editor-errors"를 설정하세요. 오류가 없으면 이 속성을 제거하세요. title은 보조 안내로만 유지하세요.
  • tests/e2e/editor-save-aria-disabled.spec.js#L13-L17: editor-errors에 대한 aria-describedby 또는 접근성 설명을 검증하세요.
📍 Affects 3 files
  • .jules/palette.md#L118-L120 (this comment)
  • app.js#L1056-L1061
  • tests/e2e/editor-save-aria-disabled.spec.js#L13-L17
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.jules/palette.md around lines 118 - 120, Update .jules/palette.md lines
118-120 to remove the claim that title is reliably announced by screen readers
and require explicit accessible descriptions such as aria-describedby; in app.js
lines 1056-1061, set aria-describedby="editor-errors" on the save button when
validation errors exist and remove it when they do not, while retaining title
only as supplemental guidance; in tests/e2e/editor-save-aria-disabled.spec.js
lines 13-17, add an assertion that the save button references editor-errors or
exposes the corresponding accessible description.

12 changes: 11 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,12 @@ function bindTableEvents(renderDraftValidation, updateEditorDraftFromEvent) {
return;
}
event.preventDefault();

const saveButton = form.querySelector('button[type="submit"]');
if (saveButton && saveButton.getAttribute('aria-disabled') === 'true') {
return;
}

renderDraftValidation.flush();
saveEditor();
Comment on lines +412 to 418

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

유효성 상태를 갱신한 후 제출을 차단하세요.

입력 이벤트의 유효성 렌더링이 대기 중이면 이 코드는 이전 aria-disabled 값을 검사합니다. 유효한 초안에서 필수 값을 지운 직후 제출하면 saveEditor()가 호출될 수 있습니다.

renderDraftValidation.flush()를 먼저 호출하세요. 그 후 갱신된 aria-disabled 또는 state.editor.errors를 검사하세요. 이 순서를 검증하는 회귀 테스트도 추가하세요.

수정 예시
     event.preventDefault();
+    renderDraftValidation.flush();
 
     const saveButton = form.querySelector('button[type="submit"]');
     if (saveButton && saveButton.getAttribute('aria-disabled') === 'true') {
       return;
     }
 
-    renderDraftValidation.flush();
     saveEditor();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.js` around lines 412 - 418, Update the submit handler around
renderDraftValidation and saveEditor so renderDraftValidation.flush() runs
before checking the submit button’s aria-disabled state or state.editor.errors;
return without calling saveEditor when the refreshed validation reports the
draft is invalid. Add a regression test covering deletion of a required value
while validation rendering is pending.

});
Expand Down Expand Up @@ -1047,7 +1053,11 @@ function renderEditorValidation() {

const saveButton = form.querySelector('button[type="submit"]');
if (saveButton) {
saveButton.disabled = errors.length > 0;
if (errors.length > 0) {
saveButton.setAttribute('aria-disabled', 'true');
} else {
saveButton.removeAttribute('aria-disabled');
}
saveButton.title = errors.length > 0 ? '입력값을 올바르게 수정해야 저장할 수 있습니다.' : '저장 (Enter)';
}

Expand Down
39 changes: 34 additions & 5 deletions cloud-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -739,25 +739,54 @@ function openReportModal() {
// no DOMParser needed → node-testable); swap for a real XML parser if
// hand-edited files ever matter.
export function parseMsProjectXml(xml) {
// Fully linear extract (indexOf/slice) — no dynamic RegExp and no lazy
// [\s\S]*? block collectors (those can quadratic-backtrack on truncated input).
const tag = (block, name) => {
const m = block.match(new RegExp(`<${name}>([^<]*)</${name}>`));
return m ? m[1].trim() : '';
const openingTag = `<${name}>`;
const closingTag = `</${name}>`;
const valueStart = block.indexOf(openingTag);
if (valueStart === -1) return '';
const contentStart = valueStart + openingTag.length;
const valueEnd = block.indexOf(closingTag, contentStart);
return valueEnd === -1 ? '' : block.slice(contentStart, valueEnd).trim();
};
const collectBlocks = (source, openTag, closeTag) => {
const out = [];
let from = 0;
for (;;) {
const start = source.indexOf(openTag, from);
if (start === -1) break;
const contentStart = start + openTag.length;
const end = source.indexOf(closeTag, contentStart);
// Incomplete open tag: stop linearly (do not rescan the remainder).
if (end === -1) break;
out.push(source.slice(start, end + closeTag.length));
from = end + closeTag.length;
}
return out;
Comment on lines 744 to +766

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

유효한 XML 태그 공백을 처리하세요.

tagcollectBlocks<Task></Task>처럼 정확히 일치하는 문자열만 찾습니다. XML은 <Task >, <UID >, </Task >와 같은 공백 표현을 허용합니다. 이 형식의 MSP XML은 작업 블록 또는 필드를 찾지 못해서 가져오기에 실패합니다.

XML 구조를 처리하는 파서로 변경하거나, 시작 및 종료 태그를 XML 구문에 맞게 토큰화하세요. 이 태그 공백 변형의 회귀 테스트도 추가하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-sync.js` around lines 744 - 766, Update the tag and collectBlocks
helpers to recognize XML-valid whitespace before the closing angle bracket in
both opening and closing tags, such as Task, UID, and their corresponding end
tags, while preserving exact element-name matching and incomplete-input
handling. Add regression coverage for these whitespace variants in task blocks
and fields.

};
const predecessorIds = (block) => {
const ids = [];
for (const link of collectBlocks(block, '<PredecessorLink>', '</PredecessorLink>')) {
const uid = tag(link, 'PredecessorUID');
if (/^\d+$/.test(uid)) ids.push(`msp-${uid}`);
}
return ids;
};
const unescape = (s) => s
.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"')
.replace(/&apos;/g, "'").replace(/&amp;/g, '&');
const day = (s) => (/^\d{4}-\d{2}-\d{2}/.test(s) ? s.slice(0, 10) : '');
const tasks = [];
const parents = {}; // depth -> last task id at that depth
const blocks = xml.match(/<Task>[\s\S]*?<\/Task>/g) || [];
const blocks = collectBlocks(String(xml || ''), '<Task>', '</Task>');
for (const block of blocks) {
const uid = tag(block, 'UID');
const name = unescape(tag(block, 'Name'));
if (!uid || uid === '0' || !name) continue; // project-summary row / blanks
const level = Math.max(1, Number(tag(block, 'OutlineLevel')) || 1);
const depth = Math.min(level, 3); // deeper levels flatten to task level
const preds = [...block.matchAll(/<PredecessorLink>[\s\S]*?<PredecessorUID>(\d+)<\/PredecessorUID>[\s\S]*?<\/PredecessorLink>/g)]
.map((m) => `msp-${m[1]}`);
const preds = predecessorIds(block);
const pct = Number(tag(block, 'PercentComplete')) || 0;
const t = {
id: `msp-${uid}`,
Expand Down
Loading
Loading