Skip to content

Commit e93af17

Browse files
Copilotserhalp
andauthored
feat: add toggle switch below permalink to show/hide raw cache headers (#89)
* Initial plan * Initial analysis: Add toggle button for raw headers Co-authored-by: serhalp <[email protected]> * Implement toggle button for raw headers with tests Co-authored-by: serhalp <[email protected]> * Fix linting issues in RunPanel test Co-authored-by: serhalp <[email protected]> * Refactor raw headers toggle to global control Co-authored-by: serhalp <[email protected]> * Improve toggle design with grouped controls and better styling Co-authored-by: serhalp <[email protected]> * Move toggle from global to per-panel scope Co-authored-by: serhalp <[email protected]> * Position toggle inline with permalink using static label Co-authored-by: serhalp <[email protected]> * Fix CSS selector specificity for toggle hover states Co-authored-by: serhalp <[email protected]> * Position toggle below permalink for better responsiveness Co-authored-by: serhalp <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: serhalp <[email protected]> Co-authored-by: Philippe Serhal <[email protected]>
1 parent fd08de2 commit e93af17

File tree

3 files changed

+278
-3
lines changed

3 files changed

+278
-3
lines changed

app/components/RunDisplay.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ vi.mock('./RunPanel.vue', () => ({
1111
default: {
1212
name: 'RunPanel',
1313
template: '<div class="run-panel-mock">{{ runId }}</div>',
14-
props: ['runId', 'url', 'status', 'durationInMs', 'cacheHeaders'],
14+
props: ['runId', 'url', 'status', 'durationInMs', 'cacheHeaders', 'enableDiffOnHover'],
1515
},
1616
}))
1717

app/components/RunPanel.test.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { describe, it, expect, vi } from 'vitest'
5+
import { mount } from '@vue/test-utils'
6+
import RunPanel from './RunPanel.vue'
7+
8+
// Mock the components that RunPanel uses
9+
vi.mock('./CacheAnalysis.vue', () => ({
10+
default: {
11+
name: 'CacheAnalysis',
12+
template: '<div class="cache-analysis-mock">Cache Analysis</div>',
13+
},
14+
}))
15+
16+
vi.mock('./RawCacheHeaders.vue', () => ({
17+
default: {
18+
name: 'RawCacheHeaders',
19+
template: '<div class="raw-cache-headers-mock">Raw Cache Headers</div>',
20+
},
21+
}))
22+
23+
const mockProps = {
24+
runId: 'test-run-id',
25+
url: 'https://example.com',
26+
status: 200,
27+
durationInMs: 150,
28+
cacheHeaders: {
29+
'cache-control': 'max-age=3600',
30+
'etag': '"abc123"',
31+
},
32+
enableDiffOnHover: false,
33+
}
34+
35+
describe('RunPanel', () => {
36+
it('renders basic information correctly', () => {
37+
const wrapper = mount(RunPanel, {
38+
props: mockProps,
39+
global: {
40+
stubs: {
41+
NuxtLink: {
42+
template: '<a :href="to"><slot /></a>',
43+
props: ['to'],
44+
},
45+
},
46+
},
47+
})
48+
49+
expect(wrapper.text()).toContain('https://example.com')
50+
expect(wrapper.text()).toContain('HTTP 200 (150 ms)')
51+
expect(wrapper.find('.cache-analysis-mock').exists()).toBe(true)
52+
})
53+
54+
it('renders permalink correctly', () => {
55+
const wrapper = mount(RunPanel, {
56+
props: mockProps,
57+
global: {
58+
stubs: {
59+
NuxtLink: {
60+
template: '<a :href="to"><slot /></a>',
61+
props: ['to'],
62+
},
63+
},
64+
},
65+
})
66+
67+
const permalink = wrapper.find('.run-permalink')
68+
expect(permalink.exists()).toBe(true)
69+
expect(permalink.attributes('href')).toBe('/run/test-run-id')
70+
expect(permalink.text()).toBe('🔗 Permalink')
71+
})
72+
73+
it('renders toggle control with static label', () => {
74+
const wrapper = mount(RunPanel, {
75+
props: mockProps,
76+
global: {
77+
stubs: {
78+
NuxtLink: {
79+
template: '<a :href="to"><slot /></a>',
80+
props: ['to'],
81+
},
82+
},
83+
},
84+
})
85+
86+
const toggleControl = wrapper.find('.toggle-control')
87+
expect(toggleControl.exists()).toBe(true)
88+
expect(toggleControl.text()).toBe('Show raw headers')
89+
90+
const toggleLabel = wrapper.find('.toggle-label')
91+
expect(toggleLabel.exists()).toBe(true)
92+
expect(toggleLabel.text()).toBe('Show raw headers')
93+
})
94+
95+
it('hides raw headers by default', () => {
96+
const wrapper = mount(RunPanel, {
97+
props: mockProps,
98+
global: {
99+
stubs: {
100+
NuxtLink: {
101+
template: '<a :href="to"><slot /></a>',
102+
props: ['to'],
103+
},
104+
},
105+
},
106+
})
107+
108+
expect(wrapper.find('.raw-cache-headers-mock').exists()).toBe(false)
109+
const checkbox = wrapper.find('input[type="checkbox"]')
110+
expect((checkbox.element as HTMLInputElement).checked).toBe(false)
111+
})
112+
113+
it('toggles raw headers when checkbox is toggled', async () => {
114+
const wrapper = mount(RunPanel, {
115+
props: mockProps,
116+
global: {
117+
stubs: {
118+
NuxtLink: {
119+
template: '<a :href="to"><slot /></a>',
120+
props: ['to'],
121+
},
122+
},
123+
},
124+
})
125+
126+
const checkbox = wrapper.find('input[type="checkbox"]')
127+
128+
// Initially hidden
129+
expect(wrapper.find('.raw-cache-headers-mock').exists()).toBe(false)
130+
expect((checkbox.element as HTMLInputElement).checked).toBe(false)
131+
132+
// Check to show
133+
await checkbox.setValue(true)
134+
expect(wrapper.find('.raw-cache-headers-mock').exists()).toBe(true)
135+
expect((checkbox.element as HTMLInputElement).checked).toBe(true)
136+
137+
// Uncheck to hide
138+
await checkbox.setValue(false)
139+
expect(wrapper.find('.raw-cache-headers-mock').exists()).toBe(false)
140+
expect((checkbox.element as HTMLInputElement).checked).toBe(false)
141+
})
142+
143+
it('has proper accessibility attributes', () => {
144+
const wrapper = mount(RunPanel, {
145+
props: mockProps,
146+
global: {
147+
stubs: {
148+
NuxtLink: {
149+
template: '<a :href="to"><slot /></a>',
150+
props: ['to'],
151+
},
152+
},
153+
},
154+
})
155+
156+
const checkbox = wrapper.find('input[type="checkbox"]')
157+
expect(checkbox.attributes('aria-label')).toContain('Show raw headers for')
158+
expect(checkbox.attributes('aria-label')).toContain('https://example.com')
159+
})
160+
161+
it('label text remains static regardless of toggle state', async () => {
162+
const wrapper = mount(RunPanel, {
163+
props: mockProps,
164+
global: {
165+
stubs: {
166+
NuxtLink: {
167+
template: '<a :href="to"><slot /></a>',
168+
props: ['to'],
169+
},
170+
},
171+
},
172+
})
173+
174+
const toggleLabel = wrapper.find('.toggle-label')
175+
expect(toggleLabel.text()).toBe('Show raw headers')
176+
177+
// Toggle the checkbox
178+
const checkbox = wrapper.find('input[type="checkbox"]')
179+
await checkbox.setValue(true)
180+
181+
// Label should remain the same
182+
expect(toggleLabel.text()).toBe('Show raw headers')
183+
})
184+
})

app/components/RunPanel.vue

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const props = defineProps<{
77
cacheHeaders: Record<string, string>
88
enableDiffOnHover: boolean
99
}>()
10+
11+
const showRawHeaders = ref(false)
1012
</script>
1113

1214
<template>
@@ -25,11 +27,27 @@ const props = defineProps<{
2527
</NuxtLink>
2628
</div>
2729

30+
<div class="toggle-container">
31+
<label class="toggle-control">
32+
<input
33+
v-model="showRawHeaders"
34+
type="checkbox"
35+
class="sr-only"
36+
:aria-label="'Show raw headers for ' + props.url"
37+
/>
38+
<span class="toggle-switch" />
39+
<span class="toggle-label">Show raw headers</span>
40+
</label>
41+
</div>
42+
2843
<CacheAnalysis
2944
:cache-headers="props.cacheHeaders"
3045
:enable-diff-on-hover="props.enableDiffOnHover"
3146
/>
32-
<RawCacheHeaders :cache-headers="props.cacheHeaders" />
47+
<RawCacheHeaders
48+
v-if="showRawHeaders"
49+
:cache-headers="props.cacheHeaders"
50+
/>
3351
</div>
3452
</template>
3553

@@ -49,7 +67,80 @@ const props = defineProps<{
4967
}
5068
5169
.run-permalink {
52-
align-self: flex-end;
5370
font-size: 0.7em;
5471
}
72+
73+
.toggle-container {
74+
display: flex;
75+
justify-content: flex-end;
76+
margin-top: 0.25rem;
77+
margin-bottom: 0.5rem;
78+
}
79+
80+
.toggle-control {
81+
display: flex;
82+
align-items: center;
83+
gap: 0.5rem;
84+
cursor: pointer;
85+
user-select: none;
86+
font-size: 0.7em;
87+
}
88+
89+
.sr-only {
90+
position: absolute;
91+
width: 1px;
92+
height: 1px;
93+
padding: 0;
94+
margin: -1px;
95+
overflow: hidden;
96+
clip: rect(0, 0, 0, 0);
97+
white-space: nowrap;
98+
border: 0;
99+
}
100+
101+
.toggle-switch {
102+
position: relative;
103+
display: inline-block;
104+
width: 2rem;
105+
height: 1.125rem;
106+
background-color: #cbd5e1;
107+
border-radius: 0.5625rem;
108+
transition: background-color 0.2s ease;
109+
flex-shrink: 0;
110+
}
111+
112+
.toggle-switch::after {
113+
content: '';
114+
position: absolute;
115+
top: 0.125rem;
116+
left: 0.125rem;
117+
width: 0.875rem;
118+
height: 0.875rem;
119+
background-color: white;
120+
border-radius: 50%;
121+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3);
122+
transition: transform 0.2s ease;
123+
}
124+
125+
input:checked + .toggle-switch {
126+
background-color: #3b82f6;
127+
}
128+
129+
input:checked + .toggle-switch::after {
130+
transform: translateX(0.875rem);
131+
}
132+
133+
.toggle-control:hover .toggle-switch {
134+
background-color: #94a3b8;
135+
}
136+
137+
.toggle-control:hover input:checked + .toggle-switch {
138+
background-color: #2563eb;
139+
}
140+
141+
.toggle-label {
142+
font-weight: 500;
143+
color: #64748b;
144+
white-space: nowrap;
145+
}
55146
</style>

0 commit comments

Comments
 (0)