[AZ-460] [AZ-462] [AZ-466] [AZ-475] Batch 4 - destructive UX/forms/overlay/save

AZ-466 — Destructive UX policy + ConfirmDialog a11y + no-alert (4pts):
  src/components/ConfirmDialog.test.tsx (8 fast),
  tests/destructive_ux.test.tsx (4 fast, AdminPage class-delete drift),
  e2e/tests/destructive_ux.e2e.ts. New static checks STC-SEC7 (alert
  allowlist) + STC-SEC8 (destructive-surfaces gated/drift) wired through
  scripts/check-banned-deps.mjs reading tests/security/banned-deps.json.

AZ-475 — Numeric form input rejection (2pts):
  tests/form_hygiene.test.tsx (3 fast). Documents two SettingsPage drifts:
  silent zero coercion via parseInt(v)||0 and labels missing htmlFor.

AZ-462 — Overlay membership at in-window edges (2pts):
  tests/overlay_membership.test.tsx (6 fast). Documents getTimeWindowDetections
  strict < drift; AC-1 boundary tests are it.fails(); AC-2 / control PASS.
  Mocks HTMLCanvasElement.getContext to capture strokeRect.

AZ-460 — Annotation save URL + payload contract (2pts):
  tests/annotations_endpoint.test.tsx (6 fast),
  e2e/tests/annotations_endpoint.e2e.ts. AC-1 URL canary PASSes; AC-2
  payload missing 4 fields documented as it.fails(); AC-3 manual-draw
  PASS, AI-suggestion-accept + bulk-edit-save QUARANTINE skip.

Test infrastructure:
  - tests/setup.ts: NoopResizeObserver + NoopEventSource JSDOM polyfills.
  - tests/msw/handlers/annotations.ts: doubly-prefixed paths matching
    production calls (e.g. /api/annotations/annotations).
  - tests/msw/handlers/flights.ts: plural /aircrafts paths.

Verification: bun run test:fast → 80 passed, 13 skipped (14 files).
scripts/run-tests.sh --static-only → 24/24 PASS (was 22; +STC-SEC7/SEC8).
Per-batch self-review verdict: PASS_WITH_WARNINGS. Cumulative review
of batches 04-06 due after batch 6 per implement/SKILL.md Step 14.5.
Report: _docs/03_implementation/batch_04_report.md.

Also includes the previously-untracked
_docs/03_implementation/cumulative_review_batches_01-03_report.md
generated at the start of this session before batch 4 began.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 04:15:01 +03:00
parent 2051088706
commit 1dd25edee3
20 changed files with 1812 additions and 32 deletions
+179
View File
@@ -0,0 +1,179 @@
import { describe, it, expect } from 'vitest'
import { renderWithProviders, screen, fireEvent, userEvent } from '../../tests/helpers/render'
import ConfirmDialog from './ConfirmDialog'
// AZ-466 — Destructive UX policy (ConfirmDialog half)
//
// Scope of this file (per AZ-466 ACs that target the dialog itself):
// AC-3 (FT-P-28): `role="dialog"`, `aria-modal="true"`, `aria-labelledby`,
// `aria-describedby` linkage.
// AC-3 (FT-P-29): focus trap — Tab cycles inside the dialog.
// AC-2 (FT-N-08): Escape on `<ConfirmDialog>` cancels — `onCancel` is invoked.
//
// Production drift (`src/components/ConfirmDialog.tsx`):
// The dialog renders a plain `<div>` shell with NO `role="dialog"`,
// `aria-modal`, `aria-labelledby`, or `aria-describedby` linkage. AC-3
// attributes are recorded as `it.fails()`. Focus trap is absent — Tab
// does not wrap inside the dialog. AC-3 focus trap is `it.skip` QUARANTINE
// until production lands a focus trap. Escape close (FT-N-08) IS wired
// (line 22-27 of ConfirmDialog.tsx) and PASSES today.
describe('AZ-466 — ConfirmDialog (component-level a11y / Escape)', () => {
describe('AC-3 (FT-P-28) — modal a11y attributes', () => {
it.fails('exposes role="dialog" + aria-modal="true" on the container', () => {
// Arrange
const noop = () => {}
renderWithProviders(
<ConfirmDialog
open
title="Delete class?"
message="This cannot be undone."
onConfirm={noop}
onCancel={noop}
/>,
)
// Assert — the dialog's container element has the modal a11y attrs.
// Drift: production renders a plain <div> with no role / aria attrs.
const dialog = screen.getByRole('dialog')
expect(dialog).toHaveAttribute('aria-modal', 'true')
})
it.fails('links aria-labelledby and aria-describedby to title + message', () => {
const noop = () => {}
renderWithProviders(
<ConfirmDialog
open
title="Delete class?"
message="This cannot be undone."
onConfirm={noop}
onCancel={noop}
/>,
)
const dialog = screen.getByRole('dialog')
const labelId = dialog.getAttribute('aria-labelledby')
const describeId = dialog.getAttribute('aria-describedby')
expect(labelId).toBeTruthy()
expect(describeId).toBeTruthy()
// The referenced ids must point to the title and message nodes.
const titleEl = document.getElementById(labelId!)
const messageEl = document.getElementById(describeId!)
expect(titleEl).toHaveTextContent('Delete class?')
expect(messageEl).toHaveTextContent('This cannot be undone.')
})
it('control: the dialog DOM is currently a non-semantic <div> shell', () => {
// Pin the current (drift) shape so a regression that, e.g., flips the
// outer node to a <span> is caught even before AC-3 is fixed.
const noop = () => {}
const { container } = renderWithProviders(
<ConfirmDialog
open
title="Delete class?"
onConfirm={noop}
onCancel={noop}
/>,
)
const outerDiv = container.querySelector('div.fixed.inset-0')
expect(outerDiv).not.toBeNull()
expect(outerDiv?.getAttribute('role')).toBeNull()
expect(outerDiv?.getAttribute('aria-modal')).toBeNull()
})
})
describe('AC-3 (FT-P-29) — focus trap', () => {
it.skip(
'QUARANTINE — Tab from the last button cycles back to the first focusable element inside the dialog',
async () => {
// Production has no focus trap. The cancel button auto-focuses on
// open (`useEffect` on line 16-18 of ConfirmDialog.tsx) but Tab can
// escape the dialog. When a focus trap is added (typically via
// `react-focus-lock` or a manual keydown handler), this test should
// assert that Tab on the last focusable element returns focus to
// the first, and Shift+Tab on the first returns focus to the last.
},
)
})
describe('AC-2 (FT-N-08) — Escape cancel', () => {
it('invokes onCancel when Escape is pressed while the dialog is open', () => {
// Arrange
let cancelCalls = 0
let confirmCalls = 0
renderWithProviders(
<ConfirmDialog
open
title="Delete?"
onConfirm={() => { confirmCalls += 1 }}
onCancel={() => { cancelCalls += 1 }}
/>,
)
// Act — fire Escape on window (production attaches a window-level keydown listener).
fireEvent.keyDown(window, { key: 'Escape', code: 'Escape' })
// Assert
expect(cancelCalls).toBe(1)
expect(confirmCalls).toBe(0)
})
it('does NOT call onCancel when Escape is pressed while the dialog is closed', () => {
let cancelCalls = 0
renderWithProviders(
<ConfirmDialog
open={false}
title="Closed"
onConfirm={() => {}}
onCancel={() => { cancelCalls += 1 }}
/>,
)
fireEvent.keyDown(window, { key: 'Escape', code: 'Escape' })
expect(cancelCalls).toBe(0)
})
})
describe('AC-1 / AC-2 — happy + cancel paths invoked via the dialog buttons', () => {
it('clicking Confirm invokes onConfirm exactly once and not onCancel', async () => {
let confirmCalls = 0
let cancelCalls = 0
renderWithProviders(
<ConfirmDialog
open
title="Delete?"
onConfirm={() => { confirmCalls += 1 }}
onCancel={() => { cancelCalls += 1 }}
/>,
)
const confirm = screen.getAllByRole('button').find(b => /confirm/i.test(b.textContent ?? ''))
expect(confirm).toBeDefined()
await userEvent.click(confirm!)
expect(confirmCalls).toBe(1)
expect(cancelCalls).toBe(0)
})
it('clicking Cancel invokes onCancel exactly once and not onConfirm', async () => {
let confirmCalls = 0
let cancelCalls = 0
renderWithProviders(
<ConfirmDialog
open
title="Delete?"
onConfirm={() => { confirmCalls += 1 }}
onCancel={() => { cancelCalls += 1 }}
/>,
)
const cancel = screen.getAllByRole('button').find(b => /cancel/i.test(b.textContent ?? ''))
expect(cancel).toBeDefined()
await userEvent.click(cancel!)
expect(cancelCalls).toBe(1)
expect(confirmCalls).toBe(0)
})
})
})