feat(01-03): paid tick renders the branded screensaver ring (FED-06)
Demo images / Build & push demo images (push) Failing after 1m14s
Demo images / Build & push demo images (push) Failing after 1m14s
Both paid-tick surfaces now show the EQ-segment ring instead of a CSS ripple burst (send modal) and a plain circle (scan modal), via a new `badge` size variant at 160px/192px with matching --viz-radius. A transform scale of the compact variant was ruled out in 01-UI-SPEC.md because it would scale segment stroke width and blur along with the geometry. Both surfaces use the identical composition — a badge-sized relative container with the checkmark core absolutely centred over the ring — so the two ticks cannot drift apart visually. ScreensaverRing also gains the prefers-reduced-motion guard it never had, on the component rather than per call site, so the screensaver and SystemDangerZone variants are covered too. Also records live-browser verification for 01-13's scroll cue (three viewports, plus a geometry probe of the hide condition). Verified: 5 new tests; full suite 102 files / 822 tests green; npm run build clean with viz-ring-badge present in the built bundle. The live visual no-clipping observation Task 2 asks for is explicitly NOT done — recorded as deferred to plan 01-07's consolidated sign-off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8b51b7e2dc
commit
579398981f
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import i18n from '@/i18n'
|
||||
import ScreensaverRing from '../ScreensaverRing.vue'
|
||||
import SendBitcoinModal from '../SendBitcoinModal.vue'
|
||||
import WalletScanModal from '../WalletScanModal.vue'
|
||||
|
||||
// Both modals fetch balances/fees on open. None of that is what this suite is
|
||||
// about — stub the transport so mounting is deterministic and offline.
|
||||
vi.mock('@/api/rpc-client', () => ({
|
||||
rpcClient: {
|
||||
call: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
}))
|
||||
|
||||
// WalletScanModal reaches for camera/QR APIs jsdom does not implement.
|
||||
beforeEach(() => {
|
||||
if (!navigator.mediaDevices) {
|
||||
Object.defineProperty(navigator, 'mediaDevices', {
|
||||
value: { getUserMedia: vi.fn().mockRejectedValue(new Error('no camera')), enumerateDevices: vi.fn().mockResolvedValue([]) },
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Teleported modal markup outlives the wrapper's root, so clear it between
|
||||
// cases — otherwise one modal's nodes answer the next one's queries.
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
const mountOpts = { props: { show: true }, global: { plugins: [i18n] } }
|
||||
|
||||
describe('paid tick renders the branded ring (FED-06)', () => {
|
||||
it('SendBitcoinModal: payment success shows exactly one badge ring, no ripple burst', async () => {
|
||||
const wrapper = mount(SendBitcoinModal, mountOpts)
|
||||
// Drive the component into its settled-payment state directly — this
|
||||
// suite is about what success *renders*, not how a payment settles.
|
||||
;(wrapper.vm as unknown as Record<string, unknown>).successInfo = {
|
||||
amount: 12345,
|
||||
methodLabel: 'Sent via Lightning',
|
||||
}
|
||||
await flushPromises()
|
||||
|
||||
const rings = wrapper.findAllComponents(ScreensaverRing)
|
||||
expect(rings).toHaveLength(1)
|
||||
expect(rings[0]?.props('size')).toBe('badge')
|
||||
|
||||
// BaseModal teleports its content to <body>, so the rendered markup lives
|
||||
// outside the wrapper's own root element — assert against the document.
|
||||
// The checkmark core survives the ring swap...
|
||||
expect(document.querySelector('.burst-core')).not.toBeNull()
|
||||
expect(document.querySelector('.burst-check')).not.toBeNull()
|
||||
// ...and the CSS ripple elements it replaced are gone entirely.
|
||||
expect(document.querySelectorAll('.burst-ring')).toHaveLength(0)
|
||||
|
||||
// Decoration only: the amount and SENT copy are untouched.
|
||||
expect(document.body.textContent).toContain('12,345')
|
||||
expect(document.body.textContent).toContain('SENT')
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('WalletScanModal: success pane shows the same badge ring and keeps its checkmark', async () => {
|
||||
const wrapper = mount(WalletScanModal, mountOpts)
|
||||
;(wrapper.vm as unknown as Record<string, unknown>).pane = 'success'
|
||||
await flushPromises()
|
||||
|
||||
const rings = wrapper.findAllComponents(ScreensaverRing)
|
||||
expect(rings).toHaveLength(1)
|
||||
expect(rings[0]?.props('size')).toBe('badge')
|
||||
expect(document.querySelector('.scan-success-core')).not.toBeNull()
|
||||
expect(document.querySelector('svg path[d="M5 13l4 4L19 7"]')).not.toBeNull()
|
||||
// The plain fixed-size circle the ring replaced is gone.
|
||||
expect(document.querySelectorAll('.success-ring')).toHaveLength(0)
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ScreensaverRing from '../ScreensaverRing.vue'
|
||||
|
||||
// The ring is shared by the screensaver (default), SystemDangerZone (compact)
|
||||
// and — as of FED-06 — the payment-success tick (badge). These assertions pin
|
||||
// the variant mapping so adding a size can never silently re-point an existing
|
||||
// call site at different geometry.
|
||||
describe('ScreensaverRing', () => {
|
||||
it('maps each size variant to its own ring class', () => {
|
||||
expect(mount(ScreensaverRing).classes()).toContain('viz-ring-default')
|
||||
expect(mount(ScreensaverRing, { props: { size: 'compact' } }).classes()).toContain(
|
||||
'viz-ring-compact',
|
||||
)
|
||||
expect(mount(ScreensaverRing, { props: { size: 'badge' } }).classes()).toContain(
|
||||
'viz-ring-badge',
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the existing variants off the badge class', () => {
|
||||
expect(mount(ScreensaverRing).classes()).not.toContain('viz-ring-badge')
|
||||
expect(mount(ScreensaverRing, { props: { size: 'compact' } }).classes()).not.toContain(
|
||||
'viz-ring-badge',
|
||||
)
|
||||
})
|
||||
|
||||
it('renders one segment per segmentCount, defaulting to 48', () => {
|
||||
expect(mount(ScreensaverRing).findAll('.viz-segment')).toHaveLength(48)
|
||||
expect(
|
||||
mount(ScreensaverRing, { props: { segmentCount: 12 } }).findAll('.viz-segment'),
|
||||
).toHaveLength(12)
|
||||
expect(
|
||||
mount(ScreensaverRing, { props: { size: 'badge', segmentCount: 24 } }).findAll('.viz-segment'),
|
||||
).toHaveLength(24)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user