feat(01-03): paid tick renders the branded screensaver ring (FED-06)
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:
archipelago
2026-08-01 06:07:10 -04:00
co-authored by Claude Opus 5
parent 8b51b7e2dc
commit 579398981f
7 changed files with 406 additions and 40 deletions
@@ -0,0 +1,170 @@
---
phase: 01-federation-mesh-hardening
plan: 03
subsystem: ui
tags: [vue, css, wallet, branding, reduced-motion]
requires:
- phase: 01-federation-mesh-hardening
provides: "ScreensaverRing.vue's existing default/compact size variants and segment-pulse animation, plus the two paid-tick surfaces (SendBitcoinModal's ripple burst, WalletScanModal's plain circle)"
provides:
- "A third `badge` ring variant (160px / 192px, --viz-radius 80px / 96px) sized to sit inside a modal card"
- "The site-wide prefers-reduced-motion guard ScreensaverRing was missing entirely, now covering every variant including the two pre-existing ones"
- "Both paid-tick surfaces rendering the identical branded ring"
affects: [wallet, web5, screensaver]
tech-stack:
added: []
patterns:
- "New size variants on a shared visual component are added as their own class + mapping, never as a transform scale of an existing one — a scaled ring would also scale its segment stroke widths and blur, which is why 01-UI-SPEC.md ruled out the transform hack."
key-files:
created:
- neode-ui/src/components/__tests__/ScreensaverRing.test.ts
- neode-ui/src/components/__tests__/PaidTick.test.ts
modified:
- neode-ui/src/components/ScreensaverRing.vue
- neode-ui/src/components/SendBitcoinModal.vue
- neode-ui/src/components/WalletScanModal.vue
key-decisions:
- "Composition is identical on both surfaces: a `position: relative` badge-sized container holding <ScreensaverRing size=\"badge\" /> with the checkmark core absolutely centred over it — so the two paid ticks cannot drift apart visually."
- "SendBitcoinModal keeps its burst-pop/burst-draw check animation; only the three ripple `burst-ring` spans and the burst-ripple keyframes were removed, since the ring now carries the motion."
- "WalletScanModal's core was kept at 6rem (it had no ripple to replace, just a w-24 circle) against SendBitcoinModal's 7rem, preserving each surface's existing checkmark proportion rather than homogenising them."
- "The reduced-motion guard was added to ScreensaverRing itself rather than per call site, so the screensaver and SystemDangerZone variants gain it too — 01-UI-SPEC.md flagged its total absence as a real gap this phase should close."
requirements-completed: [FED-06]
coverage:
- id: D1
description: "The payment-success tick in SendBitcoinModal renders the EQ-segment ring, not a CSS ripple burst"
requirement: "FED-06"
verification:
- kind: unit
ref: "neode-ui/src/components/__tests__/PaidTick.test.ts#SendBitcoinModal: payment success shows exactly one badge ring, no ripple burst"
status: pass
human_judgment: false
- id: D2
description: "The scan-modal paid tick renders the same ring, so the paid tick is identical on every surface"
requirement: "FED-06"
verification:
- kind: unit
ref: "…#WalletScanModal: success pane shows the same badge ring and keeps its checkmark"
status: pass
human_judgment: false
- id: D3
description: "A badge variant exists at 160px/192px with --viz-radius 80px/96px, alongside untouched default and compact variants"
requirement: "FED-06"
verification:
- kind: unit
ref: "neode-ui/src/components/__tests__/ScreensaverRing.test.ts#maps each size variant to its own ring class; #keeps the existing variants off the badge class"
status: pass
human_judgment: false
- id: D4
description: "Success amount numerals and SENT / Done copy are unchanged — only the ring geometry behind the checkmark changed"
requirement: "FED-06"
verification:
- kind: unit
ref: "…#SendBitcoinModal… asserts the 12,345 amount and SENT copy still render"
status: pass
human_judgment: false
- id: D5
description: "SystemDangerZone and Screensaver continue to render compact and default unchanged"
requirement: "FED-06"
verification:
- kind: other
ref: "git status shows Screensaver.vue and SystemDangerZone.vue untouched; viz-ring-compact occurrence count unchanged at 3"
status: pass
human_judgment: false
- id: D6
description: "Segment animation is disabled under prefers-reduced-motion for every size variant"
requirement: "FED-06"
verification:
- kind: other
ref: "@media (prefers-reduced-motion: reduce) { .viz-segment { animation: none; opacity: 0.55 } } — exactly one such block in ScreensaverRing.vue"
status: pass
human_judgment: false
- id: D7
description: "The badge ring fits inside the modal card without clipping at either breakpoint"
requirement: "FED-06"
verification:
- kind: manual_procedural
ref: "NOT yet observed live — see Deviations. Geometric check only: 160px badge inside a max-w-2xl card is ~344px of usable width at a 390px viewport and 672px at desktop, so the ring cannot exceed the card box."
status: deferred
human_judgment: true
duration: 55min
completed: 2026-08-01
status: complete
---
# Phase 1 Plan 3: On-Brand Paid Tick (FED-06) Summary
**Both paid-tick surfaces now render the screensaver's EQ-segment ring at a new modal-sized `badge` variant, and the shared ring component finally honours `prefers-reduced-motion` — a guard it had been missing for every variant, not just the new one.**
## Performance
- **Duration:** ~55 min
- **Completed:** 2026-08-01
- **Tasks:** 2/2
- **Files modified:** 5 (3 components, 2 new test files)
## Accomplishments
- `ScreensaverRing` gained a `badge` size (160px mobile / 192px from 768px up, with matching `--viz-radius` 80px / 96px) mapped through `sizeClass`, following the exact shape of the existing two variants. `default` and `compact` are untouched.
- The reduced-motion guard was added to the component itself, so the screensaver and SystemDangerZone call sites gain it as a side benefit — `01-UI-SPEC.md` had flagged its complete absence.
- `SendBitcoinModal`'s success pane swapped its three CSS ripple spans for the ring, keeping the emerald pop-in check and its draw animation; the dead `burst-ring*` rules and `burst-ripple` keyframes were removed and the reduced-motion rule updated to drop the clause referencing them.
- `WalletScanModal`'s plain `w-24` circle became the same composition, so both paid ticks are now literally the same markup shape.
- Two new suites (5 tests) pin the variant mapping, the segment count, and both call sites — including that the ripple elements are gone and the amount/SENT copy is unchanged.
## Task Commits
1. **Task 1: badge variant + SendBitcoinModal** and **Task 2: WalletScanModal** — committed together with this SUMMARY (both surfaces share the composition; splitting them would have committed a half-converted pair of paid ticks).
## Files Created/Modified
- `neode-ui/src/components/ScreensaverRing.vue``'badge'` added to the size union, `sizeClass` widened to an if-chain, `.viz-ring-badge` rule + 768px breakpoint, and the `prefers-reduced-motion` guard on `.viz-segment`.
- `neode-ui/src/components/SendBitcoinModal.vue``ScreensaverRing` import; success pane restructured to `.send-success-badge` > ring + centred `.send-success-burst`/`.burst-core`; ripple markup, `.burst-ring*` rules and `burst-ripple` keyframes deleted.
- `neode-ui/src/components/WalletScanModal.vue``ScreensaverRing` import; `.scan-success-badge` + `.scan-success-core` replacing the fixed circle and its `.success-ring` rule.
- `neode-ui/src/components/__tests__/ScreensaverRing.test.ts` — 3 tests.
- `neode-ui/src/components/__tests__/PaidTick.test.ts` — 2 tests, one per surface.
## Deviations from Plan
### Test assertions had to target the document, not the wrapper
**Found during:** Task 1, first green run
**Issue:** `wrapper.find('.burst-core')` returned nothing even though the markup rendered. `BaseModal` teleports its content to `document.body`, so the rendered nodes live outside the mounted wrapper's own root element. Component-tree queries (`findAllComponents`) still work, which is why the ring assertions passed while the DOM ones failed.
**Resolution:** DOM assertions switched to `document.querySelector(...)`, with an `afterEach` clearing `document.body` so one modal's teleported nodes cannot answer the next test's queries.
**Files modified:** `neode-ui/src/components/__tests__/PaidTick.test.ts`
### Auto-fixed: vue-tsc strict-null on indexed access
**Found during:** `npm run build`
**Issue:** `rings[0].props('size')` failed `vue-tsc` under `noUncheckedIndexedAccess` (TS2532) — the same class of failure commit `4a8925f0` fixed in `usePaidItemViewer.test.ts`.
**Resolution:** optional chaining (`rings[0]?.props('size')`), matching that commit's fix exactly.
**Files modified:** `neode-ui/src/components/__tests__/PaidTick.test.ts`
### Outstanding: the live dev-preview observation Task 2 asks for was NOT made
**Found during:** Task 2 verification
**Issue:** Task 2 requires confirming on a dev preview that neither ring is clipped by the modal card's scrolling container at a narrow and a desktop viewport. Reaching either success pane in a real browser requires an actually-settled payment; the built preview on `:4321` has no backend to settle one, and the running demo/dev servers in this tree belong to other sessions and are not to be disturbed.
**Resolution:** NOT resolved. Recorded honestly as deferred rather than claimed. The automated evidence (5 tests, `npm run build` green, `viz-ring-badge` present in the built bundle) covers structure but not appearance. The plan already consolidates the blocking human sign-off for this into **plan 01-07**, which is where the visual check belongs; the geometric argument (a 160px badge inside a `max-w-2xl` card, ~344px usable at a 390px viewport) says clipping is implausible, but it is not an observation.
**Files modified:** none
## Known Stubs
None.
## Threat Flags
None — presentational only. The plan's safety prohibition (the ring must never render success for an unsettled payment) is structurally satisfied: no success-gating condition was touched, only the markup inside an already-gated pane.
## Self-Check: PASSED
- FOUND: `viz-ring-badge` in `ScreensaverRing.vue` (3 occurrences: mapping + rule + breakpoint)
- FOUND: `prefers-reduced-motion` in `ScreensaverRing.vue` (exactly 1)
- CONFIRMED: `burst-ring` count in `SendBitcoinModal.vue` == 0; `success-ring` in `WalletScanModal.vue` == 0
- CONFIRMED: `npx vitest run` — 102 files, 822 tests, all pass
- CONFIRMED: `npm run build` exits 0 and `grep -rq 'viz-ring-badge' web/dist/neode-ui/assets/` succeeds
</content>
@@ -128,9 +128,32 @@ None.
None — a presentational affordance with no new endpoint, no auth surface, and no state mutation. The one safety-relevant property (that it cannot set confirmation state) is asserted by test.
## Live-browser verification (added post-commit)
Run against the built bundle on the `:4321` preview via Playwright, at the three viewports the
original executor's script targeted:
| Viewport | Cue shown | After activating it |
|---|---|---|
| 1280×620 (short) | yes | tickbox in view, `checked=false`, cue gone |
| 1440×1000 (tall) | **no** — element never renders | n/a |
| 390×740 (narrow) | yes | tickbox in view, `checked=false`, cue gone |
A geometry probe confirmed the mechanism rather than just the outcome: after activation the scroll
container sits at its maximum offset (`scrollTop == scrollHeight - clientHeight`) with the tickbox's
bottom above the container's (442 vs 454 at 1280×620; 622 vs 634 at 390×740), which is exactly the
condition the cue's visibility is derived from.
One caveat worth recording: the *first* run reported the cue still visible after activation at
1280×620. It did not reproduce on any subsequent run, and the geometry probe showed the cue absent at
600 ms, 1500 ms and 3000 ms after the click at both viewports. The first run was the cold load
immediately after a rebuild, so it is almost certainly smooth-scroll settling — but it is written
down rather than discarded, because it is the one observation that contradicts the contract.
## Self-Check: PASSED
- FOUND: `neode-ui/src/views/OnboardingSeedGenerate.vue` (modified)
- FOUND: `neode-ui/src/views/__tests__/OnboardingScrollCue.test.ts` (created, 6 tests)
- CONFIRMED: `npx vitest run` full frontend suite green
- CONFIRMED: `npx vitest run` full frontend suite green (102 files, 822 tests)
- CONFIRMED: live browser, 3 viewports (table above)
</content>
+32 -3
View File
@@ -14,13 +14,18 @@ import { computed } from 'vue'
const props = withDefaults(defineProps<{
/** Visual size: 'default' matches the screensaver; 'compact' drops the
* min-width breakpoints (useful inside overlays on narrower canvases). */
size?: 'default' | 'compact'
* min-width breakpoints (useful inside overlays on narrower canvases);
* 'badge' is the paid-tick size that fits inside a modal card (FED-06). */
size?: 'default' | 'compact' | 'badge'
/** Override segment count. Defaults to 48 (screensaver standard). */
segmentCount?: number
}>(), { size: 'default', segmentCount: 48 })
const sizeClass = computed(() => props.size === 'compact' ? 'viz-ring-compact' : 'viz-ring-default')
const sizeClass = computed(() => {
if (props.size === 'compact') return 'viz-ring-compact'
if (props.size === 'badge') return 'viz-ring-badge'
return 'viz-ring-default'
})
function getSegmentStyle(i: number) {
const deg = (i / props.segmentCount) * 360
@@ -70,6 +75,21 @@ function getSegmentStyle(i: number) {
}
}
/* Paid-tick badge (FED-06) — sized to sit inside a modal card without
clipping, unlike the compact variant (240-320px against a ~112px core). */
.viz-ring-badge {
width: 160px;
height: 160px;
--viz-radius: 80px;
}
@media (min-width: 768px) {
.viz-ring-badge {
width: 192px;
height: 192px;
--viz-radius: 96px;
}
}
.viz-segment {
position: absolute;
left: 50%;
@@ -111,4 +131,13 @@ function getSegmentStyle(i: number) {
92.9%{ opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
100% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
}
/* Site-wide reduced-motion convention — applies to every size variant, so
the ring holds a static, legible pose instead of pulsing. */
@media (prefers-reduced-motion: reduce) {
.viz-segment {
animation: none;
opacity: 0.55;
}
}
</style>
+30 -31
View File
@@ -3,14 +3,14 @@
<!-- ============ SUCCESS PANE the payment's moment, not a footnote ============ -->
<template v-if="successInfo">
<div class="text-center py-4">
<div class="send-success-burst mx-auto mb-6">
<span class="burst-ring"></span>
<span class="burst-ring burst-ring-2"></span>
<span class="burst-ring burst-ring-3"></span>
<div class="burst-core">
<svg class="w-14 h-14 text-green-400 burst-check" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
<div class="send-success-badge mx-auto mb-6">
<ScreensaverRing size="badge" />
<div class="send-success-burst">
<div class="burst-core">
<svg class="w-14 h-14 text-green-400 burst-check" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
</div>
@@ -259,6 +259,7 @@ import { ref, computed, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { rpcClient } from '@/api/rpc-client'
import BaseModal from '@/components/BaseModal.vue'
import ScreensaverRing from '@/components/ScreensaverRing.vue'
const { t } = useI18n()
@@ -681,10 +682,27 @@ async function send() {
</script>
<style scoped>
/* Success burst — pop-in check inside radiating rings, wallet palette
(emerald for the settled payment, one Archipelago-orange ring). */
.send-success-burst {
/* Success badge (FED-06) — the branded ScreensaverRing carries the motion,
with the emerald pop-in check centred over it. */
.send-success-badge {
position: relative;
width: 160px;
height: 160px;
display: flex;
align-items: center;
justify-content: center;
}
@media (min-width: 768px) {
.send-success-badge {
width: 192px;
height: 192px;
}
}
.send-success-burst {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 7rem;
height: 7rem;
}
@@ -704,20 +722,6 @@ async function send() {
stroke-dashoffset: 32;
animation: burst-draw 0.45s ease-out 0.25s forwards;
}
.burst-ring {
position: absolute;
inset: 0;
border-radius: 9999px;
border: 2px solid rgba(16, 185, 129, 0.45);
animation: burst-ripple 1.8s ease-out infinite;
}
.burst-ring-2 {
animation-delay: 0.45s;
}
.burst-ring-3 {
animation-delay: 0.9s;
border-color: rgba(249, 115, 22, 0.35);
}
@keyframes burst-pop {
from { transform: scale(0.3); opacity: 0; }
to { transform: scale(1); opacity: 1; }
@@ -725,13 +729,8 @@ async function send() {
@keyframes burst-draw {
to { stroke-dashoffset: 0; }
}
@keyframes burst-ripple {
0% { transform: scale(0.7); opacity: 0.9; }
100% { transform: scale(2); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.burst-core, .burst-check, .burst-ring { animation: none; }
.burst-core, .burst-check { animation: none; }
.burst-check { stroke-dashoffset: 0; }
.burst-ring { display: none; }
}
</style>
+35 -5
View File
@@ -229,10 +229,13 @@
<!-- ============ SUCCESS PANE ============ -->
<div v-else key="success" class="text-center py-2">
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full mx-auto mb-5 success-ring">
<svg class="w-14 h-14 text-green-400" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
<div class="scan-success-badge mx-auto mb-5">
<ScreensaverRing size="badge" />
<div class="scan-success-core">
<svg class="w-14 h-14 text-green-400" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<div v-if="successAmount > 0" class="text-5xl font-black text-green-400 mb-1">
@@ -268,6 +271,7 @@ import { rpcClient } from '@/api/rpc-client'
import { useAnimatedQRDecoder } from '@/composables/useAnimatedQRDecoder'
import { useModalKeyboard } from '@/composables/useModalKeyboard'
import { useBodyScrollLock } from '@/composables/useBodyScrollLock'
import ScreensaverRing from '@/components/ScreensaverRing.vue'
type Rail = 'onchain' | 'lightning' | 'cashu' | 'fedimint'
type Action = 'pay-invoice' | 'send-onchain' | 'redeem-token' | 'fedimint-join'
@@ -858,7 +862,33 @@ onUnmounted(stopScanning)
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.35);
}
.success-ring {
/* Paid tick (FED-06) — same composition as SendBitcoinModal: the branded
ScreensaverRing badge with the emerald checkmark core centred over it. */
.scan-success-badge {
position: relative;
width: 160px;
height: 160px;
display: flex;
align-items: center;
justify-content: center;
}
@media (min-width: 768px) {
.scan-success-badge {
width: 192px;
height: 192px;
}
}
.scan-success-core {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 6rem;
height: 6rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
background: rgba(16, 185, 129, 0.12);
box-shadow: 0 0 40px rgba(16, 185, 129, 0.25);
}
@@ -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)
})
})