Angebotsübersicht und Buchung verbinden, Mailpit einrichten
parent
762f21dfa1
commit
4ed3ab54e8
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Version bewusst festhalten, damit alle dieselbe Mailpit-Version verwenden.
|
||||||
|
FROM axllent/mailpit:v1.31.2 AS mailpit
|
||||||
|
FROM mcr.microsoft.com/devcontainers/javascript-node:24-bookworm
|
||||||
|
|
||||||
|
COPY --from=mailpit /mailpit /usr/local/bin/mailpit
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
{
|
{
|
||||||
"name": "Ernaehrungsberatung",
|
"name": "Ernaehrungsberatung",
|
||||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:24-bookworm",
|
|
||||||
"remoteUser": "node",
|
"remoteUser": "node",
|
||||||
"forwardPorts": [5173],
|
"forwardPorts": [5173, 8025],
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"5173": {
|
"5173": {
|
||||||
"label": "Webseite",
|
"label": "Webseite",
|
||||||
"onAutoForward": "notify"
|
"onAutoForward": "notify"
|
||||||
|
},
|
||||||
|
"8025": {
|
||||||
|
"label": "Mailpit – Testmails",
|
||||||
|
"onAutoForward": "notify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"postCreateCommand": "if [ -f package-lock.json ]; then npm ci; else npm install; fi",
|
"postCreateCommand": "bash .devcontainer/setup.sh",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
|
@ -17,5 +20,8 @@
|
||||||
"EditorConfig.EditorConfig"
|
"EditorConfig.EditorConfig"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
npm ci
|
||||||
|
if [ ! -e .env ]; then
|
||||||
|
cp .env.example .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Buchungsdatei nicht vorab anlegen: Das Backend erstellt sie bei der ersten Buchung.
|
||||||
|
echo "Bereit. In drei Terminals starten: npm run dev:mail, npm run dev:server, npm run dev"
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Tri-Hub Ernährungsberatung
|
||||||
|
|
||||||
|
## Im Dev Container starten
|
||||||
|
|
||||||
|
1. Repository klonen und in VS Code „Dev Containers: Reopen in Container“ wählen.
|
||||||
|
Bei einem bereits vorhandenen Container einmal „Dev Containers: Rebuild Container“
|
||||||
|
ausführen, damit Mailpit installiert wird.
|
||||||
|
2. Die Einrichtung installiert automatisch die npm-Abhängigkeiten und erstellt
|
||||||
|
`.env` aus `.env.example`, sofern noch keine `.env` vorhanden ist.
|
||||||
|
3. In drei getrennten Terminals in dieser Reihenfolge starten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev:mail
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev:server
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Startseite: http://localhost:5173 — über „Beratung kennenlernen“ zur
|
||||||
|
Angebotsübersicht und von dort zum Buchungsformular des gewählten Pakets.
|
||||||
|
Direkt: http://localhost:5173/angebotsuebersicht.html
|
||||||
|
Mailansicht: http://localhost:8025. Beide Web-Ports werden vom Dev Container
|
||||||
|
weitergeleitet. Mailpit läuft im selben Container auf SMTP-Port 1025 und fängt
|
||||||
|
Testmails lokal ab, ohne sie extern zu versenden.
|
||||||
|
|
||||||
|
Die Container-Konfiguration installiert Mailpit v1.31.2 aus dem
|
||||||
|
[offiziellen Docker-Image](https://mailpit.axllent.org/docs/install/docker/).
|
||||||
|
Die Binärdatei, lokale Mails, `.env` und Buchungsdaten werden nicht committed.
|
||||||
|
Mailpit verwendet ohne Datenbankpfad einen temporären Speicher; Testmails müssen
|
||||||
|
nach einem Neustart nicht erhalten bleiben.
|
||||||
|
|
||||||
|
Ohne Dev Container: Node.js 24 und Mailpit installieren, `npm ci` ausführen und
|
||||||
|
`.env.example` nach `.env` kopieren, falls `.env` fehlt. Danach dieselben drei
|
||||||
|
Startbefehle verwenden.
|
||||||
|
|
||||||
|
Ohne Mailpit wird die Buchung gespeichert, aber der fehlgeschlagene Mailversand
|
||||||
|
angezeigt. Der Ablauf bleibt eine Simulation ohne Zahlung oder verbindliche
|
||||||
|
Beratung. Buchungen liegen lokal in `src/server/data/bookings.csv`. Diese Datei
|
||||||
|
nicht vorab leer anlegen: Das Backend erzeugt sie bei der ersten Buchung selbst.
|
||||||
|
|
||||||
|
## Prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
npm run build
|
||||||
|
npm run test:browser
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Browsertests starten eigene Server auf 3000 und 5173; diese Ports vorher
|
||||||
|
freihalten. Sie verwenden eine temporäre CSV und simulierten Mailversand.
|
||||||
|
Falls Chromium fehlt: `npx playwright install chromium`.
|
||||||
|
|
||||||
|
Der Build enthält Startseite, Angebotsübersicht und Buchungsseite. Für ein
|
||||||
|
Deployment zusätzlich das Node-Backend betreiben und `/api` dorthin weiterleiten.
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
<section class="packages-section" id="angebotsuebersicht-section">
|
<!doctype html>
|
||||||
<div class="packages-header">
|
<html lang="de">
|
||||||
<!-- Hier könnte noch eine Überschrift hin, falls gewünscht -->
|
<head>
|
||||||
<h2>Unsere Ernährungs-Pakete</h2>
|
<meta charset="UTF-8" />
|
||||||
</div>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Ernährungs-Pakete – Tri-Hub</title>
|
||||||
<!-- Das ist der Container, den unser JS mit den Karten füllt -->
|
</head>
|
||||||
<div class="packages-container" id="packages-container">
|
<body>
|
||||||
<!-- Karten werden hier dynamisch von angebotsuebersicht.js injiziert -->
|
<header class="container"><a href="/">Zur Startseite</a></header>
|
||||||
</div>
|
<main class="packages-section" id="angebotsuebersicht-section">
|
||||||
</section>
|
<div class="packages-header"><h1>Unsere Ernährungs-Pakete</h1></div>
|
||||||
|
<div class="packages-container" id="packages-container"></div>
|
||||||
|
</main>
|
||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
src="/src/features/angebote/angebote-entry.js"
|
||||||
|
></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,16 @@ cp .env.example .env
|
||||||
|
|
||||||
Eine vorhandene `.env` vorher prüfen und nicht überschreiben. Die Beispielwerte
|
Eine vorhandene `.env` vorher prüfen und nicht überschreiben. Die Beispielwerte
|
||||||
aktivieren das klar gekennzeichnete Testpaket und verwenden nur einen Mailfänger.
|
aktivieren das klar gekennzeichnete Testpaket und verwenden nur einen Mailfänger.
|
||||||
Ohne `.env` bleibt der Demo-Modus ausgeschaltet und der echte Paketkatalog leer.
|
Ohne `.env` bleibt der Demo-Modus ausgeschaltet; die drei regulären Pakete bleiben verfügbar.
|
||||||
|
|
||||||
Einmalig [Mailpit installieren](https://mailpit.axllent.org/docs/install/).
|
Mailpit ist über `.devcontainer/Dockerfile` bereits Bestandteil neuer Dev Container.
|
||||||
|
Bestehende Container einmal neu bauen. Außerhalb davon einmalig
|
||||||
|
[Mailpit installieren](https://mailpit.axllent.org/docs/install/).
|
||||||
Es läuft als einzelne Binärdatei auch direkt im Dev Container. Kein Docker im
|
Es läuft als einzelne Binärdatei auch direkt im Dev Container. Kein Docker im
|
||||||
Container erforderlich. Mailpit ohne Weiterleitung an echte SMTP-Server starten:
|
Container erforderlich. Mailpit ohne Weiterleitung an echte SMTP-Server starten:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mailpit --listen 127.0.0.1:8025 --smtp 127.0.0.1:1025 --disable-version-check
|
npm run dev:mail
|
||||||
```
|
```
|
||||||
|
|
||||||
In Terminal 2 das Backend starten:
|
In Terminal 2 das Backend starten:
|
||||||
|
|
@ -56,8 +58,8 @@ npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Öffnen: <http://localhost:5173/booking.html#/buchen/demo-booking>.
|
Öffnen: <http://localhost:5173/booking.html#/buchen/demo-booking>.
|
||||||
Mailfänger: <http://localhost:8025>. Im Dev Container bei Bedarf Port 8025 über
|
Mailfänger: <http://localhost:8025>. Im Dev Container sind die Ports 8025 und 5173
|
||||||
VS Codes Ansicht „Ports“ weiterleiten. Port 5173 ist bereits vorkonfiguriert.
|
für die Weiterleitung vorkonfiguriert.
|
||||||
Port 3000 muss für den Browser nicht weitergeleitet werden: Vite vermittelt `/api`.
|
Port 3000 muss für den Browser nicht weitergeleitet werden: Vite vermittelt `/api`.
|
||||||
Diese Beschreibung setzt voraus, dass Mailpit im selben Container läuft.
|
Diese Beschreibung setzt voraus, dass Mailpit im selben Container läuft.
|
||||||
|
|
||||||
|
|
@ -92,7 +94,9 @@ Die `.env.example` enthält nur Testwerte und keine Geheimnisse.
|
||||||
## Paketdaten und spätere Router-Anbindung
|
## Paketdaten und spätere Router-Anbindung
|
||||||
|
|
||||||
Person 3 pflegt `packages` in **`src/shared/packages.js`**.
|
Person 3 pflegt `packages` in **`src/shared/packages.js`**.
|
||||||
Das derzeit leere Array enthält später ausschließlich abgestimmte Angebote:
|
Das Array enthält die drei Angebote Starter, Standard und Premium. Übersicht und
|
||||||
|
Buchung verwenden gemeinsam `id`, `name` und `summary`; zusätzliche Felder
|
||||||
|
beschreiben die Darstellung der Angebotskarten.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Nur Schema-Beispiel, kein verbindliches Angebot:
|
// Nur Schema-Beispiel, kein verbindliches Angebot:
|
||||||
|
|
@ -127,7 +131,7 @@ cleanup();
|
||||||
Das Modul bringt seine begrenzten `.booking`-Styles mit. Farben und Schrift
|
Das Modul bringt seine begrenzten `.booking`-Styles mit. Farben und Schrift
|
||||||
orientieren sich am vorhandenen Basis-CSS. Gemeinsame Designvariablen und eine
|
orientieren sich am vorhandenen Basis-CSS. Gemeinsame Designvariablen und eine
|
||||||
Button-Komponente gibt es bisher nicht. Layout und Navigation bleiben Aufgabe
|
Button-Komponente gibt es bisher nicht. Layout und Navigation bleiben Aufgabe
|
||||||
der anderen Teammitglieder. „Zur Angebotsseite“ zeigt vorerst auf `/#beratung`.
|
der anderen Teammitglieder. „Zur Angebotsseite“ führt zu `/angebotsuebersicht.html`.
|
||||||
|
|
||||||
## API-Beispiele
|
## API-Beispiele
|
||||||
|
|
||||||
|
|
@ -336,7 +340,7 @@ Binärdatei außerhalb des Repositorys.
|
||||||
|
|
||||||
## Offene Punkte und gemeinsame Dateien
|
## Offene Punkte und gemeinsame Dateien
|
||||||
|
|
||||||
Offen: echte Paketdaten, Anschluss an den gemeinsamen Router, abschließendes
|
Die Angebotsübersicht ist als eigene HTML-Seite angebunden. Offen: Anschluss an einen gemeinsamen Router, abschließendes
|
||||||
Teamdesign, reale Zustellprüfung und Screenreader-Abnahme. Vor öffentlichem
|
Teamdesign, reale Zustellprüfung und Screenreader-Abnahme. Vor öffentlichem
|
||||||
Betrieb sind außerdem Betriebsfragen wie Zugriff auf CSV/Backups, Aufbewahrung
|
Betrieb sind außerdem Betriebsfragen wie Zugriff auf CSV/Backups, Aufbewahrung
|
||||||
und Schutz des öffentlichen Buchungsendpunkts vor automatisiertem Massenversand
|
und Schutz des öffentlichen Buchungsendpunkts vor automatisiertem Massenversand
|
||||||
|
|
@ -347,7 +351,7 @@ Gemeinsame Änderungen: `package.json` und Lockdatei (Abhängigkeiten/Startbefeh
|
||||||
`.prettierignore` (private Daten/Testergebnisse ausschließen). Neu hinzugefügt:
|
`.prettierignore` (private Daten/Testergebnisse ausschließen). Neu hinzugefügt:
|
||||||
`vite.config.js` (Proxy, Dateizugriffsschutz, zwei HTML-Einstiegspunkte),
|
`vite.config.js` (Proxy, Dateizugriffsschutz, zwei HTML-Einstiegspunkte),
|
||||||
`src/shared/packages.js` (gemeinsamer Paketvertrag), `.env.example`, `booking.html`
|
`src/shared/packages.js` (gemeinsamer Paketvertrag), `.env.example`, `booking.html`
|
||||||
und `playwright.config.js`. Die Dev-Container-Konfiguration bleibt unverändert.
|
und `playwright.config.js`. Die Dev-Container-Konfiguration installiert Mailpit und leitet dessen Web-Port weiter.
|
||||||
|
|
||||||
Technische Referenzen: [CSV-Parser](https://csv.js.org/parse/api/sync/),
|
Technische Referenzen: [CSV-Parser](https://csv.js.org/parse/api/sync/),
|
||||||
[Playwright-Testserver](https://playwright.dev/docs/test-webserver).
|
[Playwright-Testserver](https://playwright.dev/docs/test-webserver).
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,13 @@
|
||||||
Individuelle 1:1-Beratung für deinen Alltag und deine
|
Individuelle 1:1-Beratung für deinen Alltag und deine
|
||||||
sportlichen Ziele.
|
sportlichen Ziele.
|
||||||
</p>
|
</p>
|
||||||
<a href="#beratung">Beratung kennenlernen</a>
|
<a href="/angebotsuebersicht.html">Beratung kennenlernen</a>
|
||||||
|
|
||||||
<section id="beratung" aria-labelledby="beratung-heading">
|
<section id="beratung" aria-labelledby="beratung-heading">
|
||||||
<h2 id="beratung-heading">Deine persönliche Beratung</h2>
|
<h2 id="beratung-heading">Deine persönliche Beratung</h2>
|
||||||
<p>
|
<p>
|
||||||
Hier stellen wir bald unseren Ablauf und unser Angebot vor.
|
Entdecke unsere Pakete für deine Ernährung im Alltag und
|
||||||
|
Sport.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,9 @@
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"dev:server": "node --env-file-if-exists=.env --watch src/server/index.js",
|
"dev:server": "node --env-file-if-exists=.env --watch src/server/index.js",
|
||||||
"start:server": "node --env-file-if-exists=.env src/server/index.js",
|
"start:server": "node --env-file-if-exists=.env src/server/index.js",
|
||||||
"test": "node --test src/tests/bookings.test.js",
|
"test": "node --test src/tests/*.test.js",
|
||||||
"test:browser": "playwright test"
|
"test:browser": "playwright test",
|
||||||
|
"dev:mail": "mailpit --listen 127.0.0.1:8025 --smtp 127.0.0.1:1025 --disable-version-check"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.63.0",
|
"@playwright/test": "^1.63.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import "../../styles/styles.css";
|
||||||
|
import "../../styles/packages.css";
|
||||||
|
import { renderAngebotsuebersicht } from "./angebotsuebersicht.js";
|
||||||
|
|
||||||
|
renderAngebotsuebersicht();
|
||||||
|
|
@ -28,8 +28,8 @@ export function renderAngebotsuebersicht() {
|
||||||
${highlightHtml}
|
${highlightHtml}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="package-title">${pkg.title}</h3>
|
<h3 class="package-title">${pkg.name}</h3>
|
||||||
<p class="package-desc">${pkg.description}</p>
|
<p class="package-desc">${pkg.summary}</p>
|
||||||
|
|
||||||
<div class="package-details-box">
|
<div class="package-details-box">
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
|
|
@ -47,7 +47,7 @@ export function renderAngebotsuebersicht() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- HIER ist der wichtige Link für deinen Kollegen: -->
|
<!-- HIER ist der wichtige Link für deinen Kollegen: -->
|
||||||
<a href="booking.html?id=${pkg.id}" class="package-btn">${pkg.buttonText}</a>
|
<a href="/booking.html#/buchen/${encodeURIComponent(pkg.id)}" class="package-btn">${pkg.buttonText}</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export function mountBookingPage(root, { packageId } = {}) {
|
||||||
const { signal } = controller;
|
const { signal } = controller;
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
<div class="booking">
|
<div class="booking">
|
||||||
<a href="/#beratung">Zur Angebotsseite</a>
|
<a href="/angebotsuebersicht.html">Zur Angebotsseite</a>
|
||||||
<h1>Paket buchen</h1>
|
<h1>Paket buchen</h1>
|
||||||
<p class="booking__intro" role="status">Paket wird geladen …</p>
|
<p class="booking__intro" role="status">Paket wird geladen …</p>
|
||||||
<div class="booking__content"></div>
|
<div class="booking__content"></div>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ export const packages = [
|
||||||
{
|
{
|
||||||
id: "ernaehrung-starter",
|
id: "ernaehrung-starter",
|
||||||
type: "STARTER",
|
type: "STARTER",
|
||||||
title: "Für den sicheren Einstieg in die Sporternährung",
|
name: "Für den sicheren Einstieg in die Sporternährung",
|
||||||
description:
|
summary:
|
||||||
"Für Menschen 50+, die mit Triathlon beginnen und ihre Ernährung an die neue Belastung anpassen möchten.",
|
"Für Menschen 50+, die mit Triathlon beginnen und ihre Ernährung an die neue Belastung anpassen möchten.",
|
||||||
fokus: "Grundlagen der Makronährstoffe, Hydration im Training und Gewichtsmanagement.",
|
fokus: "Grundlagen der Makronährstoffe, Hydration im Training und Gewichtsmanagement.",
|
||||||
anamnese:
|
anamnese:
|
||||||
|
|
@ -16,8 +16,8 @@ export const packages = [
|
||||||
{
|
{
|
||||||
id: "ernaehrung-standard",
|
id: "ernaehrung-standard",
|
||||||
type: "STANDARD",
|
type: "STANDARD",
|
||||||
title: "Für Best Ager mit ersten Erfahrungen und klaren Zielen",
|
name: "Für Best Ager mit ersten Erfahrungen und klaren Zielen",
|
||||||
description:
|
summary:
|
||||||
"Für Athletinnen und Athleten, die ihre Ernährung für die Kurzdistanz und im Alltag gezielt weiterentwickeln wollen.",
|
"Für Athletinnen und Athleten, die ihre Ernährung für die Kurzdistanz und im Alltag gezielt weiterentwickeln wollen.",
|
||||||
fokus: "Carb-Loading, Pacing-Strategien für den Magen und Timing der Nahrungsaufnahme.",
|
fokus: "Carb-Loading, Pacing-Strategien für den Magen und Timing der Nahrungsaufnahme.",
|
||||||
anamnese:
|
anamnese:
|
||||||
|
|
@ -30,8 +30,8 @@ export const packages = [
|
||||||
{
|
{
|
||||||
id: "ernaehrung-premium",
|
id: "ernaehrung-premium",
|
||||||
type: "PREMIUM",
|
type: "PREMIUM",
|
||||||
title: "Für maximale Individualität mit klarem Fokus",
|
name: "Für maximale Individualität mit klarem Fokus",
|
||||||
description:
|
summary:
|
||||||
"Für Menschen, die sich eine besonders enge Coaching-Beziehung und laufende Anpassung ihrer Ernährung wünschen.",
|
"Für Menschen, die sich eine besonders enge Coaching-Beziehung und laufende Anpassung ihrer Ernährung wünschen.",
|
||||||
fokus: "Individuelle Stoffwechselanalyse, Mikronährstoff-Optimierung und Raceday-Nutrition.",
|
fokus: "Individuelle Stoffwechselanalyse, Mikronährstoff-Optimierung und Raceday-Nutrition.",
|
||||||
anamnese:
|
anamnese:
|
||||||
|
|
@ -42,3 +42,13 @@ export const packages = [
|
||||||
highlight: null,
|
highlight: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Nur für lokale Demonstrationen und isolierte Tests.
|
||||||
|
export const demoPackages = [
|
||||||
|
{
|
||||||
|
id: "demo-booking",
|
||||||
|
name: "1:1-Ernährungsberatung",
|
||||||
|
summary: "Testpaket für den simulierten Buchungsablauf.",
|
||||||
|
testOnly: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
.packages-container {
|
.packages-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,30 @@
|
||||||
export async function renderAngebotsuebersicht() {
|
import test from "node:test";
|
||||||
const container = document.getElementById("packages-container");
|
import assert from "node:assert/strict";
|
||||||
if (!container) return;
|
import { createApp } from "../server/index.js";
|
||||||
|
import { packages } from "../shared/packages.js";
|
||||||
|
|
||||||
try {
|
test("API liefert für jedes angezeigte Angebot dieselbe ID, Namen und Beschreibung", async (t) => {
|
||||||
// 1. Daten vom Backend des Kollegen abfragen
|
const app = createApp({ sendConfirmation: async () => {} });
|
||||||
const response = await fetch("/api/angebote"); // Die genaue URL musst du den Kollegen fragen
|
await new Promise((resolve) => app.listen(0, "127.0.0.1", resolve));
|
||||||
const backendPackages = await response.json();
|
t.after(
|
||||||
|
() =>
|
||||||
container.innerHTML = "";
|
new Promise((resolve) => {
|
||||||
|
app.closeAllConnections();
|
||||||
// 2. Mit den echten Daten die Karten generieren
|
app.close(resolve);
|
||||||
backendPackages.forEach((pkg) => {
|
}),
|
||||||
const highlightHtml = pkg.highlight
|
);
|
||||||
? `<span class="package-highlight">${pkg.highlight}</span>`
|
const response = await fetch(
|
||||||
: "";
|
`http://127.0.0.1:${app.address().port}/api/angebote`,
|
||||||
|
);
|
||||||
const cardHtml = `
|
assert.equal(response.status, 200);
|
||||||
<div class="package-card">
|
const body = await response.json();
|
||||||
<div class="package-type-container">
|
assert.deepEqual(
|
||||||
<span class="package-type">${pkg.type}</span>
|
body.packages,
|
||||||
${highlightHtml}
|
packages.map(({ id, name, summary }) => ({
|
||||||
</div>
|
id,
|
||||||
<h3 class="package-title">${pkg.title}</h3>
|
name,
|
||||||
<p class="package-desc">${pkg.description}</p>
|
summary,
|
||||||
<!-- ... Rest des HTMLs wie gehabt ... -->
|
testOnly: false,
|
||||||
<a href="buchung.html?id=${pkg.id}" class="package-btn">${pkg.buttonText}</a>
|
})),
|
||||||
</div>
|
);
|
||||||
`;
|
});
|
||||||
container.innerHTML += cardHtml;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Fehler beim Laden der Angebote vom Backend:", error);
|
|
||||||
container.innerHTML = "<p>Angebote konnten nicht geladen werden.</p>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
import { packages } from "../../shared/packages.js";
|
||||||
|
|
||||||
const pageUrl = "/booking.html#/buchen/demo-booking";
|
const pageUrl = "/booking.html#/buchen/demo-booking";
|
||||||
async function fill(page) {
|
async function fill(page) {
|
||||||
await page
|
await page
|
||||||
|
|
@ -179,3 +181,53 @@ test("Vite liefert Serverdateien und Umgebungsdateien nicht aus", async ({
|
||||||
expect([403, 404]).toContain(response.status());
|
expect([403, 404]).toContain(response.status());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const pkg of packages) {
|
||||||
|
test(`Von der Startseite zur Buchung: ${pkg.type}`, async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
await page.getByRole("link", { name: "Beratung kennenlernen" }).click();
|
||||||
|
await expect(page.locator(".package-card")).toHaveCount(
|
||||||
|
packages.length,
|
||||||
|
);
|
||||||
|
await page.getByRole("link", { name: pkg.buttonText }).click();
|
||||||
|
await expect(page).toHaveURL(
|
||||||
|
new RegExp(`booking\\.html#/buchen/${pkg.id}$`),
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: pkg.name, exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.locator(".booking__summary")).toHaveText(pkg.summary);
|
||||||
|
await fill(page);
|
||||||
|
const responsePromise = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().endsWith("/api/bookings") &&
|
||||||
|
response.request().method() === "POST",
|
||||||
|
);
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "Paket buchen", exact: true })
|
||||||
|
.click();
|
||||||
|
const response = await responsePromise;
|
||||||
|
expect(response.status()).toBe(201);
|
||||||
|
const booking = await response.json();
|
||||||
|
expect(booking.packageId).toBe(pkg.id);
|
||||||
|
expect(booking.packageName).toBe(pkg.name);
|
||||||
|
await expect(page.locator(".booking__result")).toContainText(
|
||||||
|
booking.bookingId,
|
||||||
|
);
|
||||||
|
await page.getByRole("link", { name: "Zur Angebotsseite" }).click();
|
||||||
|
await expect(page.locator(".package-card")).toHaveCount(
|
||||||
|
packages.length,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Angebotsübersicht passt auf ein schmales Display", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 320, height: 640 });
|
||||||
|
await page.goto("/angebotsuebersicht.html");
|
||||||
|
await expect(page.locator(".package-card")).toHaveCount(packages.length);
|
||||||
|
expect(
|
||||||
|
await page.evaluate(
|
||||||
|
() => document.documentElement.scrollWidth <= window.innerWidth,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { createApp } from "../../server/index.js";
|
import { createApp } from "../../server/index.js";
|
||||||
import { createCsvStorage } from "../../server/services/csv-storage.js";
|
import { createCsvStorage } from "../../server/services/csv-storage.js";
|
||||||
import { demoPackages } from "../../shared/packages.js";
|
import { packages, demoPackages } from "../../shared/packages.js";
|
||||||
|
|
||||||
const directory = await mkdtemp(join(tmpdir(), "trihub-browser-"));
|
const directory = await mkdtemp(join(tmpdir(), "trihub-browser-"));
|
||||||
const server = createApp({
|
const server = createApp({
|
||||||
catalog: demoPackages,
|
catalog: [...packages, ...demoPackages],
|
||||||
storage: createCsvStorage(join(directory, "bookings.csv")),
|
storage: createCsvStorage(join(directory, "bookings.csv")),
|
||||||
sendConfirmation: async () => {},
|
sendConfirmation: async () => {},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,28 @@
|
||||||
import { packages } from "../shared/packages.js";
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { packages, demoPackages } from "../shared/packages.js";
|
||||||
|
import { createApp } from "../server/index.js";
|
||||||
|
|
||||||
describe("Angebots-Daten (packages.js)", () => {
|
test("Angebote erfüllen den gemeinsamen Vertrag von Übersicht und Buchung", () => {
|
||||||
test("sollte ein Array mit Angeboten exportieren", () => {
|
assert.ok(packages.length > 0);
|
||||||
expect(Array.isArray(nutritionPackages)).toBe(true);
|
assert.doesNotThrow(() =>
|
||||||
expect(nutritionPackages.length).toBeGreaterThan(0);
|
createApp({
|
||||||
});
|
catalog: [...packages, ...demoPackages],
|
||||||
|
sendConfirmation: async () => {},
|
||||||
test("jedes Paket muss eine ID und einen Titel für das Routing besitzen", () => {
|
}),
|
||||||
nutritionPackages.forEach((pkg) => {
|
);
|
||||||
expect(pkg).toHaveProperty("id");
|
for (const pkg of packages) {
|
||||||
expect(pkg).toHaveProperty("title");
|
for (const field of [
|
||||||
expect(pkg).toHaveProperty("buttonText");
|
"name",
|
||||||
// Prüft, ob die ID das korrekte Format für die URLs hat
|
"summary",
|
||||||
expect(typeof pkg.id).toBe("string");
|
"type",
|
||||||
expect(pkg.id.length).toBeGreaterThan(0);
|
"fokus",
|
||||||
});
|
"anamnese",
|
||||||
});
|
"begleitung",
|
||||||
|
"buttonText",
|
||||||
|
]) {
|
||||||
|
assert.equal(typeof pkg[field], "string");
|
||||||
|
assert.ok(pkg[field].trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
|
angebote: fileURLToPath(
|
||||||
|
new URL("./angebotsuebersicht.html", import.meta.url),
|
||||||
|
),
|
||||||
main: fileURLToPath(new URL("./index.html", import.meta.url)),
|
main: fileURLToPath(new URL("./index.html", import.meta.url)),
|
||||||
booking: fileURLToPath(
|
booking: fileURLToPath(
|
||||||
new URL("./booking.html", import.meta.url),
|
new URL("./booking.html", import.meta.url),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue