Skip to content

Commit 7a197c2

Browse files
Copilotserhalp
andauthored
feat: add loading indicator for URL inspection requests (#63)
* Initial plan for issue * Initial exploration and linting fixes Co-authored-by: serhalp <[email protected]> * Add loading indicator for URL inspection requests Co-authored-by: serhalp <[email protected]> * Fix linting issues in test file Co-authored-by: serhalp <[email protected]> * Fix TypeScript errors and remove package-lock.json - Replace $fetch with native fetch to resolve TypeScript stack depth errors - Remove package-lock.json since project uses pnpm - Add jsdom environment directive to component tests - Fix linting issues (trailing spaces and indentation) Co-authored-by: serhalp <[email protected]> * Improve test descriptions to be more specific and descriptive Co-authored-by: serhalp <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: serhalp <[email protected]>
1 parent 938ce1a commit 7a197c2

File tree

4 files changed

+132
-17
lines changed

4 files changed

+132
-17
lines changed

app/components/RequestForm.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { describe, it, expect } from 'vitest'
5+
import { mount } from '@vue/test-utils'
6+
import RequestForm from './RequestForm.vue'
7+
8+
describe('RequestForm', () => {
9+
it('displays "Inspect" button text when not in loading state', () => {
10+
const wrapper = mount(RequestForm)
11+
expect(wrapper.find('button').text()).toBe('Inspect')
12+
})
13+
14+
it('displays "Inspecting..." button text when in loading state', () => {
15+
const wrapper = mount(RequestForm, {
16+
props: { loading: true },
17+
})
18+
expect(wrapper.find('button').text()).toBe('Inspecting...')
19+
})
20+
21+
it('disables button when loading', () => {
22+
const wrapper = mount(RequestForm, {
23+
props: { loading: true },
24+
})
25+
expect(wrapper.find('button').attributes('disabled')).toBeDefined()
26+
})
27+
28+
it('does not disable button when not loading', () => {
29+
const wrapper = mount(RequestForm)
30+
expect(wrapper.find('button').attributes('disabled')).toBeUndefined()
31+
})
32+
33+
it('emits submit event when not loading', async () => {
34+
const wrapper = mount(RequestForm)
35+
await wrapper.find('button').trigger('click')
36+
expect(wrapper.emitted('submit')).toBeTruthy()
37+
})
38+
39+
it('does not emit submit event when loading', async () => {
40+
const wrapper = mount(RequestForm, {
41+
props: { loading: true },
42+
})
43+
await wrapper.find('button').trigger('click')
44+
expect(wrapper.emitted('submit')).toBeFalsy()
45+
})
46+
})

app/components/RequestForm.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ const inputUrl = ref(
33
'https://nextjs-netlify-durable-cache-demo.netlify.app/isr-page',
44
)
55
6+
const props = defineProps<{
7+
loading?: boolean
8+
}>()
9+
610
const emit = defineEmits(['submit'])
711
812
const handleSubmit = () => {
13+
if (props.loading) return
14+
915
if (!inputUrl.value.startsWith('http')) {
1016
inputUrl.value = `https://${inputUrl.value}`
1117
}
@@ -23,8 +29,11 @@ const handleSubmit = () => {
2329
@keyup.enter="handleSubmit()"
2430
/>
2531
</label>
26-
<button @click="handleSubmit()">
27-
Inspect
32+
<button
33+
:disabled="props.loading"
34+
@click="handleSubmit()"
35+
>
36+
{{ props.loading ? 'Inspecting...' : 'Inspect' }}
2837
</button>
2938
</div>
3039
</template>

app/pages/index.vue

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface Run {
1010
1111
const runs = ref<Run[]>([])
1212
const error = ref<string | null>(null)
13+
const loading = ref<boolean>(false)
1314
1415
// TODO(serhalp) Improve types
1516
type ApiRun = Omit<Run, 'cacheHeaders'> & { headers: Record<string, string> }
@@ -23,14 +24,21 @@ const handleRequestFormSubmit = async ({
2324
}: {
2425
url: string
2526
}): Promise<void> => {
27+
loading.value = true
2628
try {
27-
const responseBody: ApiRun = await $fetch(
28-
'/api/inspect-url',
29-
{
30-
method: 'POST',
31-
body: { url },
29+
const response = await fetch('/api/inspect-url', {
30+
method: 'POST',
31+
headers: {
32+
'Content-Type': 'application/json',
3233
},
33-
)
34+
body: JSON.stringify({ url }),
35+
})
36+
37+
if (!response.ok) {
38+
throw new Error(`HTTP ${response.status}`)
39+
}
40+
41+
const responseBody: ApiRun = await response.json()
3442
runs.value.push(getRunFromApiRun(responseBody))
3543
error.value = null
3644
}
@@ -42,6 +50,9 @@ const handleRequestFormSubmit = async ({
4250
?? new Error(`Fetch error: ${err}`)
4351
return
4452
}
53+
finally {
54+
loading.value = false
55+
}
4556
}
4657
4758
const handleClickClear = (): void => {
@@ -51,7 +62,17 @@ const handleClickClear = (): void => {
5162

5263
<template>
5364
<main>
54-
<RequestForm @submit="handleRequestFormSubmit" />
65+
<RequestForm
66+
:loading="loading"
67+
@submit="handleRequestFormSubmit"
68+
/>
69+
70+
<div
71+
v-if="loading"
72+
class="loading-indicator"
73+
>
74+
⏳ Inspecting URL...
75+
</div>
5576

5677
<div
5778
v-if="error"
@@ -80,6 +101,13 @@ const handleClickClear = (): void => {
80101
</template>
81102

82103
<style scoped>
104+
.loading-indicator {
105+
text-align: center;
106+
padding: 1em;
107+
color: var(--blue-600, #2563eb);
108+
font-weight: 500;
109+
}
110+
83111
.error {
84112
color: var(--red-400);
85113
}

app/pages/run/[runId].vue

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface Run {
1010
1111
const runs = ref<Run[]>([])
1212
const error = ref<string | null>(null)
13+
const loading = ref<boolean>(false)
1314
1415
// TODO(serhalp) Improve types
1516
type ApiRun = Omit<Run, 'cacheHeaders'> & { headers: Record<string, string> }
@@ -23,7 +24,11 @@ const route = useRoute()
2324
const { data: initialRuns, pending: _pending, error: preloadedRunsError } = await useAsyncData('preloadedRuns', async (): Promise<Run[]> => {
2425
const { runId } = route.params
2526
if (typeof runId === 'string') {
26-
const responseBody: ApiRun = await $fetch(`/api/runs/${runId}`)
27+
const response = await fetch(`/api/runs/${runId}`)
28+
if (!response.ok) {
29+
throw new Error(`HTTP ${response.status}`)
30+
}
31+
const responseBody: ApiRun = await response.json()
2732
return [getRunFromApiRun(responseBody)]
2833
}
2934
return []
@@ -40,14 +45,21 @@ const handleRequestFormSubmit = async ({
4045
}: {
4146
url: string
4247
}): Promise<void> => {
48+
loading.value = true
4349
try {
44-
const responseBody: ApiRun = await $fetch(
45-
'/api/inspect-url',
46-
{
47-
method: 'POST',
48-
body: { url },
50+
const response = await fetch('/api/inspect-url', {
51+
method: 'POST',
52+
headers: {
53+
'Content-Type': 'application/json',
4954
},
50-
)
55+
body: JSON.stringify({ url }),
56+
})
57+
58+
if (!response.ok) {
59+
throw new Error(`HTTP ${response.status}`)
60+
}
61+
62+
const responseBody: ApiRun = await response.json()
5163
runs.value.push(getRunFromApiRun(responseBody))
5264
error.value = null
5365
}
@@ -59,6 +71,9 @@ const handleRequestFormSubmit = async ({
5971
?? new Error(`Fetch error: ${err}`)
6072
return
6173
}
74+
finally {
75+
loading.value = false
76+
}
6277
}
6378
6479
const handleClickClear = (): void => {
@@ -68,7 +83,17 @@ const handleClickClear = (): void => {
6883

6984
<template>
7085
<main>
71-
<RequestForm @submit="handleRequestFormSubmit" />
86+
<RequestForm
87+
:loading="loading"
88+
@submit="handleRequestFormSubmit"
89+
/>
90+
91+
<div
92+
v-if="loading"
93+
class="loading-indicator"
94+
>
95+
⏳ Inspecting URL...
96+
</div>
7297

7398
<div
7499
v-if="error"
@@ -97,6 +122,13 @@ const handleClickClear = (): void => {
97122
</template>
98123

99124
<style scoped>
125+
.loading-indicator {
126+
text-align: center;
127+
padding: 1em;
128+
color: var(--blue-600, #2563eb);
129+
font-weight: 500;
130+
}
131+
100132
.error {
101133
color: var(--red-400);
102134
}

0 commit comments

Comments
 (0)