mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 11:01:11 +00:00
70fb452805
Replace the broken `GET /api/admin/auth/refresh` (no `credentials:'include'`) mount-time bootstrap with `POST /api/admin/auth/refresh` (with credentials) chained to `GET /api/admin/users/me`. Returning users with a valid HttpOnly refresh cookie no longer flash through `/login`. Closes Finding B3 / Vision P3. - Add module-scoped `bootstrapInflight` guard (StrictMode double-mount safety) + test-only reset hook exported via the `src/auth` barrel; `tests/setup.ts` resets it in `afterEach` to prevent pending-promise leakage between tests. - Defensive `hasPermission` against legacy `/users/me` payloads omitting `permissions`; default MSW handler now seeds `permissions` explicitly. - Add `endpoints.admin.usersMe()` builder (STC-ARCH-02 forbids the literal). - Bulk-swap 15 test files from `http.get` -> `http.post` for the refresh override so intentional bootstrap-fail tests still fail correctly. - Update auth component description; mark B3 closed. - Code review verdict PASS; static + fast suites green (231 / 13 skipped). Batch report: _docs/03_implementation/batch_13_cycle3_report.md Co-authored-by: Cursor <cursoragent@cursor.com>
581 lines
20 KiB
TypeScript
581 lines
20 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
import { http, HttpResponse } from 'msw'
|
|
import { server } from './msw/server'
|
|
import { jsonResponse } from './msw/helpers'
|
|
import {
|
|
renderWithProviders,
|
|
screen,
|
|
fireEvent,
|
|
waitFor,
|
|
} from './helpers/render'
|
|
import { seedBearer, clearBearer } from './helpers/auth'
|
|
import { FlightProvider } from '../src/components'
|
|
import { DatasetPage } from '../src/features/dataset'
|
|
import {
|
|
AnnotationSource,
|
|
AnnotationStatus,
|
|
Affiliation,
|
|
CombatReadiness,
|
|
MediaType,
|
|
} from '../src/types'
|
|
import type { AnnotationListItem, DatasetItem } from '../src/types'
|
|
|
|
// AZ-474 — tile-split + YOLO parser + auto-zoom + indicator + malformed.
|
|
//
|
|
// Production reality: the split UI is QUARANTINED today (per
|
|
// `_docs/04_refactoring/01-testability-refactoring/deferred_to_refactor.md`
|
|
// row D11; traceability matrix marks AC-39 / FT-P-51 [Q]).
|
|
//
|
|
// - There is no Split-tile button on `<DatasetPage>` and no
|
|
// `POST /api/annotations/dataset/{id}/split` callsite anywhere in
|
|
// `src/`.
|
|
// - There is no YOLO label parser module and no `<TileViewer>` /
|
|
// auto-zoom viewport / tile-zoom indicator.
|
|
// - `DatasetItem.isSplit: boolean` is on the type and surfaces from
|
|
// `GET /api/annotations/dataset`, but `DatasetPage` does not read it
|
|
// (it reads `isSeed` for the red-ring affordance instead).
|
|
//
|
|
// Every AC is therefore a documented drift today: the AC tests use
|
|
// `it.fails()` (and one `test.fail()` for the e2e) and each is paired
|
|
// with a control PASS that pins the *current* behaviour, so a regression
|
|
// that *removes* the placeholder (e.g., `DatasetPage` starts crashing on
|
|
// `isSplit: true`) is caught immediately, and the contract tests flip
|
|
// green automatically once the split surface lands in Phase B.
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared MSW seeds: a happy-path split annotation, a malformed-label one,
|
|
// and a non-split. The dataset-list shape mirrors `DatasetItem` exactly.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const happySplitAnnotation: AnnotationListItem = {
|
|
id: 'ann-split-happy',
|
|
mediaId: 'media-tile',
|
|
time: null,
|
|
createdDate: '2026-05-11T10:00:00Z',
|
|
userId: 'user-alice',
|
|
source: AnnotationSource.AI,
|
|
status: AnnotationStatus.Edited,
|
|
isSplit: true,
|
|
splitTile: '3 0.5 0.5 0.2 0.2',
|
|
detections: [
|
|
{
|
|
id: 'det-tile-1',
|
|
classNum: 3,
|
|
label: 'class-3',
|
|
confidence: 0.91,
|
|
affiliation: Affiliation.Hostile,
|
|
combatReadiness: CombatReadiness.Ready,
|
|
centerX: 0.5,
|
|
centerY: 0.5,
|
|
width: 0.2,
|
|
height: 0.2,
|
|
},
|
|
],
|
|
}
|
|
|
|
const malformedSplitAnnotation: AnnotationListItem = {
|
|
id: 'ann-split-malformed',
|
|
mediaId: 'media-tile',
|
|
time: null,
|
|
createdDate: '2026-05-11T10:01:00Z',
|
|
userId: 'user-alice',
|
|
source: AnnotationSource.AI,
|
|
status: AnnotationStatus.Edited,
|
|
isSplit: true,
|
|
splitTile: 'garbage',
|
|
detections: [],
|
|
}
|
|
|
|
const nonSplitAnnotation: AnnotationListItem = {
|
|
id: 'ann-not-split',
|
|
mediaId: 'media-tile',
|
|
time: null,
|
|
createdDate: '2026-05-11T10:02:00Z',
|
|
userId: 'user-alice',
|
|
source: AnnotationSource.Manual,
|
|
status: AnnotationStatus.Created,
|
|
isSplit: false,
|
|
splitTile: null,
|
|
detections: [],
|
|
}
|
|
|
|
function datasetRowFromAnnotation(a: AnnotationListItem): DatasetItem {
|
|
return {
|
|
annotationId: a.id,
|
|
imageName: `image-${a.mediaId}-${a.id}.jpg`,
|
|
thumbnailPath: `/thumbs/${a.mediaId}.jpg`,
|
|
status: a.status,
|
|
createdDate: a.createdDate,
|
|
createdEmail: 'op_alice@test.local',
|
|
flightName: 'Flight 1',
|
|
source: a.source,
|
|
isSeed: false,
|
|
isSplit: a.isSplit,
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
seedBearer()
|
|
server.use(
|
|
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
|
// FlightProvider mounts a user-settings fetch when authenticated. The
|
|
// dataset surface does not depend on it; we satisfy MSW's unhandled-
|
|
// request gate with a 404 so the noise does not pollute the report.
|
|
http.get('/api/annotations/settings/user', () => new Response(null, { status: 404 })),
|
|
http.get('/api/annotations/dataset', () =>
|
|
jsonResponse({
|
|
items: [
|
|
datasetRowFromAnnotation(happySplitAnnotation),
|
|
datasetRowFromAnnotation(malformedSplitAnnotation),
|
|
datasetRowFromAnnotation(nonSplitAnnotation),
|
|
],
|
|
totalCount: 3,
|
|
}),
|
|
),
|
|
)
|
|
})
|
|
|
|
afterEach(() => {
|
|
clearBearer()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AC-1 — tile-split endpoint contract (FT-P-51, [Q]).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('AZ-474 — AC-1 (FT-P-51 [Q]): tile-split endpoint contract', () => {
|
|
it.fails(
|
|
'splitting a tile sends POST /api/annotations/dataset/<id>/split',
|
|
async () => {
|
|
// Arrange — capture POSTs to the split endpoint. Production has no
|
|
// such callsite today, so this MSW handler will never fire and the
|
|
// assertion fails. Once the SPA wires a "Split tile" affordance,
|
|
// this test flips green.
|
|
const splitPosts: { url: string; body: unknown }[] = []
|
|
server.use(
|
|
http.post('/api/annotations/dataset/:id/split', async ({ request, params }) => {
|
|
splitPosts.push({ url: request.url, body: await request.json().catch(() => null) })
|
|
return jsonResponse({ id: params.id, ok: true }, { status: 200 })
|
|
}),
|
|
)
|
|
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
|
|
// Act — wait for the dataset to render, then look for the Split-tile
|
|
// affordance. The locator is intentionally generic (any button or
|
|
// role with "split" in its accessible name) so the test passes for
|
|
// any reasonable implementation choice in Phase B.
|
|
await waitFor(
|
|
() => expect(screen.getAllByRole('img').length).toBeGreaterThan(0),
|
|
{ timeout: 3000 },
|
|
)
|
|
const splitBtn = await screen.findByRole(
|
|
'button',
|
|
{ name: /split/i },
|
|
{ timeout: 1000 },
|
|
)
|
|
fireEvent.click(splitBtn)
|
|
|
|
// Assert — exactly one POST to /api/annotations/dataset/<id>/split.
|
|
await waitFor(() => expect(splitPosts.length).toBe(1), { timeout: 1000 })
|
|
expect(splitPosts[0].url).toMatch(
|
|
/\/api\/annotations\/dataset\/[^/]+\/split$/,
|
|
)
|
|
},
|
|
)
|
|
|
|
it('control: today no Split-tile affordance is rendered (drift snapshot)', async () => {
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
await waitFor(
|
|
() => expect(screen.getAllByRole('img').length).toBeGreaterThan(0),
|
|
{ timeout: 3000 },
|
|
)
|
|
|
|
expect(screen.queryByRole('button', { name: /split/i })).toBeNull()
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AC-2 — YOLO label parser happy path (FT-P-52).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('AZ-474 — AC-2 (FT-P-52): YOLO label parser happy path', () => {
|
|
it.fails(
|
|
'a parser module parses "3 0.5 0.5 0.2 0.2" into the canonical 5-tuple',
|
|
async () => {
|
|
// Black-box discipline note: the spec's "parser module" does not
|
|
// exist yet. The right way to test this once it ships is via the
|
|
// public surface (rendered tile rect, downstream save body, etc.),
|
|
// not via a direct import of the parser. For now the test fails
|
|
// because there IS no public surface that consumes splitTile.
|
|
//
|
|
// Production behaviour today: <DatasetPage> double-click loads
|
|
// /api/annotations/dataset/<id> (the full AnnotationListItem with
|
|
// splitTile) but the editor never reads splitTile. So the parser
|
|
// is not exercised by ANY user-visible action, and there is no
|
|
// observable to assert against.
|
|
//
|
|
// We render the full DatasetPage, double-click the happy-split
|
|
// annotation row, and look for the parsed tile rect being applied
|
|
// to a TileViewer. Today no TileViewer mounts and no rect is
|
|
// produced — the test fails as drift.
|
|
server.use(
|
|
http.get(
|
|
`/api/annotations/dataset/${happySplitAnnotation.id}`,
|
|
() => jsonResponse(happySplitAnnotation),
|
|
),
|
|
)
|
|
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
|
|
await waitFor(
|
|
() => expect(screen.getAllByRole('img').length).toBeGreaterThan(0),
|
|
{ timeout: 3000 },
|
|
)
|
|
|
|
const happyImg = screen.getByAltText(
|
|
new RegExp(`image-${happySplitAnnotation.mediaId}-${happySplitAnnotation.id}`),
|
|
)
|
|
fireEvent.doubleClick(happyImg.closest('div')!)
|
|
|
|
// The parsed tile rect should be exposed via a `data-tile-rect`
|
|
// attribute on the TileViewer mount, e.g. "0.4,0.4,0.6,0.6"
|
|
// (cx-w/2, cy-h/2, cx+w/2, cy+h/2) for input "3 0.5 0.5 0.2 0.2".
|
|
// No such element exists today.
|
|
const rect = await screen.findByTestId('tile-rect', {}, { timeout: 1500 })
|
|
expect(rect.getAttribute('data-tile-rect')).toBe('0.4,0.4,0.6,0.6')
|
|
},
|
|
)
|
|
|
|
it('control: today the editor mounts without parsing splitTile', async () => {
|
|
server.use(
|
|
http.get(
|
|
`/api/annotations/dataset/${happySplitAnnotation.id}`,
|
|
() => jsonResponse(happySplitAnnotation),
|
|
),
|
|
)
|
|
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
await waitFor(
|
|
() => expect(screen.getAllByRole('img').length).toBeGreaterThan(0),
|
|
{ timeout: 3000 },
|
|
)
|
|
|
|
const happyImg = screen.getByAltText(
|
|
new RegExp(`image-${happySplitAnnotation.mediaId}-${happySplitAnnotation.id}`),
|
|
)
|
|
fireEvent.doubleClick(happyImg.closest('div')!)
|
|
|
|
// Pin: no tile-rect testid is present today.
|
|
expect(screen.queryByTestId('tile-rect')).toBeNull()
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AC-3 — DatasetItem.isSplit honored on the dataset list (FT-P-53).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('AZ-474 — AC-3 (FT-P-53): DatasetItem.isSplit honored on dataset list', () => {
|
|
it.fails(
|
|
'items with isSplit: true render a split affordance distinct from non-split items',
|
|
async () => {
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
await waitFor(
|
|
() => expect(screen.getAllByRole('img').length).toBeGreaterThanOrEqual(3),
|
|
{ timeout: 3000 },
|
|
)
|
|
|
|
// Spec: the rendered card for an isSplit annotation MUST carry a
|
|
// visible affordance that the non-split card does NOT carry. The
|
|
// simplest acceptable shape is `data-is-split="true"` on the card
|
|
// root, but a localized badge / icon would also satisfy this.
|
|
const happyCard = screen
|
|
.getByAltText(
|
|
new RegExp(`image-${happySplitAnnotation.mediaId}-${happySplitAnnotation.id}`),
|
|
)
|
|
.closest('div')
|
|
const nonSplitCard = screen
|
|
.getByAltText(
|
|
new RegExp(`image-${nonSplitAnnotation.mediaId}-${nonSplitAnnotation.id}`),
|
|
)
|
|
.closest('div')
|
|
|
|
// Drift today: isSplit is read from the network shape but never
|
|
// consumed by the renderer.
|
|
expect(happyCard?.getAttribute('data-is-split')).toBe('true')
|
|
expect(nonSplitCard?.getAttribute('data-is-split')).not.toBe('true')
|
|
},
|
|
)
|
|
|
|
it('control: dataset list mounts and renders all rows even with mixed isSplit values', async () => {
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
|
|
// Pin: page renders both split and non-split items without crash.
|
|
await waitFor(
|
|
() =>
|
|
expect(screen.getAllByRole('img').length).toBeGreaterThanOrEqual(3),
|
|
{ timeout: 3000 },
|
|
)
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AC-4 — auto-zoom viewport matches tile rect (FT-P-54).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('AZ-474 — AC-4 (FT-P-54): tile auto-zoom viewport matches tile rect', () => {
|
|
it.fails('opening a split annotation auto-zooms the viewport to the tile rect', async () => {
|
|
server.use(
|
|
http.get(
|
|
`/api/annotations/dataset/${happySplitAnnotation.id}`,
|
|
() => jsonResponse(happySplitAnnotation),
|
|
),
|
|
)
|
|
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
await waitFor(
|
|
() => expect(screen.getAllByRole('img').length).toBeGreaterThan(0),
|
|
{ timeout: 3000 },
|
|
)
|
|
|
|
const happyImg = screen.getByAltText(
|
|
new RegExp(`image-${happySplitAnnotation.mediaId}-${happySplitAnnotation.id}`),
|
|
)
|
|
fireEvent.doubleClick(happyImg.closest('div')!)
|
|
|
|
// Spec: the viewport rect (in normalized canvas coords) should match
|
|
// the parsed tile rect — for "3 0.5 0.5 0.2 0.2" → [0.4, 0.4, 0.6, 0.6]
|
|
// ±0.5 px after rendering. We expose this via a `data-viewport-rect`
|
|
// attribute on the canvas mount.
|
|
const viewport = await screen.findByTestId(
|
|
'tile-viewport',
|
|
{},
|
|
{ timeout: 1500 },
|
|
)
|
|
const rect = viewport.getAttribute('data-viewport-rect') ?? ''
|
|
const [x1, y1, x2, y2] = rect.split(',').map(Number)
|
|
expect(Math.abs(x1 - 0.4)).toBeLessThan(0.01)
|
|
expect(Math.abs(y1 - 0.4)).toBeLessThan(0.01)
|
|
expect(Math.abs(x2 - 0.6)).toBeLessThan(0.01)
|
|
expect(Math.abs(y2 - 0.6)).toBeLessThan(0.01)
|
|
})
|
|
|
|
it('control: today no tile-viewport testid is exposed', () => {
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
expect(screen.queryByTestId('tile-viewport')).toBeNull()
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AC-5 — zoom indicator visible while active (FT-P-55).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('AZ-474 — AC-5 (FT-P-55): tile-zoom indicator visible while active', () => {
|
|
it.fails(
|
|
'while zoomed into a tile, the indicator carries an accessible name',
|
|
async () => {
|
|
server.use(
|
|
http.get(
|
|
`/api/annotations/dataset/${happySplitAnnotation.id}`,
|
|
() => jsonResponse(happySplitAnnotation),
|
|
),
|
|
)
|
|
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
await waitFor(
|
|
() => expect(screen.getAllByRole('img').length).toBeGreaterThan(0),
|
|
{ timeout: 3000 },
|
|
)
|
|
|
|
const happyImg = screen.getByAltText(
|
|
new RegExp(`image-${happySplitAnnotation.mediaId}-${happySplitAnnotation.id}`),
|
|
)
|
|
fireEvent.doubleClick(happyImg.closest('div')!)
|
|
|
|
// Spec: an indicator with role="status" and an accessible name
|
|
// matching the i18n-keyed "Tile zoom" text (or equivalent) is in
|
|
// the DOM while the zoom is active.
|
|
const indicator = await screen.findByRole(
|
|
'status',
|
|
{ name: /tile|zoom/i },
|
|
{ timeout: 1500 },
|
|
)
|
|
expect(indicator).toBeInTheDocument()
|
|
},
|
|
)
|
|
|
|
it('control: today no role="status" + name=/tile|zoom/ indicator is mounted', () => {
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
// Pin: there may be other role=status nodes (spinners), but none with
|
|
// a tile/zoom accessible name.
|
|
expect(screen.queryByRole('status', { name: /tile|zoom/i })).toBeNull()
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AC-6 — malformed YOLO label surfaces a user-visible error (FT-N-10).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('AZ-474 — AC-6 (FT-N-10): malformed YOLO label produces user-visible error', () => {
|
|
it.fails(
|
|
'opening an annotation with splitTile="garbage" renders an in-DOM error and no NaN bbox',
|
|
async () => {
|
|
server.use(
|
|
http.get(
|
|
`/api/annotations/dataset/${malformedSplitAnnotation.id}`,
|
|
() => jsonResponse(malformedSplitAnnotation),
|
|
),
|
|
)
|
|
|
|
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => undefined)
|
|
|
|
try {
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
await waitFor(
|
|
() => expect(screen.getAllByRole('img').length).toBeGreaterThan(0),
|
|
{ timeout: 3000 },
|
|
)
|
|
|
|
const malformedImg = screen.getByAltText(
|
|
new RegExp(
|
|
`image-${malformedSplitAnnotation.mediaId}-${malformedSplitAnnotation.id}`,
|
|
),
|
|
)
|
|
fireEvent.doubleClick(malformedImg.closest('div')!)
|
|
|
|
// Spec: user-visible error surfaces — a role="alert" region or a
|
|
// localized toast — and NO bbox is rendered for the malformed label.
|
|
// alert() is forbidden by NFT-SEC-07; the assertion below pins that.
|
|
const alertEl = await screen.findByRole('alert', {}, { timeout: 1500 })
|
|
expect(alertEl).toBeInTheDocument()
|
|
expect(alertSpy).not.toHaveBeenCalled()
|
|
|
|
// No NaN-rendered box: every rendered bbox stroke produces finite
|
|
// getBoundingClientRect values. We check via canvas geometry —
|
|
// CanvasEditor draws into a 2D context, so any NaN coords would
|
|
// have made the canvas blank or thrown — neither is observable
|
|
// post-fix because the page should refuse to render the tile and
|
|
// surface the alert instead.
|
|
const canvases = document.querySelectorAll('canvas')
|
|
for (const c of canvases) {
|
|
const rect = c.getBoundingClientRect()
|
|
expect(Number.isFinite(rect.width)).toBe(true)
|
|
expect(Number.isFinite(rect.height)).toBe(true)
|
|
}
|
|
} finally {
|
|
alertSpy.mockRestore()
|
|
}
|
|
},
|
|
)
|
|
|
|
it('control: today the page does NOT crash on a malformed splitTile (silent swallow)', async () => {
|
|
server.use(
|
|
http.get(
|
|
`/api/annotations/dataset/${malformedSplitAnnotation.id}`,
|
|
() => jsonResponse(malformedSplitAnnotation),
|
|
),
|
|
)
|
|
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
await waitFor(
|
|
() => expect(screen.getAllByRole('img').length).toBeGreaterThan(0),
|
|
{ timeout: 3000 },
|
|
)
|
|
|
|
const malformedImg = screen.getByAltText(
|
|
new RegExp(
|
|
`image-${malformedSplitAnnotation.mediaId}-${malformedSplitAnnotation.id}`,
|
|
),
|
|
)
|
|
fireEvent.doubleClick(malformedImg.closest('div')!)
|
|
|
|
// Pin: page stays mounted; no role="alert" is rendered today.
|
|
expect(screen.queryByRole('alert')).toBeNull()
|
|
})
|
|
|
|
it('control: alert() is never called from the dataset double-click path', async () => {
|
|
server.use(
|
|
http.get(
|
|
`/api/annotations/dataset/${malformedSplitAnnotation.id}`,
|
|
() => jsonResponse(malformedSplitAnnotation),
|
|
),
|
|
)
|
|
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => undefined)
|
|
|
|
try {
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<DatasetPage />
|
|
</FlightProvider>,
|
|
)
|
|
await waitFor(
|
|
() => expect(screen.getAllByRole('img').length).toBeGreaterThan(0),
|
|
{ timeout: 3000 },
|
|
)
|
|
|
|
const malformedImg = screen.getByAltText(
|
|
new RegExp(
|
|
`image-${malformedSplitAnnotation.mediaId}-${malformedSplitAnnotation.id}`,
|
|
),
|
|
)
|
|
fireEvent.doubleClick(malformedImg.closest('div')!)
|
|
|
|
// Defence in depth (NFT-SEC-07): alert() is banned outside the
|
|
// seeded allow-list. This control passes today (no alert) AND
|
|
// continues to pass after the fix lands (which uses an in-DOM alert).
|
|
expect(alertSpy).not.toHaveBeenCalled()
|
|
} finally {
|
|
alertSpy.mockRestore()
|
|
}
|
|
})
|
|
})
|