feat: extract VehicleInfo component from HomePage
Move vehicle-info display logic out of HomePage into a reusable VehicleInfo component. The component accepts vehicle, loading, notFound, and plate props and renders the correct state with priority: vehicle card > loading > not found. Follows the small-page-component pattern from CODING_GUIDELINES.md. - Create VehicleInfo.vue with 3-state v-if chain and scoped styles - Define and export VehicleInfo interface (make/model/year/color) - Add VehicleInfo.spec.ts with 7 tests covering all states and priority edge cases - Update HomePage.vue to use VehicleInfo, replacing 3 inline v-if/else-if blocks with a single component tag - Remove 5 unused CSS classes from HomePage (home__status, home__vehicle, home__vehicle-text, home__not-found, home__not-found p) - Update AGENTS.md to require thorough commit messages with bullet points
This commit is contained in:
parent
078f07f2ac
commit
210ac87ede
4 changed files with 152 additions and 53 deletions
|
|
@ -126,6 +126,9 @@ Full details in `@CODING_GUIDELINES.md`. Key rules:
|
|||
- Create `feature/*`, `fix/*`, or `chore/*` branches from `develop`.
|
||||
- Never commit directly to `master` or `develop`.
|
||||
- Merge strategy: fast-forward or merge — either is fine.
|
||||
- Commit messages must be thorough: describe what changed, why, and
|
||||
list concrete changes as bullet points. Never write single-line
|
||||
"feat: add X" messages.
|
||||
|
||||
### Frontend (Vue.js 3)
|
||||
- `<script setup>` with Composition API only. Never Options API.
|
||||
|
|
|
|||
74
frontend/src/__tests__/VehicleInfo.spec.ts
Normal file
74
frontend/src/__tests__/VehicleInfo.spec.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import VehicleInfo from '@/components/VehicleInfo.vue'
|
||||
import type { VehicleInfo as VehicleData } from '@/components/VehicleInfo.vue'
|
||||
|
||||
const mockVehicle: VehicleData = {
|
||||
make: 'Volvo',
|
||||
model: 'V70',
|
||||
year: 2009,
|
||||
color: 'Silver',
|
||||
}
|
||||
|
||||
function createWrapper(props: Record<string, unknown> = {}) {
|
||||
return mount(VehicleInfo, {
|
||||
props: {
|
||||
vehicle: null,
|
||||
loading: false,
|
||||
notFound: false,
|
||||
plate: '',
|
||||
...props,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('VehicleInfo', () => {
|
||||
it('shows loading text when loading is true', () => {
|
||||
const wrapper = createWrapper({ loading: true })
|
||||
expect(wrapper.text()).toContain('Söker...')
|
||||
})
|
||||
|
||||
it('shows vehicle card with make, model, year, and color', () => {
|
||||
const wrapper = createWrapper({ vehicle: mockVehicle })
|
||||
expect(wrapper.text()).toContain('Volvo')
|
||||
expect(wrapper.text()).toContain('V70')
|
||||
expect(wrapper.text()).toContain('2009')
|
||||
expect(wrapper.text()).toContain('Silver')
|
||||
})
|
||||
|
||||
it('shows not-found message when notFound is true', () => {
|
||||
const wrapper = createWrapper({ notFound: true, plate: 'ABC123' })
|
||||
expect(wrapper.text()).toContain('Inget fordon hittades')
|
||||
})
|
||||
|
||||
it('renders nothing in initial state', () => {
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.find('.vehicle-info').exists()).toBe(true)
|
||||
expect(wrapper.text().replace(/\s/g, '')).toBe('')
|
||||
})
|
||||
|
||||
it('prioritizes loading over notFound', () => {
|
||||
const wrapper = createWrapper({
|
||||
loading: true,
|
||||
notFound: true,
|
||||
plate: 'ABC123',
|
||||
})
|
||||
expect(wrapper.text()).toContain('Söker...')
|
||||
expect(wrapper.text()).not.toContain('Inget fordon hittades')
|
||||
})
|
||||
|
||||
it('prioritizes vehicle over loading', () => {
|
||||
const wrapper = createWrapper({ vehicle: mockVehicle, loading: true })
|
||||
expect(wrapper.text()).toContain('Volvo')
|
||||
expect(wrapper.text()).not.toContain('Söker...')
|
||||
})
|
||||
|
||||
it('does not render vehicle card when vehicle is null and not found', () => {
|
||||
const wrapper = createWrapper({
|
||||
vehicle: null,
|
||||
notFound: false,
|
||||
loading: false,
|
||||
})
|
||||
expect(wrapper.text().replace(/\s/g, '')).toBe('')
|
||||
})
|
||||
})
|
||||
65
frontend/src/components/VehicleInfo.vue
Normal file
65
frontend/src/components/VehicleInfo.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<script setup lang="ts">
|
||||
export interface VehicleInfo {
|
||||
make: string
|
||||
model: string
|
||||
year: number
|
||||
color: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
vehicle: VehicleInfo | null
|
||||
loading: boolean
|
||||
notFound: boolean
|
||||
plate: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vehicle-info">
|
||||
<div v-if="vehicle" class="vehicle-info__card">
|
||||
<p class="vehicle-info__card-text">
|
||||
{{ vehicle.make }} {{ vehicle.model }} ({{ vehicle.year }}) —
|
||||
{{ vehicle.color }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="loading" class="vehicle-info__loading">Söker...</div>
|
||||
<div v-else-if="notFound" class="vehicle-info__not-found">
|
||||
<p>Inget fordon hittades</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vehicle-info {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.vehicle-info__loading {
|
||||
color: #718096;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.vehicle-info__card {
|
||||
padding: 1rem;
|
||||
background: #f0fff4;
|
||||
border: 1px solid #c6f6d5;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.vehicle-info__card-text {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vehicle-info__not-found {
|
||||
padding: 1rem;
|
||||
background: #fffaf0;
|
||||
border: 1px solid #feebc8;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.vehicle-info__not-found p {
|
||||
margin: 0;
|
||||
color: #c05621;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,22 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import PlateInput from '@/components/PlateInput.vue'
|
||||
import VehicleInfo from '@/components/VehicleInfo.vue'
|
||||
import type { VehicleInfo as VehicleData } from '@/components/VehicleInfo.vue'
|
||||
|
||||
interface VehicleInfo {
|
||||
make: string
|
||||
model: string
|
||||
year: number
|
||||
color: string
|
||||
}
|
||||
|
||||
const FAKE_VEHICLES: Record<string, VehicleInfo> = {
|
||||
const FAKE_VEHICLES: Record<string, VehicleData> = {
|
||||
ABC123: { make: 'Volvo', model: 'V70', year: 2009, color: 'Silver' },
|
||||
ABC12D: { make: 'Volkswagen', model: 'Golf', year: 2020, color: 'Blå' },
|
||||
XYZ789: { make: 'Saab', model: '9-3', year: 2005, color: 'Röd' },
|
||||
}
|
||||
|
||||
const plate = ref('')
|
||||
const vehicle = ref<VehicleInfo | null>(null)
|
||||
const vehicle = ref<VehicleData | null>(null)
|
||||
const notFound = ref(false)
|
||||
const lookingUp = ref(false)
|
||||
|
||||
|
|
@ -46,18 +41,12 @@ function handleLookup(lookedUpPlate: string) {
|
|||
|
||||
<PlateInput v-model="plate" @lookup="handleLookup" />
|
||||
|
||||
<div v-if="lookingUp" class="home__status">Söker...</div>
|
||||
|
||||
<div v-else-if="vehicle" class="home__vehicle">
|
||||
<p class="home__vehicle-text">
|
||||
{{ vehicle.make }} {{ vehicle.model }} ({{ vehicle.year }}) —
|
||||
{{ vehicle.color }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="notFound" class="home__not-found">
|
||||
<p>Inget fordon hittades för {{ plate }}</p>
|
||||
</div>
|
||||
<VehicleInfo
|
||||
:vehicle="vehicle"
|
||||
:loading="lookingUp"
|
||||
:not-found="notFound"
|
||||
:plate="plate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -72,36 +61,4 @@ function handleLookup(lookedUpPlate: string) {
|
|||
color: #718096;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.home__status {
|
||||
margin-top: 0.75rem;
|
||||
color: #718096;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.home__vehicle {
|
||||
margin-top: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: #f0fff4;
|
||||
border: 1px solid #c6f6d5;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.home__vehicle-text {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.home__not-found {
|
||||
margin-top: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: #fffaf0;
|
||||
border: 1px solid #feebc8;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.home__not-found p {
|
||||
margin: 0;
|
||||
color: #c05621;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue