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:
Joakim Mörling 2026-05-01 18:06:04 +02:00
parent 078f07f2ac
commit 210ac87ede
4 changed files with 152 additions and 53 deletions

View file

@ -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.

View 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('')
})
})

View 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 }}) &mdash;
{{ 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>

View file

@ -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 }}) &mdash;
{{ 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>