Skip to content

Commit 2ad9c18

Browse files
Copilotserhalp
andauthored
refactor: extract duplicated code between index.vue and [runId].vue pages (#68)
* Initial plan for issue * Refactor: Extract duplicated code into composable and component Co-authored-by: serhalp <[email protected]> * Merge main and update refactored code with latest improvements - force push Co-authored-by: serhalp <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: serhalp <[email protected]>
1 parent d7aaa6e commit 2ad9c18

File tree

7 files changed

+432
-217
lines changed

7 files changed

+432
-217
lines changed

app/components/RunDisplay.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { describe, it, expect, vi } from 'vitest'
5+
import { mount } from '@vue/test-utils'
6+
import RunDisplay from './RunDisplay.vue'
7+
import type { Run } from '~/types/run'
8+
9+
// Mock the child components
10+
vi.mock('./RunPanel.vue', () => ({
11+
default: {
12+
name: 'RunPanel',
13+
template: '<div class="run-panel-mock">{{ runId }}</div>',
14+
props: ['runId', 'url', 'status', 'durationInMs', 'cacheHeaders'],
15+
},
16+
}))
17+
18+
describe('RunDisplay', () => {
19+
const mockRuns: Run[] = [
20+
{
21+
runId: 'test-run-1',
22+
url: 'https://example.com',
23+
status: 200,
24+
durationInMs: 100,
25+
cacheHeaders: {},
26+
},
27+
{
28+
runId: 'test-run-2',
29+
url: 'https://example2.com',
30+
status: 404,
31+
durationInMs: 200,
32+
cacheHeaders: {},
33+
},
34+
]
35+
36+
it('shows loading indicator when loading', () => {
37+
const wrapper = mount(RunDisplay, {
38+
props: {
39+
runs: [],
40+
error: null,
41+
loading: true,
42+
onClear: vi.fn(),
43+
},
44+
})
45+
46+
expect(wrapper.find('.loading-indicator').exists()).toBe(true)
47+
expect(wrapper.find('.loading-indicator').text()).toBe('⏳ Inspecting URL...')
48+
})
49+
50+
it('shows error message when error exists', () => {
51+
const errorMessage = 'Test error message'
52+
const wrapper = mount(RunDisplay, {
53+
props: {
54+
runs: [],
55+
error: errorMessage,
56+
loading: false,
57+
onClear: vi.fn(),
58+
},
59+
})
60+
61+
expect(wrapper.find('.error').exists()).toBe(true)
62+
expect(wrapper.find('.error').text()).toBe(errorMessage)
63+
})
64+
65+
it('renders run panels for each run', () => {
66+
const wrapper = mount(RunDisplay, {
67+
props: {
68+
runs: mockRuns,
69+
error: null,
70+
loading: false,
71+
onClear: vi.fn(),
72+
},
73+
})
74+
75+
const runPanels = wrapper.findAll('.run-panel-mock')
76+
expect(runPanels).toHaveLength(2)
77+
expect(runPanels[0]?.text()).toBe('test-run-1')
78+
expect(runPanels[1]?.text()).toBe('test-run-2')
79+
})
80+
81+
it('shows clear button when runs exist', () => {
82+
const mockOnClear = vi.fn()
83+
const wrapper = mount(RunDisplay, {
84+
props: {
85+
runs: mockRuns,
86+
error: null,
87+
loading: false,
88+
onClear: mockOnClear,
89+
},
90+
})
91+
92+
const clearButton = wrapper.find('button')
93+
expect(clearButton.exists()).toBe(true)
94+
expect(clearButton.text()).toBe('Clear')
95+
})
96+
97+
it('hides clear button when no runs exist', () => {
98+
const wrapper = mount(RunDisplay, {
99+
props: {
100+
runs: [],
101+
error: null,
102+
loading: false,
103+
onClear: vi.fn(),
104+
},
105+
})
106+
107+
expect(wrapper.find('button').exists()).toBe(false)
108+
})
109+
110+
it('calls onClear when clear button is clicked', async () => {
111+
const mockOnClear = vi.fn()
112+
const wrapper = mount(RunDisplay, {
113+
props: {
114+
runs: mockRuns,
115+
error: null,
116+
loading: false,
117+
onClear: mockOnClear,
118+
},
119+
})
120+
121+
await wrapper.find('button').trigger('click')
122+
expect(mockOnClear).toHaveBeenCalledOnce()
123+
})
124+
})

app/components/RunDisplay.vue

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<script setup lang="ts">
2+
import type { Run } from '~/types/run'
3+
4+
defineProps<{
5+
runs: readonly Run[]
6+
error: string | null
7+
loading: boolean
8+
onClear: () => void
9+
}>()
10+
</script>
11+
12+
<template>
13+
<div>
14+
<div
15+
v-if="loading"
16+
class="loading-indicator"
17+
>
18+
⏳ Inspecting URL...
19+
</div>
20+
21+
<div
22+
v-if="error"
23+
class="error"
24+
>
25+
{{ error }}
26+
</div>
27+
28+
<div class="flex-btwn run-panels">
29+
<RunPanel
30+
v-for="(run, i) in runs"
31+
v-bind="run"
32+
:key="i"
33+
:enable-diff-on-hover="runs.length > 1"
34+
/>
35+
</div>
36+
37+
<div class="reset-container">
38+
<button
39+
v-if="runs.length > 0"
40+
@click="onClear()"
41+
>
42+
Clear
43+
</button>
44+
</div>
45+
</div>
46+
</template>
47+
48+
<style scoped>
49+
.loading-indicator {
50+
text-align: center;
51+
padding: 1em;
52+
color: var(--blue-600, #2563eb);
53+
font-weight: 500;
54+
}
55+
56+
.error {
57+
color: var(--red-400);
58+
}
59+
60+
.run-panels {
61+
flex-wrap: wrap;
62+
align-items: stretch;
63+
}
64+
65+
.run-panels>* {
66+
flex: 1 1 20em;
67+
}
68+
69+
.reset-container {
70+
text-align: center;
71+
background-color: inherit;
72+
}
73+
</style>

app/composables/useRunManager.test.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { describe, it, expect, vi, beforeEach } from 'vitest'
5+
import { useRunManager } from './useRunManager'
6+
import type { ApiRun } from '~/types/run'
7+
8+
// Mock the getCacheHeaders function
9+
vi.mock('~/utils/getCacheHeaders', () => ({
10+
default: vi.fn((headers: Record<string, string>) => headers),
11+
}))
12+
13+
// Mock fetch and $fetch
14+
global.fetch = vi.fn()
15+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
16+
global.$fetch = vi.fn() as any
17+
18+
describe('useRunManager', () => {
19+
beforeEach(() => {
20+
vi.clearAllMocks()
21+
})
22+
23+
it('initializes with empty state', () => {
24+
const { runs, error, loading } = useRunManager()
25+
26+
expect(runs.value).toEqual([])
27+
expect(error.value).toBe(null)
28+
expect(loading.value).toBe(false)
29+
})
30+
31+
it('transforms ApiRun to Run correctly', () => {
32+
const { getRunFromApiRun } = useRunManager()
33+
34+
const apiRun: ApiRun = {
35+
runId: 'test-run',
36+
url: 'https://example.com',
37+
status: 200,
38+
durationInMs: 100,
39+
headers: { 'cache-control': 'max-age=3600' },
40+
}
41+
42+
const run = getRunFromApiRun(apiRun)
43+
44+
expect(run).toEqual({
45+
runId: 'test-run',
46+
url: 'https://example.com',
47+
status: 200,
48+
durationInMs: 100,
49+
cacheHeaders: { 'cache-control': 'max-age=3600' },
50+
})
51+
})
52+
53+
it('handles successful API request', async () => {
54+
const mockApiRun: ApiRun = {
55+
runId: 'test-run',
56+
url: 'https://example.com',
57+
status: 200,
58+
durationInMs: 100,
59+
headers: { 'cache-control': 'max-age=3600' },
60+
}
61+
62+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
63+
const mockFetch = vi.mocked($fetch as any)
64+
mockFetch.mockResolvedValueOnce(mockApiRun)
65+
66+
const { runs, error, loading, handleRequestFormSubmit } = useRunManager()
67+
68+
await handleRequestFormSubmit({ url: 'https://example.com' })
69+
70+
expect(loading.value).toBe(false)
71+
expect(error.value).toBe(null)
72+
expect(runs.value).toHaveLength(1)
73+
expect(runs.value[0]?.url).toBe('https://example.com')
74+
expect(mockFetch).toHaveBeenCalledWith('/api/inspect-url', {
75+
method: 'POST',
76+
body: { url: 'https://example.com' },
77+
})
78+
})
79+
80+
it('handles API request error', async () => {
81+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
82+
const mockFetch = vi.mocked($fetch as any)
83+
mockFetch.mockRejectedValueOnce(new Error('HTTP 500'))
84+
85+
const { runs, error, loading, handleRequestFormSubmit } = useRunManager()
86+
87+
await handleRequestFormSubmit({ url: 'https://example.com' })
88+
89+
expect(loading.value).toBe(false)
90+
expect(error.value).toBeTruthy()
91+
expect(runs.value).toHaveLength(0)
92+
})
93+
94+
it('clears runs when handleClickClear is called', () => {
95+
const { runs, setRuns, handleClickClear } = useRunManager()
96+
97+
// Add some runs first
98+
setRuns([{
99+
runId: 'test-run',
100+
url: 'https://example.com',
101+
status: 200,
102+
durationInMs: 100,
103+
cacheHeaders: {},
104+
}])
105+
106+
expect(runs.value).toHaveLength(1)
107+
108+
handleClickClear()
109+
110+
expect(runs.value).toHaveLength(0)
111+
})
112+
113+
it('adds runs with addRun', () => {
114+
const { runs, addRun } = useRunManager()
115+
116+
const run = {
117+
runId: 'test-run',
118+
url: 'https://example.com',
119+
status: 200,
120+
durationInMs: 100,
121+
cacheHeaders: {},
122+
}
123+
124+
addRun(run)
125+
126+
expect(runs.value).toHaveLength(1)
127+
expect(runs.value[0]).toEqual(run)
128+
})
129+
130+
it('sets error with setError', () => {
131+
const { error, setError } = useRunManager()
132+
133+
setError('Test error')
134+
135+
expect(error.value).toBe('Test error')
136+
})
137+
})

0 commit comments

Comments
 (0)