Compare commits
No commits in common. "c7d443f2361de70e3170c57ca6b80cbafae1736a" and "210ac87ede81c8e742b0dfcbeeafd3d7cb4fc16c" have entirely different histories.
c7d443f236
...
210ac87ede
25 changed files with 25 additions and 548 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -10,7 +10,6 @@ target/
|
||||||
*.jar
|
*.jar
|
||||||
*.war
|
*.war
|
||||||
!.mvn/wrapper/maven-wrapper.jar
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
|
|
@ -40,10 +39,6 @@ Thumbs.db
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
certs/
|
certs/
|
||||||
|
|
||||||
# Gradle
|
|
||||||
.gradle/
|
|
||||||
build/
|
|
||||||
|
|
||||||
# Java
|
# Java
|
||||||
*.hprof
|
*.hprof
|
||||||
|
|
||||||
|
|
|
||||||
28
AGENTS.md
28
AGENTS.md
|
|
@ -24,23 +24,11 @@ PostgreSQL 16. Deployed via Docker Compose.
|
||||||
|
|
||||||
Always run these after making changes to verify nothing is broken.
|
Always run these after making changes to verify nothing is broken.
|
||||||
|
|
||||||
Gradle lives at repo root. All commands below run from the repo root unless noted.
|
|
||||||
|
|
||||||
### Quick start (everything)
|
### Quick start (everything)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # first time only, then fill in keys
|
cp .env.example .env # first time only, then fill in keys
|
||||||
docker compose up -d # starts postgres, backend, frontend
|
docker compose up -d # starts postgres, backend, frontend
|
||||||
./gradlew up # same as above (Gradle wrapper)
|
|
||||||
```
|
|
||||||
|
|
||||||
### All-in-one
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew check # frontend lint → frontend test → backend test → integration test
|
|
||||||
./gradlew up # docker compose up -d
|
|
||||||
./gradlew down # docker compose down
|
|
||||||
./gradlew reset # docker compose down -v && docker compose up -d (full DB reset)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend (Vue.js 3 + Vite)
|
### Frontend (Vue.js 3 + Vite)
|
||||||
|
|
@ -57,8 +45,10 @@ npm run test # vitest
|
||||||
### Backend (Spring Boot 4 + Java 21)
|
### Backend (Spring Boot 4 + Java 21)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew :backend:bootRun # dev server on :8080
|
cd backend
|
||||||
./gradlew :backend:test # JUnit 5 + Mockito (backend only)
|
./gradlew bootRun # dev server on :8080
|
||||||
|
./gradlew test # JUnit 5 + Mockito
|
||||||
|
./gradlew check # full verification including integration tests
|
||||||
```
|
```
|
||||||
|
|
||||||
### Stripe webhooks (local testing)
|
### Stripe webhooks (local testing)
|
||||||
|
|
@ -90,8 +80,7 @@ bilhej/
|
||||||
│ │ ├── router/ # Vue Router config
|
│ │ ├── router/ # Vue Router config
|
||||||
│ │ └── assets/ # Static files, CSS
|
│ │ └── assets/ # Static files, CSS
|
||||||
│ └── ...
|
│ └── ...
|
||||||
├── backend/ # Spring Boot 4 (Java 21) — Gradle subproject
|
├── backend/ # Spring Boot 4 (Java 21)
|
||||||
│ ├── build.gradle # Spring Boot plugin, Java deps, test config
|
|
||||||
│ ├── src/main/java/se/bilhalsning/
|
│ ├── src/main/java/se/bilhalsning/
|
||||||
│ │ ├── config/ # @Configuration classes
|
│ │ ├── config/ # @Configuration classes
|
||||||
│ │ ├── controller/ # REST controllers
|
│ │ ├── controller/ # REST controllers
|
||||||
|
|
@ -107,7 +96,7 @@ bilhej/
|
||||||
│ ├── application-docker.yml # docker profile (PostgreSQL)
|
│ ├── application-docker.yml # docker profile (PostgreSQL)
|
||||||
│ └── db/migration/ # Flyway migrations
|
│ └── db/migration/ # Flyway migrations
|
||||||
├── docker/ # Dockerfiles
|
├── docker/ # Dockerfiles
|
||||||
│ ├── backend.Dockerfile # dev: JDK 21 + gradle :backend:bootRun
|
│ ├── backend.Dockerfile # dev: JDK 21 + gradle bootRun
|
||||||
│ ├── backend.prod.Dockerfile # prod: multi-stage (Gradle build → JRE Alpine, non-root)
|
│ ├── backend.prod.Dockerfile # prod: multi-stage (Gradle build → JRE Alpine, non-root)
|
||||||
│ ├── frontend.Dockerfile # dev: Node 24 + vite dev server
|
│ ├── frontend.Dockerfile # dev: Node 24 + vite dev server
|
||||||
│ ├── frontend.prod.Dockerfile # prod: multi-stage (Node build → nginx)
|
│ ├── frontend.prod.Dockerfile # prod: multi-stage (Node build → nginx)
|
||||||
|
|
@ -115,11 +104,6 @@ bilhej/
|
||||||
│ └── entrypoint.sh # prod: self-signed cert generation
|
│ └── entrypoint.sh # prod: self-signed cert generation
|
||||||
├── docker-compose.yml # dev: postgres + backend (bootRun) + frontend (Vite HMR)
|
├── docker-compose.yml # dev: postgres + backend (bootRun) + frontend (Vite HMR)
|
||||||
├── docker-compose.prod.yml # prod: multi-stage images, no source mounts, restart always
|
├── docker-compose.prod.yml # prod: multi-stage images, no source mounts, restart always
|
||||||
├── gradlew # Gradle wrapper (repo root)
|
|
||||||
├── gradle/
|
|
||||||
│ └── wrapper/
|
|
||||||
├── settings.gradle # rootProject.name + include 'backend'
|
|
||||||
├── build.gradle # convenience tasks: check, up, down, reset
|
|
||||||
├── .env.example
|
├── .env.example
|
||||||
├── AGENTS.md # This file
|
├── AGENTS.md # This file
|
||||||
├── README.md
|
├── README.md
|
||||||
|
|
|
||||||
39
README.md
39
README.md
|
|
@ -34,7 +34,7 @@ The user enters a registration number, composes a letter (from a template or fre
|
||||||
git clone <repo-url> bilhej
|
git clone <repo-url> bilhej
|
||||||
cd bilhej
|
cd bilhej
|
||||||
cp .env.example .env # fill in your keys
|
cp .env.example .env # fill in your keys
|
||||||
docker compose up -d # or: ./gradlew up
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
The app will be available at:
|
The app will be available at:
|
||||||
|
|
@ -72,7 +72,7 @@ No CORS configuration needed in development — the browser never calls the back
|
||||||
**Spring profiles:**
|
**Spring profiles:**
|
||||||
| Profile | Datasource | Use |
|
| Profile | Datasource | Use |
|
||||||
|---------|-----------|-----|
|
|---------|-----------|-----|
|
||||||
| default | H2 in-memory | Local IDE dev (`./gradlew :backend:bootRun`) |
|
| default | H2 in-memory | Local IDE dev (`./gradlew bootRun`) |
|
||||||
| `docker` | PostgreSQL via `docker-compose.yml` | Docker Compose dev |
|
| `docker` | PostgreSQL via `docker-compose.yml` | Docker Compose dev |
|
||||||
| `prod` | PostgreSQL (production config) | Deploy (`docker-compose.prod.yml`) |
|
| `prod` | PostgreSQL (production config) | Deploy (`docker-compose.prod.yml`) |
|
||||||
|
|
||||||
|
|
@ -130,21 +130,17 @@ bilhej/
|
||||||
├── docker-compose.yml # dev: postgres + backend (bootRun) + frontend (Vite HMR)
|
├── docker-compose.yml # dev: postgres + backend (bootRun) + frontend (Vite HMR)
|
||||||
├── docker-compose.prod.yml # prod: multi-stage builds, no source mounts, restart: unless-stopped
|
├── docker-compose.prod.yml # prod: multi-stage builds, no source mounts, restart: unless-stopped
|
||||||
├── docker/
|
├── docker/
|
||||||
│ ├── backend.Dockerfile # dev: JDK + gradle :backend:bootRun
|
│ ├── backend.Dockerfile # dev: JDK + gradle bootRun
|
||||||
│ ├── backend.prod.Dockerfile # prod: multi-stage (Gradle → JRE Alpine, non-root)
|
│ ├── backend.prod.Dockerfile # prod: multi-stage (Gradle → JRE Alpine, non-root)
|
||||||
│ ├── frontend.Dockerfile # dev: Node + vite dev server
|
│ ├── frontend.Dockerfile # dev: Node + vite dev server
|
||||||
│ ├── frontend.prod.Dockerfile # prod: multi-stage (Node → nginx)
|
│ ├── frontend.prod.Dockerfile # prod: multi-stage (Node → nginx)
|
||||||
│ ├── nginx.conf # prod: SPA fallback + /api proxy
|
│ ├── nginx.conf # prod: SPA fallback + /api proxy
|
||||||
│ └── entrypoint.sh # prod: self-signed cert generation
|
│ └── entrypoint.sh # prod: self-signed cert generation
|
||||||
├── gradlew # Gradle wrapper (run from repo root)
|
|
||||||
├── gradle/
|
|
||||||
│ └── wrapper/
|
|
||||||
├── settings.gradle # rootProject.name + include 'backend'
|
|
||||||
├── build.gradle # convenience tasks: check, up, down, reset
|
|
||||||
├── .env.example
|
├── .env.example
|
||||||
├── README.md
|
├── README.md
|
||||||
├── REQUIREMENTS.md
|
├── REQUIREMENTS.md
|
||||||
└── CODING_GUIDELINES.md
|
├── CODING_GUIDELINES.md
|
||||||
|
└── ARCHITECTURE.md
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -153,7 +149,7 @@ bilhej/
|
||||||
|
|
||||||
| Aspect | `docker compose up -d` | `docker compose -f docker-compose.prod.yml up -d` |
|
| Aspect | `docker compose up -d` | `docker compose -f docker-compose.prod.yml up -d` |
|
||||||
|--------|------------------------|---------------------------------------------------|
|
|--------|------------------------|---------------------------------------------------|
|
||||||
| Backend | `./gradlew :backend:bootRun` (compiles on change) | Multi-stage build → `java -jar app.jar` |
|
| Backend | `./gradlew bootRun` (compiles on change) | Multi-stage build → `java -jar app.jar` |
|
||||||
| Backend image | `eclipse-temurin:21-jdk` (~400 MB) | `eclipse-temurin:21-jre-alpine` (~200 MB) |
|
| Backend image | `eclipse-temurin:21-jdk` (~400 MB) | `eclipse-temurin:21-jre-alpine` (~200 MB) |
|
||||||
| Backend user | root | `bilhej` (non-root) |
|
| Backend user | root | `bilhej` (non-root) |
|
||||||
| Frontend | Vite dev server (HMR, `--host 0.0.0.0`) | nginx serving static `dist/` |
|
| Frontend | Vite dev server (HMR, `--host 0.0.0.0`) | nginx serving static `dist/` |
|
||||||
|
|
@ -167,27 +163,19 @@ bilhej/
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### All-in-one (from repo root)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew check # lint → frontend test → backend test → integration test
|
|
||||||
./gradlew up # docker compose up -d
|
|
||||||
./gradlew down # docker compose down
|
|
||||||
./gradlew reset # docker compose down -v && docker compose up -d (full DB reset)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend (dev server with HMR)
|
### Frontend (dev server with HMR)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install # first time only
|
npm install
|
||||||
npm run dev # :3000 with HMR
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backend (IDE or CLI)
|
### Backend (IDE or CLI)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew :backend:bootRun # :8080, profile: default (H2)
|
cd backend
|
||||||
|
./gradlew bootRun
|
||||||
```
|
```
|
||||||
|
|
||||||
### Stripe Webhooks (local testing)
|
### Stripe Webhooks (local testing)
|
||||||
|
|
@ -196,15 +184,10 @@ npm run dev # :3000 with HMR
|
||||||
stripe listen --forward-to localhost:8080/api/webhooks/stripe
|
stripe listen --forward-to localhost:8080/api/webhooks/stripe
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database reset
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew reset # wipes DB volume and restarts containers
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Related Documents
|
## Related Documents
|
||||||
|
|
||||||
- [REQUIREMENTS.md](./REQUIREMENTS.md) — Full product requirements and business model
|
- [REQUIREMENTS.md](./REQUIREMENTS.md) — Full product requirements and business model
|
||||||
- [CODING_GUIDELINES.md](./CODING_GUIDELINES.md) — Code conventions and standards
|
- [CODING_GUIDELINES.md](./CODING_GUIDELINES.md) — Code conventions and standards
|
||||||
|
- [ARCHITECTURE.md](./ARCHITECTURE.md) — Detailed architecture and data flow
|
||||||
|
|
|
||||||
0
gradlew → backend/gradlew
vendored
0
gradlew → backend/gradlew
vendored
|
|
@ -1,3 +1 @@
|
||||||
rootProject.name = 'bilhej'
|
rootProject.name = 'bilhej'
|
||||||
|
|
||||||
include 'backend'
|
|
||||||
38
build.gradle
38
build.gradle
|
|
@ -1,38 +0,0 @@
|
||||||
plugins {
|
|
||||||
id 'base'
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('frontendLint', Exec) {
|
|
||||||
description = 'Run ESLint in the frontend directory'
|
|
||||||
workingDir = file("${rootProject.projectDir}/frontend")
|
|
||||||
commandLine 'npm', 'run', 'lint'
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('frontendTest', Exec) {
|
|
||||||
description = 'Run Vitest in the frontend directory'
|
|
||||||
dependsOn frontendLint
|
|
||||||
workingDir = file("${rootProject.projectDir}/frontend")
|
|
||||||
commandLine 'npm', 'run', 'test'
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named('check').configure {
|
|
||||||
dependsOn frontendLint, frontendTest
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('up', Exec) {
|
|
||||||
description = 'Start all services via Docker Compose'
|
|
||||||
workingDir = rootProject.projectDir
|
|
||||||
commandLine 'docker', 'compose', 'up', '-d'
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('down', Exec) {
|
|
||||||
description = 'Stop all Docker Compose services'
|
|
||||||
workingDir = rootProject.projectDir
|
|
||||||
commandLine 'docker', 'compose', 'down'
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('reset', Exec) {
|
|
||||||
description = 'Wipe database and restart all services'
|
|
||||||
workingDir = rootProject.projectDir
|
|
||||||
commandLine 'bash', '-c', 'docker compose down -v && docker compose up -d'
|
|
||||||
}
|
|
||||||
|
|
@ -36,7 +36,7 @@ services:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- ./backend:/app
|
||||||
- gradle-cache:/root/.gradle
|
- gradle-cache:/root/.gradle
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
FROM eclipse-temurin:21-jdk
|
FROM eclipse-temurin:21-jdk
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENTRYPOINT ["./gradlew", ":backend:bootRun", "--no-daemon"]
|
ENTRYPOINT ["./gradlew", "bootRun", "--no-daemon"]
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
|
||||||
import AppFooter from '@/components/AppFooter.vue'
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppHeader />
|
|
||||||
<main class="app__main">
|
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</main>
|
|
||||||
<AppFooter />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
.app__main {
|
|
||||||
min-height: calc(100vh - 12rem);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import AboutPage from '@/pages/AboutPage.vue'
|
|
||||||
|
|
||||||
describe('AboutPage', () => {
|
|
||||||
it('renders heading', () => {
|
|
||||||
const wrapper = mount(AboutPage)
|
|
||||||
expect(wrapper.text()).toContain('Om BilHälsning')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,23 +1,9 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import App from '@/App.vue'
|
import App from '@/App.vue'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
|
||||||
import AppFooter from '@/components/AppFooter.vue'
|
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
it('renders AppHeader and AppFooter', async () => {
|
|
||||||
router.push('/')
|
|
||||||
await router.isReady()
|
|
||||||
const wrapper = mount(App, {
|
|
||||||
global: {
|
|
||||||
plugins: [router],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(wrapper.findComponent(AppHeader).exists()).toBe(true)
|
|
||||||
expect(wrapper.findComponent(AppFooter).exists()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders RouterView with HomePage content', async () => {
|
it('renders RouterView with HomePage content', async () => {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
await router.isReady()
|
await router.isReady()
|
||||||
|
|
@ -26,6 +12,6 @@ describe('App', () => {
|
||||||
plugins: [router],
|
plugins: [router],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(wrapper.text()).toContain('Skicka ett brev till en fordonsägare')
|
expect(wrapper.text()).toContain('BilHälsning')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
|
||||||
import AppFooter from '@/components/AppFooter.vue'
|
|
||||||
|
|
||||||
function createTestRouter() {
|
|
||||||
return createRouter({
|
|
||||||
history: createMemoryHistory(),
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/om',
|
|
||||||
name: 'about',
|
|
||||||
component: { template: '<div>About</div>' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/kontakt',
|
|
||||||
name: 'contact',
|
|
||||||
component: { template: '<div>Contact</div>' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/integritetspolicy',
|
|
||||||
name: 'privacy',
|
|
||||||
component: { template: '<div>Privacy</div>' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/villkor',
|
|
||||||
name: 'terms',
|
|
||||||
component: { template: '<div>Terms</div>' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('AppFooter', () => {
|
|
||||||
it('renders all four links', () => {
|
|
||||||
const router = createTestRouter()
|
|
||||||
const wrapper = mount(AppFooter, {
|
|
||||||
global: { plugins: [router] },
|
|
||||||
})
|
|
||||||
const links = wrapper.findAll('a')
|
|
||||||
|
|
||||||
expect(links[0].text()).toBe('Om oss')
|
|
||||||
expect(links[0].attributes('href')).toBe('/om')
|
|
||||||
|
|
||||||
expect(links[1].text()).toBe('Kontakt')
|
|
||||||
expect(links[1].attributes('href')).toBe('/kontakt')
|
|
||||||
|
|
||||||
expect(links[2].text()).toBe('Integritetspolicy')
|
|
||||||
expect(links[2].attributes('href')).toBe('/integritetspolicy')
|
|
||||||
|
|
||||||
expect(links[3].text()).toBe('Villkor')
|
|
||||||
expect(links[3].attributes('href')).toBe('/villkor')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
|
||||||
|
|
||||||
function createTestRouter() {
|
|
||||||
return createRouter({
|
|
||||||
history: createMemoryHistory(),
|
|
||||||
routes: [
|
|
||||||
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('AppHeader', () => {
|
|
||||||
it('renders the logo text', () => {
|
|
||||||
const router = createTestRouter()
|
|
||||||
const wrapper = mount(AppHeader, {
|
|
||||||
global: { plugins: [router] },
|
|
||||||
})
|
|
||||||
expect(wrapper.text()).toContain('BilHälsning')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has a link to home', () => {
|
|
||||||
const router = createTestRouter()
|
|
||||||
const wrapper = mount(AppHeader, {
|
|
||||||
global: { plugins: [router] },
|
|
||||||
})
|
|
||||||
const links = wrapper.findAll('a')
|
|
||||||
const homeLink = links.find((a) => a.attributes('href') === '/')
|
|
||||||
expect(homeLink).toBeTruthy()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
|
||||||
import ComposePage from '@/pages/ComposePage.vue'
|
|
||||||
|
|
||||||
function createTestRouter() {
|
|
||||||
return createRouter({
|
|
||||||
history: createMemoryHistory(),
|
|
||||||
routes: [{ path: '/compose', name: 'compose', component: ComposePage }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ComposePage', () => {
|
|
||||||
it('renders heading', async () => {
|
|
||||||
const router = createTestRouter()
|
|
||||||
router.push('/compose')
|
|
||||||
await router.isReady()
|
|
||||||
const wrapper = mount(ComposePage, {
|
|
||||||
global: { plugins: [router] },
|
|
||||||
})
|
|
||||||
expect(wrapper.text()).toContain('Skriv ditt brev')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('displays plate from query param', async () => {
|
|
||||||
const router = createTestRouter()
|
|
||||||
router.push({ path: '/compose', query: { plate: 'ABC123' } })
|
|
||||||
await router.isReady()
|
|
||||||
const wrapper = mount(ComposePage, {
|
|
||||||
global: { plugins: [router] },
|
|
||||||
})
|
|
||||||
expect(wrapper.text()).toContain('ABC123')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not show plate when no query param', async () => {
|
|
||||||
const router = createTestRouter()
|
|
||||||
router.push('/compose')
|
|
||||||
await router.isReady()
|
|
||||||
const wrapper = mount(ComposePage, {
|
|
||||||
global: { plugins: [router] },
|
|
||||||
})
|
|
||||||
expect(wrapper.find('.compose__plate').exists()).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import ContactPage from '@/pages/ContactPage.vue'
|
|
||||||
|
|
||||||
describe('ContactPage', () => {
|
|
||||||
it('renders heading', () => {
|
|
||||||
const wrapper = mount(ContactPage)
|
|
||||||
expect(wrapper.text()).toContain('Kontakta oss')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,85 +1,10 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
|
||||||
import HomePage from '@/pages/HomePage.vue'
|
import HomePage from '@/pages/HomePage.vue'
|
||||||
import ComposePage from '@/pages/ComposePage.vue'
|
|
||||||
|
|
||||||
function createTestRouter() {
|
|
||||||
return createRouter({
|
|
||||||
history: createMemoryHistory(),
|
|
||||||
routes: [
|
|
||||||
{ path: '/', name: 'home', component: HomePage },
|
|
||||||
{ path: '/compose', name: 'compose', component: ComposePage },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function mountHome(router: ReturnType<typeof createTestRouter>) {
|
|
||||||
return mount(HomePage, {
|
|
||||||
global: { plugins: [router] },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('HomePage', () => {
|
describe('HomePage', () => {
|
||||||
it('renders subtitle', () => {
|
it('mounts successfully', () => {
|
||||||
const router = createTestRouter()
|
const wrapper = mount(HomePage)
|
||||||
const wrapper = mountHome(router)
|
expect(wrapper.text()).toContain('BilHälsning')
|
||||||
expect(wrapper.text()).toContain('Skicka ett brev till en fordonsägare')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not show CTA button initially', () => {
|
|
||||||
const router = createTestRouter()
|
|
||||||
const wrapper = mountHome(router)
|
|
||||||
expect(wrapper.find('.home__cta').exists()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not show CTA while loading', async () => {
|
|
||||||
const router = createTestRouter()
|
|
||||||
const wrapper = mountHome(router)
|
|
||||||
const plateInput = wrapper.findComponent({ name: 'PlateInput' })
|
|
||||||
|
|
||||||
await plateInput.vm.$emit('lookup', 'ABC123')
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
expect(wrapper.find('.home__cta').exists()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not show CTA after not-found', async () => {
|
|
||||||
const router = createTestRouter()
|
|
||||||
const wrapper = mountHome(router)
|
|
||||||
const plateInput = wrapper.findComponent({ name: 'PlateInput' })
|
|
||||||
|
|
||||||
await plateInput.vm.$emit('lookup', 'UNKNOWN')
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
||||||
|
|
||||||
expect(wrapper.find('.home__cta').exists()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows CTA button when vehicle data present', async () => {
|
|
||||||
const router = createTestRouter()
|
|
||||||
const wrapper = mountHome(router)
|
|
||||||
const plateInput = wrapper.findComponent({ name: 'PlateInput' })
|
|
||||||
|
|
||||||
await plateInput.vm.$emit('update:modelValue', 'ABC123')
|
|
||||||
await plateInput.vm.$emit('lookup', 'ABC123')
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
||||||
|
|
||||||
const cta = wrapper.find('.home__cta')
|
|
||||||
expect(cta.exists()).toBe(true)
|
|
||||||
expect(cta.text()).toBe('Skicka ett brev till ägaren')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('CTA links to compose page with plate query param', async () => {
|
|
||||||
const router = createTestRouter()
|
|
||||||
const wrapper = mountHome(router)
|
|
||||||
const plateInput = wrapper.findComponent({ name: 'PlateInput' })
|
|
||||||
|
|
||||||
await plateInput.vm.$emit('update:modelValue', 'ABC123')
|
|
||||||
await plateInput.vm.$emit('lookup', 'ABC123')
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
||||||
|
|
||||||
const cta = wrapper.find('.home__cta')
|
|
||||||
const href = cta.attributes('href')
|
|
||||||
expect(href).toBe('/compose?plate=ABC123')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { RouterLink } from 'vue-router'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<footer class="app-footer">
|
|
||||||
<nav class="app-footer__links">
|
|
||||||
<RouterLink to="/om" class="app-footer__link">Om oss</RouterLink>
|
|
||||||
<RouterLink to="/kontakt" class="app-footer__link">Kontakt</RouterLink>
|
|
||||||
<RouterLink to="/integritetspolicy" class="app-footer__link"
|
|
||||||
>Integritetspolicy</RouterLink
|
|
||||||
>
|
|
||||||
<RouterLink to="/villkor" class="app-footer__link">Villkor</RouterLink>
|
|
||||||
</nav>
|
|
||||||
</footer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.app-footer {
|
|
||||||
background: #f7fafc;
|
|
||||||
border-top: 1px solid #e2e8f0;
|
|
||||||
padding: 1.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-footer__links {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 2rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-footer__link {
|
|
||||||
color: #718096;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-footer__link:hover {
|
|
||||||
color: #1a202c;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { RouterLink } from 'vue-router'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<header class="app-header">
|
|
||||||
<RouterLink to="/" class="app-header__logo">BilHälsning</RouterLink>
|
|
||||||
<nav class="app-header__nav">
|
|
||||||
<RouterLink to="/" class="app-header__link">Hem</RouterLink>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.app-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header__logo {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1a202c;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header__nav {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header__link {
|
|
||||||
color: #4a5568;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header__link:hover {
|
|
||||||
color: #1a202c;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="about">
|
|
||||||
<h1>Om BilHälsning</h1>
|
|
||||||
<p>
|
|
||||||
BilHälsning är en tjänst som låter dig skicka fysiska brev till
|
|
||||||
fordonsägare via registreringsnummer.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.about {
|
|
||||||
max-width: 28rem;
|
|
||||||
margin: 3rem auto 0;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const plate = (route.query.plate as string) || ''
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="compose">
|
|
||||||
<h1>Skriv ditt brev</h1>
|
|
||||||
<p v-if="plate" class="compose__plate">Registreringsnummer: {{ plate }}</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.compose {
|
|
||||||
max-width: 28rem;
|
|
||||||
margin: 3rem auto 0;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compose__plate {
|
|
||||||
color: #4a5568;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="contact">
|
|
||||||
<h1>Kontakta oss</h1>
|
|
||||||
<p>
|
|
||||||
Har du frågor eller feedback? Hör av dig till oss på
|
|
||||||
<a href="mailto:hej@bilhalsning.se">hej@bilhalsning.se</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.contact {
|
|
||||||
max-width: 28rem;
|
|
||||||
margin: 3rem auto 0;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
|
||||||
import PlateInput from '@/components/PlateInput.vue'
|
import PlateInput from '@/components/PlateInput.vue'
|
||||||
import VehicleInfo from '@/components/VehicleInfo.vue'
|
import VehicleInfo from '@/components/VehicleInfo.vue'
|
||||||
import type { VehicleInfo as VehicleData } from '@/components/VehicleInfo.vue'
|
import type { VehicleInfo as VehicleData } from '@/components/VehicleInfo.vue'
|
||||||
|
|
@ -37,6 +36,7 @@ function handleLookup(lookedUpPlate: string) {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="home">
|
<div class="home">
|
||||||
|
<h1>BilHälsning</h1>
|
||||||
<p class="home__subtitle">Skicka ett brev till en fordonsägare</p>
|
<p class="home__subtitle">Skicka ett brev till en fordonsägare</p>
|
||||||
|
|
||||||
<PlateInput v-model="plate" @lookup="handleLookup" />
|
<PlateInput v-model="plate" @lookup="handleLookup" />
|
||||||
|
|
@ -47,14 +47,6 @@ function handleLookup(lookedUpPlate: string) {
|
||||||
:not-found="notFound"
|
:not-found="notFound"
|
||||||
:plate="plate"
|
:plate="plate"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RouterLink
|
|
||||||
v-if="vehicle"
|
|
||||||
:to="{ name: 'compose', query: { plate } }"
|
|
||||||
class="home__cta"
|
|
||||||
>
|
|
||||||
Skicka ett brev till ägaren
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -69,21 +61,4 @@ function handleLookup(lookedUpPlate: string) {
|
||||||
color: #718096;
|
color: #718096;
|
||||||
margin: 0 0 1.5rem 0;
|
margin: 0 0 1.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home__cta {
|
|
||||||
display: block;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
padding: 0.875rem 1.5rem;
|
|
||||||
background: #38a169;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home__cta:hover {
|
|
||||||
background: #2f855a;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import HomePage from '@/pages/HomePage.vue'
|
import HomePage from '@/pages/HomePage.vue'
|
||||||
import ComposePage from '@/pages/ComposePage.vue'
|
|
||||||
import AboutPage from '@/pages/AboutPage.vue'
|
|
||||||
import ContactPage from '@/pages/ContactPage.vue'
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
|
@ -12,21 +9,6 @@ const router = createRouter({
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomePage,
|
component: HomePage,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/compose',
|
|
||||||
name: 'compose',
|
|
||||||
component: ComposePage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/om',
|
|
||||||
name: 'about',
|
|
||||||
component: AboutPage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/kontakt',
|
|
||||||
name: 'contact',
|
|
||||||
component: ContactPage,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue