V13-FE-006: consolidate approved UI and contract hardening
This commit is contained in:
@@ -1,17 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { formatAsOf } from '../formatters/financial'
|
||||
|
||||
const props = defineProps<{
|
||||
asOf: string
|
||||
staleAfterMinutes: number
|
||||
now: string
|
||||
source?: string
|
||||
revision?: number
|
||||
refreshable?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{ refresh: [] }>()
|
||||
|
||||
const ageMinutes = computed(() => Math.max(0, (Date.now() - new Date(props.asOf).getTime()) / 60_000))
|
||||
const ageMinutes = computed(() => Math.max(0, (new Date(props.now).getTime() - new Date(props.asOf).getTime()) / 60_000))
|
||||
const stale = computed(() => ageMinutes.value > props.staleAfterMinutes)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :aria-label="stale ? '데이터 지연' : '데이터 최신'" :data-status="stale ? 'stale' : 'fresh'">
|
||||
{{ stale ? 'STALE' : 'FRESH' }} · {{ new Date(props.asOf).toLocaleString() }}
|
||||
<button v-if="props.refreshable" type="button" :aria-label="stale ? '데이터 지연, 새로 고침' : '데이터 최신, 새로 고침'" :data-status="stale ? 'stale' : 'fresh'" :title="props.source ? `출처: ${props.source}` : undefined" @click="emit('refresh')">
|
||||
<span aria-hidden="true">↻</span> {{ stale ? 'STALE' : 'FRESH' }} · {{ formatAsOf(props.asOf) }}<span v-if="props.revision !== undefined"> · rev {{ props.revision }}</span>
|
||||
</button>
|
||||
<span v-else :aria-label="stale ? '데이터 지연' : '데이터 최신'" :data-status="stale ? 'stale' : 'fresh'" :title="props.source ? `출처: ${props.source}` : undefined">
|
||||
{{ stale ? 'STALE' : 'FRESH' }} · {{ formatAsOf(props.asOf) }}<span v-if="props.revision !== undefined"> · rev {{ props.revision }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import DataFreshnessBadge from '../DataFreshnessBadge.vue'
|
||||
|
||||
describe('DataFreshnessBadge', () => {
|
||||
const base = { asOf: '2026-08-12T00:00:00Z', staleAfterMinutes: 30, now: '2026-08-12T00:29:00Z' }
|
||||
|
||||
it('uses the injected instant instead of the machine clock', () => {
|
||||
const wrapper = mount(DataFreshnessBadge, { props: base })
|
||||
expect(wrapper.attributes('data-status')).toBe('fresh')
|
||||
})
|
||||
|
||||
it('marks data stale only after the configured boundary', () => {
|
||||
const wrapper = mount(DataFreshnessBadge, { props: { ...base, now: '2026-08-12T00:30:01Z' } })
|
||||
expect(wrapper.attributes('data-status')).toBe('stale')
|
||||
expect(wrapper.attributes('aria-label')).toBe('데이터 지연')
|
||||
})
|
||||
|
||||
it('exposes source/revision and emits an explicit refresh command only when enabled', async () => {
|
||||
const wrapper = mount(DataFreshnessBadge, { props: { ...base, source: 'approved-read-model', revision: 3, refreshable: true } })
|
||||
expect(wrapper.text()).toContain('rev 3')
|
||||
expect(wrapper.attributes('title')).toContain('approved-read-model')
|
||||
await wrapper.trigger('click')
|
||||
expect(wrapper.emitted('refresh')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user