Angebotsübersicht und Buchung verbinden, Mailpit einrichten

feature/angebote-booking-integration-detailseiten
Marvin Gemlin 2026-09-25 11:53:07 +00:00
parent 762f21dfa1
commit 4ed3ab54e8
18 changed files with 259 additions and 92 deletions

View File

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

View File

@ -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"
} }
} }

View File

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

59
README.md 100644
View File

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

View File

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

View File

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

View File

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

View File

@ -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",

View File

@ -0,0 +1,5 @@
import "../../styles/styles.css";
import "../../styles/packages.css";
import { renderAngebotsuebersicht } from "./angebotsuebersicht.js";
renderAngebotsuebersicht();

View File

@ -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>
`; `;

View File

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

View File

@ -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,
},
];

View File

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

View File

@ -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>";
}
}

View File

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

View File

@ -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 () => {},
}); });

View File

@ -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());
}
}
}); });

View File

@ -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),