Terminplaner TP2: UI-Testserver Entwurf und Playwright Fragebogen bis Bestellklick
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87d267e51a
commit
1ac142b92f
2 files changed
+876
-5
No files matched your search
@@ -21,6 +21,14 @@
|
||||
// startet ohne Einstellung (Vorlagenauswahl); der Server stellt auch dashboard.html und /api/funktionen bereit, die uebrigen
|
||||
// Portal-Schnittstellen (/api/auth/me usw.) mockt das Skript per page.route.
|
||||
//
|
||||
// Entwurf und Bestellung (TP2, Fragebogen bis Bestellklick): srv.zuruecksetzen({ entwurf: true }) startet wie ein Kunde vor der ersten Branchenwahl:
|
||||
// kein Funktionsdokument, keine Einstellung, aber ein laufendes Wartungs-Abo (wartungDoc) fuer c1 und ein Konto mit Firma und Anschrift
|
||||
// (Vorschlag fuer die Betriebsangaben). srv.abo(zustand) tauscht das Abo ('wartung' | 'hosting' | 'keins' | 'gekuendigt' | 'nicht_aktiv'),
|
||||
// srv.entwurf({ eingabe, fragebogen }) legt Funktionsdokument (off), Einstellung und Fragebogen an (Abkuerzung statt aller Klicks),
|
||||
// srv.bestellt({ mitZustimmung }) macht daraus eine Bestellung im Zustand ordered mit offener Stripe-Sitzung (und dem Nachweis dieser Sitzung),
|
||||
// srv.kundenansichtCookie() ist die Sitzung eines Mitarbeiters in der Kundenansicht (der Waechter haengt hier nicht: geprueft wird die
|
||||
// Absicherung der Router selbst). srv.fragebogen() / srv.version() lesen den gespeicherten Stand.
|
||||
//
|
||||
// Seitenherkunft: srv.url ist http://127.0.0.1:PORT, srv.pageOrigin ist http://localhost:PORT (derselbe Server, anderer Host).
|
||||
// Mit { crossOrigin: true } gilt pageOrigin als Herkunft des Portals (config.publicBaseUrl): Seiten von pageOrigin mit
|
||||
// data-api=srv.url sind dann echte Cross-Origin-Anfragen (Preflight fuer POST), die der Server erlaubt.
|
||||
@@ -118,6 +126,8 @@ export async function startTermineUiServer({ jetzt = JETZT, crossOrigin = false,
|
||||
termineClaims: schemaKeys('termineClaims'),
|
||||
termineOutbox: schemaKeys('termineOutbox'),
|
||||
customerViewLog: schemaKeys('customerViewLog'),
|
||||
subscriptions: schemaKeys('subscriptions'),
|
||||
featureConsents: schemaKeys('featureConsents'),
|
||||
},
|
||||
datetimes: TERMINE_DATETIMES,
|
||||
defaultLimit: 25,
|
||||
@@ -128,14 +138,19 @@ export async function startTermineUiServer({ jetzt = JETZT, crossOrigin = false,
|
||||
const { config } = await import('../../server/config.js')
|
||||
const { startTestApp } = await import('../support/testApp.js')
|
||||
const { setMailTransportForTests } = await import('../../server/services/mailer.js')
|
||||
const { deleteDocument } = await import('../../server/services/appwriteAdmin.js')
|
||||
const { deleteDocument, updateDocument } = await import('../../server/services/appwriteAdmin.js')
|
||||
const { default: publicTermineRoutes, setzeUhrFuerTests: setzeUhrPublic, resetPublicTermineForTests } = await import('../../server/routes/publicTermine.js')
|
||||
const { default: termineRoutes, setzeUhrFuerTests: setzeUhrKunde, resetTermineRoutesForTests } = await import('../../server/routes/termine.js')
|
||||
const { default: funktionenRoutes } = await import('../../server/routes/funktionen.js')
|
||||
const { normalisiereEinstellung } = await import('../../server/services/termine/einstellungen.js')
|
||||
const { speichereEinstellung, resetKontextForTests } = await import('../../server/services/termine/kontext.js')
|
||||
const { speichereEinstellung, speichereFragebogen, resetKontextForTests } = await import('../../server/services/termine/kontext.js')
|
||||
const { resetOutboxForTests, warteAufVersand } = await import('../../server/services/termine/outbox.js')
|
||||
const { resetTermineLimitsForTests } = await import('../../server/services/termine/limits.js')
|
||||
const { SCHRITTE, leerFragebogen } = await import('../../server/services/termine/fragebogen.js')
|
||||
const { featureConfig, featureHaken } = await import('../../server/services/featureRegistry.js')
|
||||
const { hakenTexte, markOrdered } = await import('../../server/services/siteFeatures.js')
|
||||
const { nachweisTexte, speichereZustimmung, zustimmungsfassungen } = await import('../../server/services/featureConsents.js')
|
||||
const { hostingDoc, wartungDoc } = await import('../support/fixtures.js')
|
||||
|
||||
// Testseiten (HTML aus dem Skript) kommen vom Server selbst: Chromium lehnt Anfragen von Seiten, die Playwright per
|
||||
// route.fulfill liefert, an 127.0.0.1 ab (Local Network Access); Seiten desselben Servers duerfen.
|
||||
@@ -233,15 +248,123 @@ export async function startTermineUiServer({ jetzt = JETZT, crossOrigin = false,
|
||||
return { name: roh.slice(0, i), value: roh.slice(i + 1), url: app.url }
|
||||
},
|
||||
|
||||
/**
|
||||
* Wartungs-Abo des Kunden c1 tauschen: 'wartung' (laeuft), 'hosting' (nur Hosting-Abo, Grund kein_abo), 'keins' (gar kein Abo, kein_abo),
|
||||
* 'gekuendigt' (Kuendigung vorgemerkt, Grund gekuendigt) oder 'nicht_aktiv' (Zahlung offen, Grund nicht_aktiv).
|
||||
*/
|
||||
async abo(zustand = 'wartung') {
|
||||
for (const id of [...aw.docs('subscriptions').keys()]) await deleteDocument('subscriptions', id)
|
||||
const docs = {
|
||||
wartung: [wartungDoc],
|
||||
hosting: [hostingDoc],
|
||||
keins: [],
|
||||
gekuendigt: [{ ...wartungDoc, cancelAtPeriodEnd: true }],
|
||||
nicht_aktiv: [{ ...wartungDoc, status: 'past_due' }],
|
||||
}[zustand]
|
||||
if (!docs) throw new Error(`Unbekanntes Abo: ${zustand}`)
|
||||
for (const doc of docs) aw.seed('subscriptions', doc)
|
||||
},
|
||||
|
||||
/** Cookie einer Kundenansicht (Mitarbeiter Andrej sieht das Portal von c1) fuer page.context().addCookies([...]): { name, value, url }. */
|
||||
async kundenansichtCookie({ customerId = KUNDE } = {}) {
|
||||
const { kundenansichtCookie } = await import('../support/fixtures.js')
|
||||
const roh = kundenansichtCookie({ customerId })
|
||||
const i = roh.indexOf('=')
|
||||
return { name: roh.slice(0, i), value: roh.slice(i + 1), url: app.url }
|
||||
},
|
||||
|
||||
/**
|
||||
* Entwurf wie nach der Branchenwahl, ohne alle Klicks: Funktionsdokument im Zustand off (mit siteKey), Einstellung (Vorgabe: salonEingabe,
|
||||
* gespeichert als Entwurf, also auch ohne Pflichtangaben fuer Gaeste) und Fragebogen (Vorgabe: Branche salon, nichts bestaetigt; fragebogen
|
||||
* ueberschreibt einzelne Felder, z. B. { bestaetigt: srv.SCHRITTE, bundeslandGewaehlt: true }).
|
||||
*/
|
||||
async entwurf({ eingabe = salonEingabe(), fragebogen = {} } = {}) {
|
||||
const erg = normalisiereEinstellung(eingabe, { now: uhr(), entwurf: true })
|
||||
if (erg.fehler) throw new Error(`Einstellung ungueltig: ${JSON.stringify(erg.fehler)}`)
|
||||
resetKontextForTests()
|
||||
aw.seed('portalFeatures', srv.featureDoc('off'))
|
||||
const doc = aw.docs('termineConfig').get(FEATURE)
|
||||
await speichereEinstellung(srv.featureDoc('off'), erg.einstellung, { version: doc ? doc.version : 0, now: uhr() })
|
||||
await speichereFragebogen(srv.featureDoc('off'), () => ({ ...leerFragebogen(), branche: 'salon', ...fragebogen }), { by: `kunde:${KUNDE}`, now: uhr() })
|
||||
},
|
||||
|
||||
/** Gespeicherter Fragebogen (Objekt) oder null; Version der gespeicherten Einstellung (0 ohne Dokument). */
|
||||
fragebogen() {
|
||||
const doc = aw.docs('termineConfig').get(FEATURE)
|
||||
return doc?.fragebogen ? JSON.parse(doc.fragebogen) : null
|
||||
},
|
||||
version() { return aw.docs('termineConfig').get(FEATURE)?.version ?? 0 },
|
||||
SCHRITTE,
|
||||
|
||||
/**
|
||||
* Bestellung im Zustand ordered, wie sie der Checkout hinterlaesst: eine offene Stripe-Sitzung im Nachbau, das Funktionsdokument ordered
|
||||
* (markOrdered) und, mit mitZustimmung, der Zustimmungsnachweis dieser Sitzung (Texte des Registers samt Belehrung, Fassungen, Zeit).
|
||||
* Setzt einen Entwurf voraus (srv.entwurf). Liefert die ID der Sitzung.
|
||||
*/
|
||||
async bestellt({ sitzungId = 'cs_test_ui_bestellt', mitZustimmung = true } = {}) {
|
||||
const register = featureConfig('termine')
|
||||
st.checkoutSessions.set(sitzungId, {
|
||||
id: sitzungId,
|
||||
object: 'checkout.session',
|
||||
url: `https://checkout.stripe.test/c/pay/${sitzungId}`,
|
||||
mode: 'payment',
|
||||
customer: null,
|
||||
client_reference_id: KUNDE,
|
||||
subscription: null,
|
||||
metadata: { customerId: KUNDE, projectId: PROJEKT, featureKey: 'termine' },
|
||||
status: 'open',
|
||||
payment_status: 'unpaid',
|
||||
invoice: null,
|
||||
})
|
||||
const klickAt = uhr().toISOString()
|
||||
await markOrdered({ customerId: KUNDE, projectId: PROJEKT, key: 'termine', sessionId: sitzungId, consent: { text: hakenTexte(register), at: klickAt, ip: '127.0.0.1', userAgent: 'ui-test' } })
|
||||
if (mitZustimmung) {
|
||||
await speichereZustimmung({
|
||||
customerId: KUNDE,
|
||||
projectId: PROJEKT,
|
||||
featureKey: 'termine',
|
||||
stripeSessionId: sitzungId,
|
||||
fassungen: zustimmungsfassungen(register),
|
||||
texte: nachweisTexte(register, { belehrung: (await featureHaken('termine').belehrung?.()) ?? null }),
|
||||
ip: '127.0.0.1',
|
||||
userAgent: 'ui-test',
|
||||
konto: 'Silke Müller <silke@salon-mueller.example>',
|
||||
at: klickAt,
|
||||
})
|
||||
}
|
||||
resetKontextForTests()
|
||||
return sitzungId
|
||||
},
|
||||
|
||||
/**
|
||||
* Bestellung bezahlt, Einrichtungs-Ticket angelegt: das Funktionsdokument im Zustand setup mit Ticketnummer (Ticket im Nachbau, noch nicht
|
||||
* geschlossen, sonst schaltete GET /api/funktionen frei) und Rechnungslink. Setzt ein Funktionsdokument voraus (srv.entwurf, srv.bestellt).
|
||||
*/
|
||||
async einrichtung({ woid = 100342, invoiceUrl = 'https://invoice.stripe.test/i/in_ui1' } = {}) {
|
||||
aw.seed('workorders', { $id: 't_ui1', woid, status: 'In Progress' })
|
||||
await updateDocument('portalFeatures', FEATURE, { state: 'setup', enabled: false, purchaseStatus: 'paid', ticketId: 't_ui1', invoiceUrl })
|
||||
resetKontextForTests()
|
||||
},
|
||||
|
||||
/** Zustimmungsnachweise loeschen (ein ordered ohne Nachweis: der Zustand nach einer abgelaufenen Sitzung oder einem Fehler). */
|
||||
async loescheZustimmungen() {
|
||||
for (const id of [...aw.docs('featureConsents').keys()]) await deleteDocument('featureConsents', id)
|
||||
},
|
||||
|
||||
/**
|
||||
* Alles auf Anfang: Dokumente loeschen, Zaehler und Zwischenspeicher leeren, neue Senke, Einstellung und Funktion neu anlegen.
|
||||
* ohneEinstellung: die Funktion ist aktiv, aber es gibt noch keine Einstellung (erstes Oeffnen im Portal: Vorlage waehlen).
|
||||
* entwurf: ein Kunde vor der ersten Branchenwahl (TP2): kein Funktionsdokument, keine Einstellung, ein laufendes Wartungs-Abo fuer c1 und ein
|
||||
* Konto mit Firma und Anschrift; die Stripe-Sitzungen und Aufrufe des Nachbaus sind leer.
|
||||
*/
|
||||
async zuruecksetzen({ eingabe, uhrNeu = true, ohneEinstellung = false } = {}) {
|
||||
async zuruecksetzen({ eingabe, uhrNeu = true, ohneEinstellung = false, entwurf = false } = {}) {
|
||||
await warteAufVersand()
|
||||
for (const collection of ['portalFeatures', 'termineConfig', 'termineBookings', 'termineClaims', 'termineOutbox', 'customers', 'websiteProjects']) {
|
||||
const sammlungen = ['portalFeatures', 'termineConfig', 'termineBookings', 'termineClaims', 'termineOutbox', 'customers', 'websiteProjects', 'subscriptions', 'featureConsents', 'workorders']
|
||||
for (const collection of sammlungen) {
|
||||
for (const id of [...aw.docs(collection).keys()]) await deleteDocument(collection, id)
|
||||
}
|
||||
st.checkoutSessions.clear()
|
||||
st.calls.length = 0
|
||||
resetKontextForTests()
|
||||
resetOutboxForTests()
|
||||
resetTermineLimitsForTests()
|
||||
@@ -259,7 +382,16 @@ export async function startTermineUiServer({ jetzt = JETZT, crossOrigin = false,
|
||||
srv.sink = mailSink()
|
||||
setMailTransportForTests(srv.sink)
|
||||
aw.seed('websiteProjects', { $id: PROJEKT, customerId: KUNDE, projectName: 'Salon Müller', subdomain: 'salon', liveDomain: 'salon-mueller.example' })
|
||||
aw.seed('customers', { $id: KUNDE, name: 'Silke Müller', email: 'silke@salon-mueller.example' })
|
||||
aw.seed('customers', {
|
||||
$id: KUNDE,
|
||||
name: 'Silke Müller',
|
||||
email: 'silke@salon-mueller.example',
|
||||
...(entwurf ? { companyName: 'Salon Müller', street: 'Hauptstr. 1', postalCode: '67722', city: 'Winnweiler' } : {}),
|
||||
})
|
||||
if (entwurf) {
|
||||
await srv.abo('wartung')
|
||||
return
|
||||
}
|
||||
aw.seed('portalFeatures', srv.featureDoc())
|
||||
if (!ohneEinstellung) await srv.speichere(anpassen(eingabe ?? salonEingabe()))
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user