From a029e951b8acfa325d7aa66e750ca4a8a2b83b90 Mon Sep 17 00:00:00 2001 From: nitrix Date: Thu, 5 Mar 2026 10:57:10 +0100 Subject: [PATCH] Add event stub page with clipboard sharing Post-creation confirmation page showing shareable event URL with copy-to-clipboard and fallback feedback on failure. Co-Authored-By: Claude Opus 4.6 --- frontend/src/views/EventStubView.vue | 132 ++++++++++++++++++ .../src/views/__tests__/EventStubView.spec.ts | 87 ++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 frontend/src/views/EventStubView.vue create mode 100644 frontend/src/views/__tests__/EventStubView.spec.ts diff --git a/frontend/src/views/EventStubView.vue b/frontend/src/views/EventStubView.vue new file mode 100644 index 0000000..545ff1f --- /dev/null +++ b/frontend/src/views/EventStubView.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/frontend/src/views/__tests__/EventStubView.spec.ts b/frontend/src/views/__tests__/EventStubView.spec.ts new file mode 100644 index 0000000..5f93e1f --- /dev/null +++ b/frontend/src/views/__tests__/EventStubView.spec.ts @@ -0,0 +1,87 @@ +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: '
' } }, + { path: '/events/:token', 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('/') + }) +})