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)
@@ -25,23 +27,37 @@ defineProps<{
{{ error }}
+
+
+
+
+
+
-
-
-
-
@@ -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: '',
+ },
+}))
+
+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"
/>
-
+