LoVisual/frontend/src/shared/easter/tests/breakable.test.tsx
loki5512344 fd2e29cede
feat(frontend): avatar upload QOL — feedback, re-crop, drop, progress, history
Task 12 batch around the existing crop/preview flow:
- Success Notice (auto-dismiss ~2.5s) after an avatar upload.
- "Reposition" re-crops the current avatar without re-picking a file.
- Drag & drop an image straight onto the profile avatar circle.
- Real upload progress via a new XHR-backed api.upload (fetch has none);
  refreshOnce now also stores the renewed token (reload-restore fix).
- Local avatar history (last 5, IndexedDB, this-device-only) strip that
  re-uploads a stored crop. Stored as ArrayBuffer (Blob is not cloneable
  under structuredClone); label makes the per-device scope explicit.
- Added a "success" tone to Notice.

Infra (same session): registered @shared/@features/@pages/@app/@test path
aliases in vite.config + tsconfigs and rewrote deep ../../../ imports;
moved avatar components into components/avatar/ to keep folders at <=4 files.

Tests: avatarHistory cap/order, recrop visibility, drop-opens-cropper,
client upload/progress/401-retry. bun test 103 pass, lint 0 errors, build ok.
2026-09-28 22:56:01 +02:00

36 lines
1.4 KiB
TypeScript

import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { expect, test, vi } from 'vitest'
import { BreakableBlock } from '../BreakableBlock'
import { renderApp } from '@test/render'
// motion's useReducedMotion reads matchMedia, which jsdom lacks.
vi.stubGlobal('matchMedia', (query: string) => ({
matches: !query.includes('reduce'),
media: query,
onchange: null,
addEventListener() {},
removeEventListener() {},
addListener() {},
removeListener() {},
dispatchEvent: () => false,
}))
test('a block drops in and cracks over three clicks, then bursts and respawns', async () => {
const user = userEvent.setup()
renderApp([{ path: '/', element: <BreakableBlock /> }])
// The block remounts (new key) on respawn, so re-query it before each click.
const block = () => screen.getByRole('button', { name: 'Сломать блок' })
expect(block().querySelector('.lv-block-crack')).toBeNull()
await user.click(block())
expect(block().querySelector('.lv-block-crack')).toHaveAttribute('data-stage', '1')
await user.click(block())
expect(block().querySelector('.lv-block-crack')).toHaveAttribute('data-stage', '2')
// Third click shatters: cracks clear (respawn) and debris spawns.
await user.click(block())
expect(block().querySelector('.lv-block-crack')).toBeNull()
expect(document.querySelectorAll('.lv-block-particle').length).toBeGreaterThan(0)
})