Merge remote-tracking branch 'origin/test' into feature/oeffnungszeiten

Zusammenführung von TP2 und Webshop im Ticketsystem (origin/test 00fa229) mit Öffnungszeiten live. Die Funktionen
stehen in der Reihenfolge news, termine, shop, zeiten.

Konfliktdateien (6, erwartet war nur CustomerFunktionen.jsx; origin/test enthält seit der Vorbereitung auch die
beiden Webshop-Commits 0a2eab8 und 00fa229): scripts/setup-legal.mjs, src/components/customer/CustomerFunktionen.jsx,
src/lib/funktionenView.js, src/lib/teamApi.js, tests/setup-legal.test.mjs, tests/unit/funktionenView.test.mjs.

Entscheidungen: ersatzTicket-Behandlung der Zeiten und speicherStatus/K16_WARNUNG von TP2 bleiben beide in save()
(Abschalten der Zeiten zeigt abschaltMeldung, jede andere Aktion speicherStatus); Rechtliches-Karten Webshop (sort 10, 11)
vor der Karte der Öffnungszeiten (sort 12, DOCUMENTS hat 12 Einträge); siteKeyOf kennt termine, shop und zeiten; aktionen
behandelt shop und zeiten wie den Terminplaner (ticketbasiert, Abnahme der Zeiten mit hindernis); teamApi trägt Webshop-
und Zeiten-Aufrufe nebeneinander.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-06 06:19:42 +00:00
commit 8319084930
19 files changed
+906 -165

No files matched your search

+8 -4
View File
@@ -52,6 +52,8 @@ test('terminplaner_karten_vorhanden', () => {
{ key: 'leistungsbeschreibung-terminplaner', title: 'Leistungsbeschreibung Terminplaner', sort: 7 },
{ key: 'avv-terminplaner', title: 'AVV Terminplaner (Auftragsverarbeitung)', sort: 8 },
{ key: 'widerrufsbelehrung-funktionen', title: 'Widerrufsbelehrung und Muster-Widerrufsformular (Funktionen)', sort: 9 },
{ key: 'leistungsbeschreibung-webshop', title: 'Leistungsbeschreibung Webshop', sort: 10 },
{ key: 'avv-webshop', title: 'AVV Webshop', sort: 11 },
]
for (const e of erwartet) {
const d = DOCUMENTS.find((x) => x.key === e.key)
@@ -62,8 +64,8 @@ test('terminplaner_karten_vorhanden', () => {
assert.ok(d.liveUrl.startsWith(base))
assert.ok(!/\d{4}-\d{2}-\d{2}/.test(d.liveUrl), 'Alias-Seite ohne Fassungsdatum')
}
// Oeffnungszeiten live: Karte 10, hinter den Terminplaner-Karten; der Schluessel hat 28 Zeichen (WOMS-Grenze 36, Ruling R35)
assert.equal(DOCUMENTS.length, 10, 'keine Analyse-Karten und keine weiteren')
// Oeffnungszeiten live: Karte 12, hinter den Terminplaner- und Webshop-Karten; der Schluessel hat 28 Zeichen (WOMS-Grenze 36, Ruling R35)
assert.equal(DOCUMENTS.length, 12, 'keine Analyse-Karten und keine weiteren')
})
test('zeiten_karte_vorhanden_am_ende', () => {
@@ -73,10 +75,10 @@ test('zeiten_karte_vorhanden_am_ende', () => {
key: 'leistungsbeschreibung-zeiten',
title: 'Leistungsbeschreibung Öffnungszeiten live',
liveUrl: 'https://project.webklar.com/vertragsunterlagen/zeiten-leistungsbeschreibung.html',
sort: 10,
sort: 12,
})
assert.ok(d.key.length <= 36, 'Dokument-ID hoechstens 36 Zeichen')
assert.equal(DOCUMENTS.at(-1), d, 'am Ende von DOCUMENTS, nach den Terminplaner-Karten')
assert.equal(DOCUMENTS.at(-1), d, 'am Ende von DOCUMENTS, nach den Terminplaner- und Webshop-Karten')
assert.equal(DOCUMENTS.findIndex((x) => x.key === 'widerrufsbelehrung-funktionen'), 8, 'Terminplaner-Karten unveraendert davor')
})
@@ -106,6 +108,8 @@ test('dry_run_listet_karten', () => {
assert.ok(zeilen.some((z) => z.startsWith('Rechtstext leistungsbeschreibung-terminplaner:')))
assert.ok(zeilen.some((z) => z.startsWith('Rechtstext avv-terminplaner:')))
assert.ok(zeilen.some((z) => z.startsWith('Rechtstext widerrufsbelehrung-funktionen:')))
assert.ok(zeilen.some((z) => z.startsWith('Rechtstext leistungsbeschreibung-webshop:')))
assert.ok(zeilen.some((z) => z.startsWith('Rechtstext avv-webshop:')))
assert.ok(zeilen.includes('Rechtstext leistungsbeschreibung-zeiten: Leistungsbeschreibung Öffnungszeiten live'))
assert.ok(!/angelegt|vorhanden|Fertig/.test(r.stdout), 'Trockenlauf legt nichts an')
})
+214 -2
View File
@@ -20,12 +20,28 @@ const {
textformNoetig,
termineTextformLabel,
termineChatTemplate,
shopTextformLabel,
shopChatTemplate,
TEXTFORM_FUNKTIONEN,
textformAnsicht,
textformBereit,
featureBody,
speicherStatus,
K16_WARNUNG,
warenartLabel,
artikelzahlLabel,
anfrageStatus,
anfrageZeilen,
anfrageText,
stripeText,
stripeTon,
shopHatEintrag,
begruendungOk,
entscheidungBody,
entscheidungStatus,
} = await loadModule('src/lib/funktionenView.js')
test('FUNKTIONEN: News, Terminplaner und Öffnungszeiten live (in dieser Reihenfolge) mit den Bestätigungstexten', () => {
test('FUNKTIONEN: News, Terminplaner, Webshop und Öffnungszeiten live (in dieser Reihenfolge) mit den Bestätigungstexten', () => {
assert.deepEqual(FUNKTIONEN, [
{
key: 'news',
@@ -45,6 +61,16 @@ test('FUNKTIONEN: News, Terminplaner und Öffnungszeiten live (in dieser Reihenf
ticket: 'Fehlendes Einrichtungs-Ticket neu anlegen?',
},
},
{
key: 'shop',
label: 'Webshop',
bestaetigen: {
activate: 'Webshop ohne Zahlung aktivieren?',
disable: 'Webshop abschalten? Auf der Website sind dann keine Bestellungen mehr möglich.',
abnahme: 'Webshop freischalten? Der Kunde bekommt die Mail „bereit“ und der Shop kann geöffnet werden.',
ticket: 'Fehlendes Einrichtungs-Ticket neu anlegen?',
},
},
{
key: 'zeiten',
label: 'Öffnungszeiten live',
@@ -59,7 +85,7 @@ test('FUNKTIONEN: News, Terminplaner und Öffnungszeiten live (in dieser Reihenf
})
test('FUNKTIONEN: Schlüssel sind eindeutig und jede Funktion hat Aktivieren und Abschalten bestätigt', () => {
assert.deepEqual(FUNKTIONEN.map((f) => f.key), ['news', 'termine', 'zeiten'])
assert.deepEqual(FUNKTIONEN.map((f) => f.key), ['news', 'termine', 'shop', 'zeiten'])
for (const f of FUNKTIONEN) {
assert.ok(f.bestaetigen.activate && f.bestaetigen.disable, f.key)
}
@@ -434,6 +460,71 @@ test('textformNoetig: nur "Aktivieren (ohne Zahlung)" des Terminplaners und nur,
assert.equal(textformNoetig(undefined, 'activate', termine()), false)
})
// Feld "shop" der Portal-Antwort GET /funktionen/:customerId (Fassung = config.shop.fassung)
const shopFeld = (extra = {}) => ({
textformNoetig: true,
avvVersion: '2026-10-05',
links: {
lb: 'https://project.webklar.com/rechtliches/leistungsbeschreibung-webshop-2026-10-05.html',
avv: 'https://project.webklar.com/rechtliches/avv-webshop-2026-10-05.html',
},
...extra,
})
test('textformNoetig: auch das Aktivieren (ohne Zahlung) des Webshops, mit dem Feld shop; die Funktion kommt aus dem Schlüssel', () => {
assert.deepEqual(TEXTFORM_FUNKTIONEN, ['termine', 'shop'])
assert.equal(textformNoetig('shop', 'activate', shopFeld()), true)
assert.equal(textformNoetig('shop', 'activate', shopFeld({ textformNoetig: false })), false)
assert.equal(textformNoetig('shop', 'activate', undefined), false, 'altes Portal ohne Feld shop: wie bisher')
assert.equal(textformNoetig('shop', 'activate', null), false)
assert.equal(textformNoetig('shop', 'activate', shopFeld({ textformNoetig: 'true' })), false, 'nur echtes true')
for (const action of ['disable', 'abnahme', 'ticket']) assert.equal(textformNoetig('shop', action, shopFeld()), false, action)
assert.equal(textformNoetig('news', 'activate', shopFeld()), false)
})
test('shopTextformLabel und shopChatTemplate: wörtlich, mit Fassung, den Erklärungen des Nachweises und den versionierten Adressen', () => {
const l = shopTextformLabel('2026-10-05')
assert.equal(l.lb, 'Leistungsbeschreibung Webshop')
assert.equal(l.avv, 'Auftragsverarbeitungsvertrag Webshop')
assert.equal(
l.before + l.lb + l.between + l.avv + l.after,
'Der Kunde hat der Leistungsbeschreibung Webshop und dem Auftragsverarbeitungsvertrag Webshop (Fassung 2026-10-05) ' +
'in Textform zugestimmt und bestätigt, dass er als Unternehmer in eigenem Namen verkauft, für seine Produkte, Preise, Produktangaben und Rechtstexte selbst verantwortlich ist, ' +
'keine der in der Leistungsbeschreibung ausgeschlossenen Waren verkauft und die Zahlungen seiner Käufer über sein eigenes Stripe-Konto laufen.',
)
const { lb, avv } = shopFeld().links
const text = shopChatTemplate('2026-10-05', { lb, avv })
assert.ok(text.startsWith('Damit wir den Webshop für Dich einschalten können, brauchen wir Deine Zustimmung in Textform. '))
assert.ok(text.includes(`in der Fassung 2026-10-05: ${lb} · ${avv}. `))
assert.match(text, /leistungsbeschreibung-webshop-2026-10-05\.html/)
assert.match(text, /avv-webshop-2026-10-05\.html/)
assert.match(text, /Ich stimme zu/)
assert.match(text, /eigenes Stripe-Konto/)
assert.ok(!text.includes('undefined') && !text.includes('{') && !text.includes('Terminplaner'))
})
test('textformAnsicht(feld, "shop"): Label, Vorlage und Links des Webshops; ohne Schlüssel bleibt es der Terminplaner', () => {
const a = textformAnsicht(shopFeld(), 'shop')
assert.equal(a.version, '2026-10-05')
assert.deepEqual(a.links, shopFeld().links)
assert.deepEqual(a.label, shopTextformLabel('2026-10-05'))
assert.equal(a.vorlage, shopChatTemplate('2026-10-05', shopFeld().links))
assert.deepEqual(textformAnsicht(termine()).label, termineTextformLabel('2026-10-05'))
// Adressen ohne https werden nie Link, ohne beide Adressen keine Vorlage
const schlecht = textformAnsicht(shopFeld({ links: { lb: 'javascript:alert(1)', avv: 'http://x.example/a.html' } }), 'shop')
assert.deepEqual(schlecht.links, { lb: '', avv: '' })
assert.equal(schlecht.vorlage, '')
assert.equal(textformAnsicht(undefined, 'shop').version, '')
})
test('textformBereit mit dem Feld shop: Datum zwischen Fassung und heute, ohne Fassung nie bereit', () => {
const ok = { confirmed: true, date: '2026-10-06', channel: 'email', today: '2026-10-07' }
assert.equal(textformBereit(ok, shopFeld()), true)
assert.equal(textformBereit({ ...ok, date: '2026-10-04' }, shopFeld()), false, 'vor der Fassung')
assert.equal(textformBereit(ok, shopFeld({ avvVersion: '' })), false)
assert.equal(textformBereit(ok, undefined), false)
})
test('termineChatTemplate: wörtlich, mit Fassung und den beiden versionierten Adressen', () => {
const { lb, avv } = termine().links
assert.equal(
@@ -524,3 +615,124 @@ test('featureBody: Aktivieren mit Textform trägt Datum und Weg, sonst bleibt de
'halbe Angaben gehen nicht mit',
)
})
test('speicherStatus: warnung true (K16 nicht gesendet) wird zur Warnung, sonst "Gespeichert."', () => {
assert.deepEqual(speicherStatus({ projects: [], warnung: true }), { ok: false, text: K16_WARNUNG })
assert.match(K16_WARNUNG, /K16/)
assert.match(K16_WARNUNG, /von Hand/)
assert.deepEqual(speicherStatus({ projects: [] }), { ok: true, text: 'Gespeichert.' })
assert.deepEqual(speicherStatus(undefined), { ok: true, text: 'Gespeichert.' })
})
test('aktionen: Webshop verhält sich wie der Terminplaner (ticketbasiert)', () => {
assert.deepEqual(aktionen('off', 'shop'), ['activate'])
assert.deepEqual(aktionen('disabled', 'shop'), ['activate'])
assert.deepEqual(aktionen('ordered', 'shop'), ['disable'])
assert.deepEqual(aktionen('active', 'shop'), ['disable'])
assert.deepEqual(aktionen('setup', 'shop'), ['abnahme', 'disable'])
assert.deepEqual(aktionen('setup', 'shop', { ticketMissing: true }), ['ticket', 'abnahme', 'disable'])
for (const state of ['off', 'ordered', 'active', 'disabled']) assert.deepEqual(aktionen(state, 'shop', { ticketMissing: true }), aktionen(state, 'shop'))
for (const action of aktionen('setup', 'shop', { ticketMissing: true })) assert.ok(FUNKTIONEN.find((f) => f.key === 'shop').bestaetigen[action], `Text für ${action}`)
})
test('siteKeyOf: auch der Webshop zeigt seinen Site-Key', () => {
assert.equal(siteKeyOf({ state: 'active', siteKey: ' abcdefghijklmnop ' }, 'shop'), 'abcdefghijklmnop')
assert.equal(siteKeyOf({ state: 'active' }, 'shop'), '')
})
test('warenartLabel und artikelzahlLabel: Beschriftungen, Unbekanntes bleibt sichtbar', () => {
assert.equal(warenartLabel('waren'), 'Waren')
assert.equal(warenartLabel('lebensmittel'), 'Lebensmittel')
assert.equal(warenartLabel('kleidung'), 'Kleidung')
assert.equal(warenartLabel('kunst'), 'Kunst/Handgemachtes')
assert.equal(warenartLabel('sonstiges'), 'Sonstiges')
assert.equal(warenartLabel('xyz'), 'xyz')
assert.equal(warenartLabel(undefined), '–')
assert.equal(warenartLabel('toString'), 'toString', 'kein Treffer über die Prototypkette')
assert.equal(artikelzahlLabel('bis20'), 'bis 20')
assert.equal(artikelzahlLabel('bis100'), 'bis 100')
assert.equal(artikelzahlLabel('mehr'), 'mehr als 100')
assert.equal(artikelzahlLabel(''), '–')
})
test('anfrageStatus: Ton und Beschriftung', () => {
assert.deepEqual(anfrageStatus('offen'), ['warn', 'Offen'])
assert.deepEqual(anfrageStatus('angenommen'), ['ok', 'Angenommen'])
assert.deepEqual(anfrageStatus('abgelehnt'), ['danger', 'Abgelehnt'])
assert.deepEqual(anfrageStatus('zurueckgezogen'), ['muted', 'Zurückgezogen'])
assert.deepEqual(anfrageStatus(undefined), ['muted', '–'])
})
const ANFRAGE = { warenart: 'kunst', beschreibung: ' Handgemachte Kerzen ', artikelzahl: 'bis20', versand: true, abholung: false, unternehmer: true, keineAusgeschlossenen: true, nachricht: '' }
test('anfrageZeilen: alle Angaben der Anfrage als Zeilen', () => {
assert.deepEqual(anfrageZeilen(ANFRAGE), [
{ label: 'Warenart', text: 'Kunst/Handgemachtes' },
{ label: 'Beschreibung', text: 'Handgemachte Kerzen' },
{ label: 'Artikelzahl', text: 'bis 20' },
{ label: 'Übergabe', text: 'Versand' },
{ label: 'Unternehmer', text: 'Ja' },
{ label: 'Keine ausgeschlossenen Waren', text: 'Ja' },
{ label: 'Nachricht', text: '–' },
])
assert.equal(anfrageZeilen({ ...ANFRAGE, abholung: true }).find((z) => z.label === 'Übergabe').text, 'Versand und Abholung')
assert.equal(anfrageZeilen({ ...ANFRAGE, versand: false }).find((z) => z.label === 'Übergabe').text, '–')
assert.deepEqual(anfrageZeilen(null), [])
})
test('anfrageText: eine Zeile aus Warenart, Menge und Übergabe', () => {
assert.equal(anfrageText(ANFRAGE), 'Kunst/Handgemachtes, bis 20 Artikel, Versand')
assert.equal(anfrageText({ ...ANFRAGE, abholung: true }), 'Kunst/Handgemachtes, bis 20 Artikel, Versand und Abholung')
assert.equal(anfrageText({ warenart: 'waren' }), 'Waren')
assert.equal(anfrageText(null), '')
})
test('stripeText: nicht verbunden, aktiv, Angaben fehlen', () => {
assert.equal(stripeText(null, false), 'nicht verbunden')
assert.equal(stripeText(undefined), 'nicht verbunden')
assert.equal(stripeText(null, true), 'Konto angelegt, Stand noch nicht geprüft')
assert.equal(stripeText({ charges_enabled: true, payouts_enabled: true }, true), 'Zahlungen aktiv')
assert.equal(stripeText({ charges_enabled: true, payouts_enabled: false }, true), 'Zahlungen aktiv, Auszahlungen noch nicht freigegeben')
assert.equal(stripeText({ charges_enabled: false, currently_due: ['external_account', 'individual.dob.day'], past_due: [] }, true), 'Angaben fehlen: external_account, individual.dob.day')
assert.equal(stripeText({ charges_enabled: false, past_due: ['a'], currently_due: ['a', 'b'] }, true), 'Angaben fehlen: a, b', 'doppelte Einträge nur einmal')
assert.equal(stripeText({ charges_enabled: false, currently_due: ['a', 'b', 'c', 'd', 'e', 'f'] }, true), 'Angaben fehlen: a, b, c, d und 2 weitere')
assert.equal(stripeText({ charges_enabled: false, currently_due: [], disabled_reason: 'requirements.pending_verification' }, true), 'Angaben fehlen: requirements.pending_verification')
assert.equal(stripeText({ charges_enabled: false, details_submitted: true }, true), 'Angaben fehlen: Prüfung durch Stripe läuft')
assert.equal(stripeText({ charges_enabled: false, details_submitted: false }, true), 'Angaben fehlen: Einrichtung nicht abgeschlossen')
})
test('stripeTon: ok, warn, muted', () => {
assert.equal(stripeTon({ charges_enabled: true }, true), 'ok')
assert.equal(stripeTon({ charges_enabled: false }, true), 'warn')
assert.equal(stripeTon(null, true), 'warn')
assert.equal(stripeTon(null, false), 'muted')
})
test('shopHatEintrag: Anfrage oder Zustand außer "off"', () => {
assert.equal(shopHatEintrag({ state: 'off', anfrage: null }), false)
assert.equal(shopHatEintrag({ state: 'off', anfrage: { status: 'offen' } }), true)
assert.equal(shopHatEintrag({ state: 'active', anfrage: null }), true)
assert.equal(shopHatEintrag({ state: 'disabled' }), true)
assert.equal(shopHatEintrag(null), false)
})
test('begruendungOk: Pflicht, höchstens 1000 Zeichen', () => {
assert.equal(begruendungOk(''), false)
assert.equal(begruendungOk(' '), false)
assert.equal(begruendungOk(undefined), false)
assert.equal(begruendungOk('Passt nicht'), true)
assert.equal(begruendungOk('x'.repeat(1000)), true)
assert.equal(begruendungOk('x'.repeat(1001)), false)
assert.equal(begruendungOk(` ${'x'.repeat(1000)} `), true, 'ohne Rand gezählt')
})
test('entscheidungBody: Begründung nur beim Ablehnen, getrimmt', () => {
assert.deepEqual(entscheidungBody({ projectId: 'p1', entscheidung: 'annehmen', begruendung: 'egal' }), { projectId: 'p1', entscheidung: 'annehmen' })
assert.deepEqual(entscheidungBody({ projectId: 'p1', entscheidung: 'ablehnen', begruendung: ' Zu groß ' }), { projectId: 'p1', entscheidung: 'ablehnen', begruendung: 'Zu groß' })
})
test('entscheidungStatus: Warnung, wenn die Mail an den Kunden fehlschlug', () => {
assert.deepEqual(entscheidungStatus({ ok: true }), { ok: true, text: 'Gespeichert.' })
assert.deepEqual(entscheidungStatus({ ok: true, mailFehler: true }), { ok: false, text: 'Entscheidung gespeichert, Mail an den Kunden fehlgeschlagen.' })
assert.deepEqual(entscheidungStatus(null), { ok: true, text: 'Gespeichert.' })
})
+34
View File
@@ -0,0 +1,34 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { throttleRuns } = await loadModule('src/lib/throttleRuns.js')
test('Repo-Import hoechstens einmal pro Minute, laufender Abgleich wird geteilt', async () => {
let runs = 0
let release
const throttled = throttleRuns(() => { runs += 1; return new Promise((r) => { release = r }) }, 60000)
const a = throttled()
const b = throttled()
assert.equal(a, b)
await new Promise((r) => setTimeout(r, 0))
release({ created: 0 })
await a
assert.equal(runs, 1)
assert.equal(await throttled(), null)
assert.equal(runs, 1)
const later = throttled({ now: Date.now() + 61000 })
await new Promise((r) => setTimeout(r, 0))
release({ created: 1 })
assert.deepEqual(await later, { created: 1 })
assert.equal(runs, 2)
})
test('Fehler wird weitergereicht, danach darf sofort neu versucht werden', async () => {
let runs = 0
const throttled = throttleRuns(async () => { runs += 1; if (runs === 1) throw new Error('weg'); return { ok: true } }, 60000)
await assert.rejects(throttled(), /weg/)
assert.deepEqual(await throttled(), { ok: true })
assert.equal(runs, 2)
assert.equal(await throttled(), null)
})
+30 -3
View File
@@ -2,7 +2,7 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { isOutsideOrg, needsPreviewButton, assignSummary, assignSequentially } = await loadModule(
const { isOutsideOrg, needsPreviewButton, assignSummary, assignSequentially, assignableProjects, ticketIdForCustomer } = await loadModule(
'src/lib/projectAssign.js'
)
@@ -17,9 +17,36 @@ test('isOutsideOrg', () => {
})
test('needsPreviewButton', () => {
assert.equal(needsPreviewButton({ previewUrl: 'https://x', status: 'deployed' }), false)
assert.equal(needsPreviewButton({ subdomain: 'x', previewUrl: 'https://x', status: 'deployed' }), false)
assert.equal(needsPreviewButton({ previewUrl: '', status: 'synced' }), true)
assert.equal(needsPreviewButton({ previewUrl: 'https://x', status: 'pending' }), true)
// gerade angelegt, der Runner baut noch (status noch synced/pending): kein "Preview einschalten"
assert.equal(needsPreviewButton({ subdomain: 'x', previewUrl: 'https://x', status: 'synced' }), false)
assert.equal(needsPreviewButton({ subdomain: 'x', previewUrl: 'https://x', status: 'pending' }), false)
// archiviert: erst reaktivieren
assert.equal(needsPreviewButton({ archived: true, status: 'archived' }), false)
})
test('assignableProjects: Flag archived zaehlt, nicht status', () => {
const all = [
{ $id: 'a', repoFullName: 'WEBklar/a', status: 'archived', archived: false }, // reaktiviert, alter status
{ $id: 'b', repoFullName: 'JUSN/b', status: 'synced', archived: true }, // in Gitea archiviert
{ $id: 'c', repoFullName: 'WEBklar/c', customerId: 'k2' }, // anderer Kunde
{ $id: 'd', repoFullName: 'WEBklar/d', customerId: 'k1' },
{ $id: 'e', repoFullName: 'WEBklar/e', isTemplate: true },
{ $id: 'f', projectName: 'ohne Repo' },
{ $id: 'g', repoFullName: 'WEBklar/g' },
]
const ids = assignableProjects(all, { customerId: 'k1' }, new Set(['g'])).map((p) => p.$id)
assert.deepEqual(ids, ['a', 'd'])
})
test('ticketIdForCustomer: altes Ticket nur beim gleichen Kunden', () => {
const p = { customerId: 'A', ticketId: 'tA' }
const wo = { tA: { customerId: 'A' }, tB: { customerId: 'B' } }
assert.equal(ticketIdForCustomer(p, 'A', wo), 'tA')
assert.equal(ticketIdForCustomer(p, 'B', wo), '')
assert.equal(ticketIdForCustomer({ customerId: 'A', ticketId: 'tB' }, 'B', wo), 'tB')
assert.equal(ticketIdForCustomer({ customerId: '', ticketId: '' }, 'B', wo), '')
})
test('assignSummary', () => {
+78
View File
@@ -0,0 +1,78 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { projectCounts, projectsInView, viewShowing, matchesSearch, hiddenMatches } = await loadModule('src/lib/projectList.js')
const P = [
{ $id: 'a' },
{ $id: 'b', customerId: 'k' },
{ $id: 'c', archived: true },
{ $id: 'd', isTemplate: true },
{ $id: 'e', isTemplate: true, archived: true },
]
const ids = (list) => list.map((p) => p.$id)
test('Aktive Projekte ohne Archiv und Vorlagen', () => {
assert.deepEqual(ids(projectsInView(P, 'all')), ['a', 'b'])
assert.deepEqual(ids(projectsInView(P, 'unassigned')), ['a'])
assert.deepEqual(ids(projectsInView(P, 'assigned')), ['b'])
})
test('Alle inkl. Archiv: wirklich alles (Vorlagen nur fuer Admins)', () => {
assert.deepEqual(ids(projectsInView(P, 'everything')), ['a', 'b', 'c'])
assert.deepEqual(ids(projectsInView(P, 'everything', { isAdmin: true })), ['a', 'b', 'c', 'd', 'e'])
})
test('Archiviert und Vorlagen', () => {
assert.deepEqual(ids(projectsInView(P, 'archived')), ['c'])
assert.deepEqual(ids(projectsInView(P, 'archived', { isAdmin: true })), ['c', 'e'])
assert.deepEqual(ids(projectsInView(P, 'templates')), ['d'])
})
test('Zaehler passen zu den Ansichten', () => {
assert.deepEqual(projectCounts(P), { all: 2, unassigned: 1, assigned: 1, archived: 1, templates: 1, everything: 3 })
assert.deepEqual(projectCounts(P, { isAdmin: true }).everything, 5)
})
// 04.10.2026: SHK-vorlage zur Vorlage gemacht -> verschwand kommentarlos aus "Aktive Projekte"
test('viewShowing: bleibt in der aktuellen Ansicht, wenn das Projekt dort steht', () => {
assert.equal(viewShowing(P[0], 'all', { isAdmin: true }), 'all')
assert.equal(viewShowing(P[1], 'assigned'), 'assigned')
assert.equal(viewShowing(P[3], 'everything', { isAdmin: true }), 'everything')
})
test('viewShowing: zur Vorlage gemacht -> Vorlagen, zurueck -> Aktive Projekte', () => {
assert.equal(viewShowing({ $id: 'x', isTemplate: true }, 'all', { isAdmin: true }), 'templates')
assert.equal(viewShowing({ $id: 'x', isTemplate: true }, 'unassigned', { isAdmin: true }), 'templates')
assert.equal(viewShowing({ $id: 'x' }, 'templates', { isAdmin: true }), 'all')
assert.equal(viewShowing({ $id: 'x', archived: true }, 'all'), 'archived')
})
test('viewShowing: ohne Admin keine Vorlagen-Ansicht, dann bleibt die Ansicht', () => {
assert.equal(viewShowing({ $id: 'x', isTemplate: true }, 'all'), 'all')
assert.equal(viewShowing(null, 'assigned'), 'assigned')
})
test('matchesSearch: Titel, Subdomain, Repo; leer passt immer', () => {
const p = { projectName: 'SHK-vorlage', subdomain: 'shk-vorlage', repoFullName: 'knso/SHK-vorlage', giteaRepoName: 'SHK-vorlage' }
assert.equal(matchesSearch(p, ''), true)
assert.equal(matchesSearch(p, ' shk '), true)
assert.equal(matchesSearch(p, 'knso/'), true)
assert.equal(matchesSearch(p, 'olzinger'), false)
})
test('hiddenMatches: Treffer, die die aktuelle Ansicht ausblendet', () => {
const list = [
{ $id: 'a', projectName: 'SHK Kunde' },
{ $id: 't', projectName: 'SHK-vorlage', isTemplate: true },
{ $id: 'r', projectName: 'SHK alt', archived: true },
{ $id: 'o', projectName: 'Olzinger' },
]
const shk = (p) => matchesSearch(p, 'shk')
assert.deepEqual(hiddenMatches(list, 'all', shk, { isAdmin: true }), [{ view: 'templates', count: 1 }, { view: 'archived', count: 1 }])
assert.deepEqual(hiddenMatches(list, 'all', shk), [{ view: 'archived', count: 1 }], 'Vorlagen nur fuer Admins')
assert.deepEqual(hiddenMatches(list, 'templates', shk, { isAdmin: true }), [{ view: 'archived', count: 1 }])
assert.deepEqual(hiddenMatches(list, 'everything', shk, { isAdmin: true }), [], 'zeigt schon alles')
assert.deepEqual(hiddenMatches(list, 'all', (p) => matchesSearch(p, 'olz'), { isAdmin: true }), [])
})
+8 -1
View File
@@ -2,7 +2,14 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { suggestSubdomain } = await loadModule('src/lib/subdomain.js')
const { suggestSubdomain, slugify } = await loadModule('src/lib/subdomain.js')
test('slugify schreibt Umlaute aus wie das Portal', () => {
assert.equal(slugify('Müller & Söhne'), 'mueller-soehne')
assert.equal(slugify(' ZZ Test Ablauf '), 'zz-test-ablauf')
assert.equal(slugify('x'.repeat(70)), 'x'.repeat(63))
assert.equal(slugify(null), '')
})
test('Kundenname vor Projektname', () => {
assert.equal(suggestSubdomain('Döner Nation', 'D-ner-Nation'), 'doener-nation')