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`.
|
- Create `feature/*`, `fix/*`, or `chore/*` branches from `develop`.
|
||||||
- Never commit directly to `master` or `develop`.
|
- Never commit directly to `master` or `develop`.
|
||||||
- Merge strategy: fast-forward or merge — either is fine.
|
- 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)
|
### Frontend (Vue.js 3)
|
||||||
- `<script setup>` with Composition API only. Never Options API.
|
- `<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">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import PlateInput from '@/components/PlateInput.vue'
|
import PlateInput from '@/components/PlateInput.vue'
|
||||||
|
import VehicleInfo from '@/components/VehicleInfo.vue'
|
||||||
|
import type { VehicleInfo as VehicleData } from '@/components/VehicleInfo.vue'
|
||||||
|
|
||||||
interface VehicleInfo {
|
const FAKE_VEHICLES: Record<string, VehicleData> = {
|
||||||
make: string
|
|
||||||
model: string
|
|
||||||
year: number
|
|
||||||
color: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const FAKE_VEHICLES: Record<string, VehicleInfo> = {
|
|
||||||
ABC123: { make: 'Volvo', model: 'V70', year: 2009, color: 'Silver' },
|
ABC123: { make: 'Volvo', model: 'V70', year: 2009, color: 'Silver' },
|
||||||
ABC12D: { make: 'Volkswagen', model: 'Golf', year: 2020, color: 'Blå' },
|
ABC12D: { make: 'Volkswagen', model: 'Golf', year: 2020, color: 'Blå' },
|
||||||
XYZ789: { make: 'Saab', model: '9-3', year: 2005, color: 'Röd' },
|
XYZ789: { make: 'Saab', model: '9-3', year: 2005, color: 'Röd' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const plate = ref('')
|
const plate = ref('')
|
||||||
const vehicle = ref<VehicleInfo | null>(null)
|
const vehicle = ref<VehicleData | null>(null)
|
||||||
const notFound = ref(false)
|
const notFound = ref(false)
|
||||||
const lookingUp = ref(false)
|
const lookingUp = ref(false)
|
||||||
|
|
||||||
|
|
@ -46,18 +41,12 @@ function handleLookup(lookedUpPlate: string) {
|
||||||
|
|
||||||
<PlateInput v-model="plate" @lookup="handleLookup" />
|
<PlateInput v-model="plate" @lookup="handleLookup" />
|
||||||
|
|
||||||
<div v-if="lookingUp" class="home__status">Söker...</div>
|
<VehicleInfo
|
||||||
|
:vehicle="vehicle"
|
||||||
<div v-else-if="vehicle" class="home__vehicle">
|
:loading="lookingUp"
|
||||||
<p class="home__vehicle-text">
|
:not-found="notFound"
|
||||||
{{ vehicle.make }} {{ vehicle.model }} ({{ vehicle.year }}) —
|
:plate="plate"
|
||||||
{{ vehicle.color }}
|
/>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="notFound" class="home__not-found">
|
|
||||||
<p>Inget fordon hittades för {{ plate }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -72,36 +61,4 @@ function handleLookup(lookedUpPlate: string) {
|
||||||
color: #718096;
|
color: #718096;
|
||||||
margin: 0 0 1.5rem 0;
|
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>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue