Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
egress-policy: audit

- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
Expand Down Expand Up @@ -52,7 +52,7 @@
egress-policy: audit

- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
Expand Down
6 changes: 6 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@
## 2026-07-30 - Add window.confirm for destructive actions
**Learning:** Destructive actions like deleting groups and edge relationships previously occurred immediately without user confirmation.
**Action:** Always wrap delete operations with window.confirm() dialogs and ensure corresponding tests successfully mock window.confirm.
## 2024-08-17 - Contextual ARIA labels for generic action buttons
**Learning:** Generic action buttons like "ν…Œμ΄λΈ” μ‚­μ œ" (Delete Table) inside modals lack sufficient context for screen reader users when multiple modals or nested actions might exist.
**Action:** Always provide an explicit `aria-label` directly on generic action buttons (e.g., `aria-label={`${editingNode.data.title} ν…Œμ΄λΈ” μ‚­μ œ`}`) to give screen readers full context about the specific entity being acted upon.
## 2026-08-17 - Ambiguous Generic Action Buttons (μ—΄κΈ°)
**Learning:** Having multiple elements with the generic "μ—΄κΈ°" (Open) label in the same interface (like "Open Editor" vs "Open Diagram") without unique accessible names causes screen reader ambiguity, and breaks accessibility test suites that search by role and name (e.g. `getByRole('button', { name: 'μ—΄κΈ°' })`). This specific issue triggered a CI failure because `App.coverage.test.tsx` couldn't uniquely identify the correct snapshot "μ—΄κΈ°" button among project "μ—΄κΈ°" buttons.
**Action:** When a screen has multiple generic action buttons (e.g., in a data table), always apply a contextual `aria-label` (e.g., `aria-label={`${item.name} λ‹€μ΄μ–΄κ·Έλž¨ μ—΄κΈ°`}`) so that assistive tech and test queries can precisely target the correct element.
24 changes: 12 additions & 12 deletions frontend/src/App.coverage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ describe('App orchestration coverage', () => {
expect(screen.getAllByText('<Billing & Core>').length).toBeGreaterThan(0)
fireEvent.click(screen.getByRole('button', { name: '전체 보기' }))
expect(screen.getByRole('heading', { name: 'ν”„λ‘œμ νŠΈ' })).toBeInTheDocument()
fireEvent.click(screen.getAllByRole('button', { name: 'μ—΄κΈ°' })[1]!)
fireEvent.click(screen.getAllByRole('button', { name: /(λ‹€μ΄μ–΄κ·Έλž¨ 보기|λ‹€μ΄μ–΄κ·Έλž¨ μ—΄κΈ°)/ })[1]!)
expect(screen.getByRole('heading', { name: 'λ‹€μ΄μ–΄κ·Έλž¨' })).toBeInTheDocument()
fireEvent.change(screen.getByLabelText('λ‹€μ΄μ–΄κ·Έλž¨ 검색'), { target: { value: 'no-match' } })
expect(screen.getByText('검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.')).toBeInTheDocument()
Expand Down Expand Up @@ -372,7 +372,7 @@ describe('App orchestration coverage', () => {
it('polls a terminal snapshot, builds graph state, and exercises editor handlers', async () => {
await renderReadyApp()
fireEvent.click(screen.getByRole('button', { name: 'λ‹€μ΄μ–΄κ·Έλž¨' }))
const openButtons = await screen.findAllByRole('button', { name: 'μ—΄κΈ°' })
const openButtons = await screen.findAllByRole('button', { name: /λ‹€μ΄μ–΄κ·Έλž¨ μ—΄κΈ°/ })
vi.useFakeTimers()
fireEvent.click(openButtons[0]!)
await act(async () => {
Expand Down Expand Up @@ -527,8 +527,8 @@ describe('App orchestration coverage', () => {
fireEvent.click(screen.getByRole('button', { name: /Billing.*λ‹€μ΄μ–΄κ·Έλž¨ 보기/ }))
expect(screen.getByRole('heading', { name: 'λ‹€μ΄μ–΄κ·Έλž¨' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'λŒ€μ‹œλ³΄λ“œ' }))
await waitFor(() => expect(screen.getAllByRole('button', { name: 'μ—΄κΈ°' }).length).toBeGreaterThan(0))
fireEvent.click(screen.getAllByRole('button', { name: 'μ—΄κΈ°' })[0]!)
await waitFor(() => expect(screen.getAllByRole('button', { name: /λ‹€μ΄μ–΄κ·Έλž¨ μ—΄κΈ°/ }).length).toBeGreaterThan(0))
fireEvent.click(screen.getAllByRole('button', { name: /λ‹€μ΄μ–΄κ·Έλž¨ μ—΄κΈ°/ })[0]!)
expect(screen.getByRole('toolbar', { name: 'ERD μΊ”λ²„μŠ€ 도ꡬ' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'λŒ€μ‹œλ³΄λ“œ' }))
fireEvent.click(screen.getByRole('button', { name: 'νŽΈμ§‘κΈ°λ‘œ 이동' }))
Expand All @@ -541,7 +541,7 @@ describe('App orchestration coverage', () => {
fireEvent.change(screen.getByLabelText('μƒˆ ν”„λ‘œμ νŠΈ 이름'), { target: { value: 'Roadmap' } })
fireEvent.click(screen.getByRole('button', { name: 'μƒˆ ν”„λ‘œμ νŠΈ' }))
await waitFor(() => expect(api.createProject).toHaveBeenCalledWith('Roadmap'))
fireEvent.click(screen.getAllByRole('button', { name: 'μ—΄κΈ°' })[0]!)
fireEvent.click(screen.getAllByRole('button', { name: /λ‹€μ΄μ–΄κ·Έλž¨ (μ—΄κΈ°|보기)/ })[0]!)
expect(screen.getByRole('heading', { name: 'λ‹€μ΄μ–΄κ·Έλž¨' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'λŒ€μ‹œλ³΄λ“œ' }))
fireEvent.click(screen.getByRole('button', { name: 'λͺ©λ‘ 보기' }))
Expand Down Expand Up @@ -611,7 +611,7 @@ describe('App orchestration coverage', () => {
await renderReadyApp()
fireEvent.click(screen.getByRole('button', { name: 'λ‹€μ΄μ–΄κ·Έλž¨' }))
vi.useFakeTimers()
fireEvent.click(screen.getAllByRole('button', { name: 'μ—΄κΈ°' })[0]!)
fireEvent.click(screen.getAllByRole('button', { name: /λ‹€μ΄μ–΄κ·Έλž¨ (μ—΄κΈ°|보기)/ })[0]!)
await act(async () => {
vi.advanceTimersByTime(1000)
await Promise.resolve()
Expand Down Expand Up @@ -642,7 +642,7 @@ describe('App orchestration coverage', () => {
await renderReadyApp()
fireEvent.click(screen.getByRole('button', { name: 'λ‹€μ΄μ–΄κ·Έλž¨' }))
vi.useFakeTimers()
fireEvent.click(screen.getAllByRole('button', { name: 'μ—΄κΈ°' })[0]!)
fireEvent.click(screen.getAllByRole('button', { name: /λ‹€μ΄μ–΄κ·Έλž¨ (μ—΄κΈ°|보기)/ })[0]!)
await act(async () => {
vi.advanceTimersByTime(1000)
await Promise.resolve()
Expand Down Expand Up @@ -717,7 +717,7 @@ describe('App orchestration coverage', () => {
/>,
)
expect(screen.getAllByText('ν˜„μž¬ ν”„λ‘œμ νŠΈ')).toHaveLength(2)
fireEvent.click(screen.getAllByRole('button', { name: 'μ—΄κΈ°' })[0]!)
fireEvent.click(screen.getAllByRole('button', { name: /λ‹€μ΄μ–΄κ·Έλž¨ (μ—΄κΈ°|보기)/ })[0]!)
expect(onOpenEditor).toHaveBeenCalledWith('s1')
})

Expand Down Expand Up @@ -745,7 +745,7 @@ describe('App orchestration coverage', () => {
await renderReadyApp()
fireEvent.click(screen.getByRole('button', { name: 'λ‹€μ΄μ–΄κ·Έλž¨' }))
vi.useFakeTimers()
fireEvent.click(screen.getAllByRole('button', { name: 'μ—΄κΈ°' })[0]!)
fireEvent.click(screen.getAllByRole('button', { name: /λ‹€μ΄μ–΄κ·Έλž¨ (μ—΄κΈ°|보기)/ })[0]!)
await act(async () => {
vi.advanceTimersByTime(1000)
await Promise.resolve()
Expand Down Expand Up @@ -785,7 +785,7 @@ describe('App orchestration coverage', () => {
await renderReadyApp()
fireEvent.click(screen.getByRole('button', { name: 'λ‹€μ΄μ–΄κ·Έλž¨' }))
vi.useFakeTimers()
fireEvent.click(screen.getAllByRole('button', { name: 'μ—΄κΈ°' })[0]!)
fireEvent.click(screen.getAllByRole('button', { name: /λ‹€μ΄μ–΄κ·Έλž¨ (μ—΄κΈ°|보기)/ })[0]!)
await act(async () => {
vi.advanceTimersByTime(1000)
await Promise.resolve()
Expand Down Expand Up @@ -823,10 +823,10 @@ describe('App orchestration coverage', () => {
api.listSnapshots.mockResolvedValue(snapshots)
await renderReadyApp()
fireEvent.click(screen.getByRole('button', { name: 'λ‹€μ΄μ–΄κ·Έλž¨' }))
await waitFor(() => expect(screen.getAllByRole('button', { name: 'μ—΄κΈ°' }).length).toBeGreaterThan(0))
await waitFor(() => expect(screen.getAllByRole('button', { name: /λ‹€μ΄μ–΄κ·Έλž¨ (μ—΄κΈ°|보기)/ }).length).toBeGreaterThan(0))
vi.useFakeTimers()
api.getSnapshot.mockRejectedValueOnce(new Error('poll down'))
fireEvent.click(screen.getAllByRole('button', { name: 'μ—΄κΈ°' })[0]!)
fireEvent.click(screen.getAllByRole('button', { name: /λ‹€μ΄μ–΄κ·Έλž¨ (μ—΄κΈ°|보기)/ })[0]!)
await act(async () => {
vi.advanceTimersByTime(1000)
await Promise.resolve()
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/App.searchPolling.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ async function renderReadyApp() {

async function diagramOpenButtons() {
fireEvent.click(screen.getByRole('button', { name: 'λ‹€μ΄μ–΄κ·Έλž¨' }))
return screen.findAllByRole('button', { name: 'μ—΄κΈ°' })
return screen.findAllByRole('button', { name: /(λ‹€μ΄μ–΄κ·Έλž¨ 보기|λ‹€μ΄μ–΄κ·Έλž¨ μ—΄κΈ°)/ })
}

async function openSnapshot(index: number) {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,7 @@ export default function App() {
setSelectedProjectId(project.project_space_uuid);
setActiveView("diagrams");
}}
aria-label={`${project.project_name} λ‹€μ΄μ–΄κ·Έλž¨ 보기`}
>
μ—΄κΈ°
</button>
Expand Down Expand Up @@ -1780,6 +1781,7 @@ export function DiagramTable({
<button
type="button"
onClick={() => onOpenEditor(item.schema_snapshot_uuid)}
aria-label={`${item.schema_snapshot_uuid} λ‹€μ΄μ–΄κ·Έλž¨ μ—΄κΈ°`}
>
μ—΄κΈ°
</button>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/modals/EditTableModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export function EditTableModal({
type="button"
onClick={onDeleteTable}
style={{ color: "#b91c1c", borderColor: "#fca5a5" }}
aria-label={`${editingNode.data.title} ν…Œμ΄λΈ” μ‚­μ œ`}
>
ν…Œμ΄λΈ” μ‚­μ œ
</button>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/modals/ModalCoverage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ describe('modal behavior coverage', () => {
expect(deleteEditing(tableNode)?.data.columns).toHaveLength(1)

fireEvent.submit(document.getElementById('editTableForm')!)
fireEvent.click(screen.getByRole('button', { name: 'ν…Œμ΄λΈ” μ‚­μ œ' }))
fireEvent.click(screen.getByRole('button', { name: 'public.users ν…Œμ΄λΈ” μ‚­μ œ' }))
fireEvent.click(screen.getByRole('button', { name: '볡제' }))
const duplicate = setNodes.mock.calls[2]?.[0] as (nodes: Node<TableNodeData>[]) => Node<TableNodeData>[]
const duplicated = duplicate([tableNode])[1]!
Expand Down
Loading