diff --git a/app/components/RunDisplay.test.ts b/app/components/RunDisplay.test.ts index 3ff598c..8b6e479 100644 --- a/app/components/RunDisplay.test.ts +++ b/app/components/RunDisplay.test.ts @@ -10,8 +10,8 @@ import type { Run } from '~/types/run' vi.mock('./RunPanel.vue', () => ({ default: { name: 'RunPanel', - template: '
{{ runId }}
', - props: ['runId', 'url', 'status', 'durationInMs', 'cacheHeaders'], + template: '
{{ runId }}-{{ showRawHeaders }}
', + props: ['runId', 'url', 'status', 'durationInMs', 'cacheHeaders', 'enableDiffOnHover', 'showRawHeaders'], }, })) @@ -74,8 +74,58 @@ describe('RunDisplay', () => { const runPanels = wrapper.findAll('.run-panel-mock') expect(runPanels).toHaveLength(2) - expect(runPanels[0]?.text()).toBe('test-run-1') - expect(runPanels[1]?.text()).toBe('test-run-2') + expect(runPanels[0]?.text()).toBe('test-run-1-false') + expect(runPanels[1]?.text()).toBe('test-run-2-false') + }) + + it('shows raw headers toggle when runs exist', () => { + const wrapper = mount(RunDisplay, { + props: { + runs: mockRuns, + error: null, + loading: false, + onClear: vi.fn(), + }, + }) + + const toggleControl = wrapper.find('.toggle-control') + expect(toggleControl.exists()).toBe(true) + expect(toggleControl.text()).toBe('Show raw headers') + + const checkbox = wrapper.find('input[type="checkbox"]') + expect(checkbox.exists()).toBe(true) + expect((checkbox.element as HTMLInputElement).checked).toBe(false) + }) + + it('hides raw headers toggle when no runs exist', () => { + const wrapper = mount(RunDisplay, { + props: { + runs: [], + error: null, + loading: false, + onClear: vi.fn(), + }, + }) + + expect(wrapper.find('.toggle-control').exists()).toBe(false) + }) + + it('passes showRawHeaders prop to run panels when toggled', async () => { + const wrapper = mount(RunDisplay, { + props: { + runs: mockRuns, + error: null, + loading: false, + onClear: vi.fn(), + }, + }) + + const checkbox = wrapper.find('input[type="checkbox"]') + await checkbox.setValue(true) + + const runPanels = wrapper.findAll('.run-panel-mock') + expect(runPanels[0]?.text()).toBe('test-run-1-true') + expect(runPanels[1]?.text()).toBe('test-run-2-true') }) it('shows clear button when runs exist', () => { @@ -89,7 +139,7 @@ describe('RunDisplay', () => { }, }) - const clearButton = wrapper.find('button') + const clearButton = wrapper.find('.clear-button') expect(clearButton.exists()).toBe(true) expect(clearButton.text()).toBe('Clear') }) @@ -104,7 +154,7 @@ describe('RunDisplay', () => { }, }) - expect(wrapper.find('button').exists()).toBe(false) + expect(wrapper.find('.clear-button').exists()).toBe(false) }) it('calls onClear when clear button is clicked', async () => { @@ -118,7 +168,7 @@ describe('RunDisplay', () => { }, }) - await wrapper.find('button').trigger('click') + await wrapper.find('.clear-button').trigger('click') expect(mockOnClear).toHaveBeenCalledOnce() }) }) diff --git a/app/components/RunDisplay.vue b/app/components/RunDisplay.vue index 5bdfac0..9a6c359 100644 --- a/app/components/RunDisplay.vue +++ b/app/components/RunDisplay.vue @@ -7,6 +7,8 @@ defineProps<{ loading: boolean onClear: () => void }>() + +const showRawHeaders = ref(false) @@ -60,14 +76,111 @@ defineProps<{ .run-panels { flex-wrap: wrap; align-items: stretch; + margin-top: 1em; } .run-panels>* { flex: 1 1 20em; } -.reset-container { - text-align: center; - background-color: inherit; +.controls-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 8px; + margin-bottom: 1.5rem; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); +} + +.toggle-control { + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; + user-select: none; + font-size: 0.875rem; + font-weight: 500; + color: #374151; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.toggle-switch { + position: relative; + display: inline-block; + width: 2.5rem; + height: 1.5rem; + background-color: #cbd5e1; + border-radius: 0.75rem; + transition: background-color 0.2s ease; + cursor: pointer; +} + +.toggle-switch::after { + content: ''; + position: absolute; + top: 0.125rem; + left: 0.125rem; + width: 1.25rem; + height: 1.25rem; + background-color: white; + border-radius: 50%; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2); + transition: transform 0.2s ease; +} + +input:checked + .toggle-switch { + background-color: #3b82f6; +} + +input:checked + .toggle-switch::after { + transform: translateX(1rem); +} + +.toggle-control:hover .toggle-switch { + background-color: #94a3b8; +} + +input:checked + .toggle-switch:hover { + background-color: #2563eb; +} + +.toggle-label { + font-weight: 500; + color: #374151; +} + +.clear-button { + padding: 0.5rem 1rem; + background-color: #ef4444; + color: white; + border: none; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.1s ease; +} + +.clear-button:hover { + background-color: #dc2626; + transform: translateY(-1px); +} + +.clear-button:active { + transform: translateY(0); } diff --git a/app/components/RunPanel.test.ts b/app/components/RunPanel.test.ts new file mode 100644 index 0000000..cbabd99 --- /dev/null +++ b/app/components/RunPanel.test.ts @@ -0,0 +1,105 @@ +/** + * @vitest-environment jsdom + */ +import { describe, it, expect, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import RunPanel from './RunPanel.vue' + +// Mock the components that RunPanel uses +vi.mock('./CacheAnalysis.vue', () => ({ + default: { + name: 'CacheAnalysis', + template: '
Cache Analysis
', + }, +})) + +vi.mock('./RawCacheHeaders.vue', () => ({ + default: { + name: 'RawCacheHeaders', + template: '
Raw Cache Headers
', + }, +})) + +const mockProps = { + runId: 'test-run-id', + url: 'https://example.com', + status: 200, + durationInMs: 150, + cacheHeaders: { + 'cache-control': 'max-age=3600', + 'etag': '"abc123"', + }, + enableDiffOnHover: false, + showRawHeaders: false, +} + +describe('RunPanel', () => { + it('renders basic information correctly', () => { + const wrapper = mount(RunPanel, { + props: mockProps, + global: { + stubs: { + NuxtLink: { + template: '', + props: ['to'], + }, + }, + }, + }) + + expect(wrapper.text()).toContain('https://example.com') + expect(wrapper.text()).toContain('HTTP 200 (150 ms)') + expect(wrapper.find('.cache-analysis-mock').exists()).toBe(true) + }) + + it('renders permalink correctly', () => { + const wrapper = mount(RunPanel, { + props: mockProps, + global: { + stubs: { + NuxtLink: { + template: '', + props: ['to'], + }, + }, + }, + }) + + const permalink = wrapper.find('.run-permalink') + expect(permalink.exists()).toBe(true) + expect(permalink.attributes('href')).toBe('/run/test-run-id') + expect(permalink.text()).toBe('🔗 Permalink') + }) + + it('hides raw headers when showRawHeaders prop is false', () => { + const wrapper = mount(RunPanel, { + props: { ...mockProps, showRawHeaders: false }, + global: { + stubs: { + NuxtLink: { + template: '', + props: ['to'], + }, + }, + }, + }) + + expect(wrapper.find('.raw-cache-headers-mock').exists()).toBe(false) + }) + + it('shows raw headers when showRawHeaders prop is true', () => { + const wrapper = mount(RunPanel, { + props: { ...mockProps, showRawHeaders: true }, + global: { + stubs: { + NuxtLink: { + template: '', + props: ['to'], + }, + }, + }, + }) + + expect(wrapper.find('.raw-cache-headers-mock').exists()).toBe(true) + }) +}) diff --git a/app/components/RunPanel.vue b/app/components/RunPanel.vue index 89d2c43..826a1ab 100644 --- a/app/components/RunPanel.vue +++ b/app/components/RunPanel.vue @@ -6,6 +6,7 @@ const props = defineProps<{ durationInMs: number cacheHeaders: Record enableDiffOnHover: boolean + showRawHeaders: boolean }>() @@ -29,7 +30,10 @@ const props = defineProps<{ :cache-headers="props.cacheHeaders" :enable-diff-on-hover="props.enableDiffOnHover" /> - +