Enable users to see all their saved events on the home screen, sorted by date with upcoming events first. Key capabilities: - EventCard with title, relative time display, and organizer/attendee role badge - Sortable EventList with past-event visual distinction (faded style) - Empty state when no events are stored - Swipe-to-delete gesture with confirmation dialog - Floating action button for quick event creation - Rename router param :token → :eventToken across all views - useRelativeTime composable (Intl.RelativeTimeFormat) - useEventStorage: add validation, removeEvent(), reactive versioning - Full E2E and unit test coverage for all new components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
import { mount } from '@vue/test-utils'
|
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
|
import EventStubView from '../EventStubView.vue'
|
|
|
|
function createTestRouter() {
|
|
return createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [
|
|
{ path: '/', name: 'home', component: { template: '<div />' } },
|
|
{ path: '/events/:eventToken', name: 'event', component: EventStubView },
|
|
],
|
|
})
|
|
}
|
|
|
|
async function mountWithToken(token = 'test-token-123') {
|
|
const router = createTestRouter()
|
|
await router.push(`/events/${token}`)
|
|
await router.isReady()
|
|
return mount(EventStubView, {
|
|
global: { plugins: [router] },
|
|
})
|
|
}
|
|
|
|
describe('EventStubView', () => {
|
|
it('renders the event URL based on route param', async () => {
|
|
const wrapper = await mountWithToken('abc-def')
|
|
|
|
const linkText = wrapper.find('.stub__link').text()
|
|
expect(linkText).toContain('/events/abc-def')
|
|
})
|
|
|
|
it('shows the correct share URL with origin', async () => {
|
|
const wrapper = await mountWithToken('my-event-token')
|
|
|
|
const linkText = wrapper.find('.stub__link').text()
|
|
expect(linkText).toBe(`${window.location.origin}/events/my-event-token`)
|
|
})
|
|
|
|
it('has a copy button', async () => {
|
|
const wrapper = await mountWithToken()
|
|
|
|
const copyBtn = wrapper.find('.stub__copy')
|
|
expect(copyBtn.exists()).toBe(true)
|
|
expect(copyBtn.text()).toBe('Copy')
|
|
})
|
|
|
|
it('copies link to clipboard and shows confirmation', async () => {
|
|
const writeTextMock = vi.fn().mockResolvedValue(undefined)
|
|
Object.assign(navigator, {
|
|
clipboard: { writeText: writeTextMock },
|
|
})
|
|
|
|
const wrapper = await mountWithToken('copy-test')
|
|
|
|
await wrapper.find('.stub__copy').trigger('click')
|
|
|
|
expect(writeTextMock).toHaveBeenCalledWith(
|
|
`${window.location.origin}/events/copy-test`,
|
|
)
|
|
expect(wrapper.find('.stub__copy').text()).toBe('Copied!')
|
|
})
|
|
|
|
it('shows failure message when clipboard is unavailable', async () => {
|
|
Object.assign(navigator, {
|
|
clipboard: { writeText: vi.fn().mockRejectedValue(new Error('Not allowed')) },
|
|
})
|
|
|
|
const wrapper = await mountWithToken('fail-test')
|
|
|
|
await wrapper.find('.stub__copy').trigger('click')
|
|
|
|
expect(wrapper.find('.stub__copy').text()).toBe('Failed')
|
|
expect(wrapper.find('.stub__copy').attributes('aria-label')).toBe(
|
|
'Copy failed — select the link to copy manually',
|
|
)
|
|
})
|
|
|
|
it('has a back link to home', async () => {
|
|
const wrapper = await mountWithToken()
|
|
|
|
const backLink = wrapper.find('.stub__back')
|
|
expect(backLink.exists()).toBe(true)
|
|
expect(backLink.attributes('aria-label')).toBe('Back to home')
|
|
expect(backLink.attributes('href')).toBe('/')
|
|
})
|
|
})
|