2026-04-07 23:50:19 +02:00
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
|
|
|
|
|
|
|
|
// Mock the entreprise-search module since it's imported by app.js
|
|
|
|
|
vi.mock('../../assets/modules/entreprise-search.js', () => ({
|
|
|
|
|
initEntrepriseSearch: vi.fn(),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
// Mock the scss import
|
|
|
|
|
vi.mock('../../assets/app.scss', () => ({}))
|
|
|
|
|
|
|
|
|
|
// localStorage mock
|
|
|
|
|
const localStorageMock = (() => {
|
|
|
|
|
let store = {}
|
|
|
|
|
return {
|
|
|
|
|
getItem: vi.fn((key) => store[key] ?? null),
|
|
|
|
|
setItem: vi.fn((key, value) => { store[key] = String(value) }),
|
|
|
|
|
removeItem: vi.fn((key) => { delete store[key] }),
|
|
|
|
|
clear: vi.fn(() => { store = {} }),
|
|
|
|
|
get length() { return Object.keys(store).length },
|
|
|
|
|
key: vi.fn((i) => Object.keys(store)[i] ?? null),
|
|
|
|
|
}
|
|
|
|
|
})()
|
|
|
|
|
|
|
|
|
|
Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, writable: true })
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
|
test: couverture JS 100% lignes app.js (73 tests) + PHP 100% methodes
JS (app.js) :
- 73 tests (etait 39), 100% lignes, 98% statements, 99% fonctions
- initTabSearch : 7 tests (recherche devis/factures/avis par onglet,
query courte, resultats vides, click outside, Escape, labels etats)
- initDevisLines : 18 tests (ajout/suppression lignes, renumerotation,
recalcul total, quick-price-btn, validation formulaire esymail,
chargement services par type, drag & drop reordering, prefill initial)
- Recherche globale : 5 tests (query courte, resultats, type inconnu)
- initStripePayment : marque istanbul ignore (interaction Stripe)
PHP : 1179 tests, 2369 assertions, 100% methodes toutes classes App
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:08:36 +02:00
|
|
|
// Track DOMContentLoaded listeners so we can remove them between tests
|
|
|
|
|
let domContentLoadedListeners = []
|
|
|
|
|
const originalAddEventListener = document.addEventListener.bind(document)
|
|
|
|
|
const originalRemoveEventListener = document.removeEventListener.bind(document)
|
|
|
|
|
document.addEventListener = function(type, listener, options) {
|
|
|
|
|
if (type === 'DOMContentLoaded') {
|
|
|
|
|
domContentLoadedListeners.push(listener)
|
|
|
|
|
}
|
|
|
|
|
return originalAddEventListener(type, listener, options)
|
|
|
|
|
}
|
|
|
|
|
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
describe('app.js DOMContentLoaded', () => {
|
|
|
|
|
beforeEach(() => {
|
test: couverture JS 100% lignes app.js (73 tests) + PHP 100% methodes
JS (app.js) :
- 73 tests (etait 39), 100% lignes, 98% statements, 99% fonctions
- initTabSearch : 7 tests (recherche devis/factures/avis par onglet,
query courte, resultats vides, click outside, Escape, labels etats)
- initDevisLines : 18 tests (ajout/suppression lignes, renumerotation,
recalcul total, quick-price-btn, validation formulaire esymail,
chargement services par type, drag & drop reordering, prefill initial)
- Recherche globale : 5 tests (query courte, resultats, type inconnu)
- initStripePayment : marque istanbul ignore (interaction Stripe)
PHP : 1179 tests, 2369 assertions, 100% methodes toutes classes App
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:08:36 +02:00
|
|
|
// Remove all previously registered DOMContentLoaded listeners to prevent accumulation
|
|
|
|
|
domContentLoadedListeners.forEach(listener => {
|
|
|
|
|
originalRemoveEventListener('DOMContentLoaded', listener)
|
|
|
|
|
})
|
|
|
|
|
domContentLoadedListeners = []
|
|
|
|
|
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
document.body.innerHTML = ''
|
2026-04-07 23:50:19 +02:00
|
|
|
localStorageMock.clear()
|
|
|
|
|
vi.restoreAllMocks()
|
|
|
|
|
// Re-apply localStorage mock after restoreAllMocks
|
|
|
|
|
localStorageMock.getItem.mockImplementation((key) => {
|
|
|
|
|
// Use internal store - reimplemented per test via setItem
|
|
|
|
|
return null
|
|
|
|
|
})
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const loadApp = async () => {
|
|
|
|
|
vi.resetModules()
|
2026-04-07 23:50:19 +02:00
|
|
|
|
|
|
|
|
// Re-mock the modules before re-import
|
|
|
|
|
vi.doMock('../../assets/modules/entreprise-search.js', () => ({
|
|
|
|
|
initEntrepriseSearch: vi.fn(),
|
|
|
|
|
}))
|
|
|
|
|
vi.doMock('../../assets/app.scss', () => ({}))
|
|
|
|
|
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
await import('../../assets/app.js')
|
|
|
|
|
document.dispatchEvent(new Event('DOMContentLoaded'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('Member/Admin checkboxes', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
document.body.innerHTML = `
|
feat: comptabilite + prestataires + rapport financier + stats dynamiques
Comptabilite (Super Admin) :
- ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE
(journal ventes, grand livre, FEC, balance agee, reglements,
commissions Stripe 1.5%+0.25E, couts services)
- Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli,
tableau pagine, champ signature DocuSeal
- Signature electronique DocuSeal + callback + envoi email signe
avec template dedie (compta_export_signed.html.twig)
- Rapport financier public (RapportFinancierPdf) : recettes par
service, depenses (Stripe, infra, prestataires), bilan excedent/deficit
- Codes comptables clients EC-XXXX (plus de 411xxx)
Prestataires (Super Admin) :
- Entite Prestataire (raisonSociale, siret, email, phone, adresse)
- Entite FacturePrestataire (numFacture, montantHt, montantTtc,
year, month, isPaid, PDF via Vich)
- CRUD complet avec recherche SIRET via proxy API data.gouv.fr
- Commande cron app:reminder:factures-prestataire (5 du mois)
- Factures prestataires integrees dans export couts services
- Sidebar Super Admin : entree Prestataires + Comptabilite
Stats (/admin/stats) :
- Cout prestataire dynamique depuis FacturePrestataire
- Fusion Infra + Prestataire en "Cout de fonctionnement"
- Commission Stripe corrigee (1.5% + 0.25E par transaction)
Divers :
- DocuSealService::sendComptaForSignature() + getApi()
- Customer::generateCodeComptable() format EC-XXXX-XXXXX
- Protection double prefixe EC- a la creation client
- Bouton regenerer PDF cache quand advert state=accepted
- Modals sans script inline (data-modal-open/close dans app.js)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:39:31 +02:00
|
|
|
<input type="checkbox" name="groups[]" value="gp_member" checked>
|
|
|
|
|
<input type="checkbox" name="groups[]" value="superadmin">
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
<input type="checkbox" name="groups[]" value="esy-web">
|
|
|
|
|
<input type="checkbox" name="groups[]" value="esy-mail">
|
|
|
|
|
`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('unchecks other groups when member is checked', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
feat: comptabilite + prestataires + rapport financier + stats dynamiques
Comptabilite (Super Admin) :
- ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE
(journal ventes, grand livre, FEC, balance agee, reglements,
commissions Stripe 1.5%+0.25E, couts services)
- Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli,
tableau pagine, champ signature DocuSeal
- Signature electronique DocuSeal + callback + envoi email signe
avec template dedie (compta_export_signed.html.twig)
- Rapport financier public (RapportFinancierPdf) : recettes par
service, depenses (Stripe, infra, prestataires), bilan excedent/deficit
- Codes comptables clients EC-XXXX (plus de 411xxx)
Prestataires (Super Admin) :
- Entite Prestataire (raisonSociale, siret, email, phone, adresse)
- Entite FacturePrestataire (numFacture, montantHt, montantTtc,
year, month, isPaid, PDF via Vich)
- CRUD complet avec recherche SIRET via proxy API data.gouv.fr
- Commande cron app:reminder:factures-prestataire (5 du mois)
- Factures prestataires integrees dans export couts services
- Sidebar Super Admin : entree Prestataires + Comptabilite
Stats (/admin/stats) :
- Cout prestataire dynamique depuis FacturePrestataire
- Fusion Infra + Prestataire en "Cout de fonctionnement"
- Commission Stripe corrigee (1.5% + 0.25E par transaction)
Divers :
- DocuSealService::sendComptaForSignature() + getApi()
- Customer::generateCodeComptable() format EC-XXXX-XXXXX
- Protection double prefixe EC- a la creation client
- Bouton regenerer PDF cache quand advert state=accepted
- Modals sans script inline (data-modal-open/close dans app.js)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:39:31 +02:00
|
|
|
const member = document.querySelector('[value="gp_member"]')
|
|
|
|
|
const admin = document.querySelector('[value="superadmin"]')
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
const esyWeb = document.querySelector('[value="esy-web"]')
|
|
|
|
|
|
|
|
|
|
admin.checked = true
|
|
|
|
|
esyWeb.checked = true
|
|
|
|
|
|
|
|
|
|
member.checked = true
|
|
|
|
|
member.dispatchEvent(new Event('change'))
|
|
|
|
|
|
|
|
|
|
expect(admin.checked).toBe(false)
|
|
|
|
|
expect(esyWeb.checked).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('checks all groups and unchecks member when admin is checked', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
feat: comptabilite + prestataires + rapport financier + stats dynamiques
Comptabilite (Super Admin) :
- ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE
(journal ventes, grand livre, FEC, balance agee, reglements,
commissions Stripe 1.5%+0.25E, couts services)
- Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli,
tableau pagine, champ signature DocuSeal
- Signature electronique DocuSeal + callback + envoi email signe
avec template dedie (compta_export_signed.html.twig)
- Rapport financier public (RapportFinancierPdf) : recettes par
service, depenses (Stripe, infra, prestataires), bilan excedent/deficit
- Codes comptables clients EC-XXXX (plus de 411xxx)
Prestataires (Super Admin) :
- Entite Prestataire (raisonSociale, siret, email, phone, adresse)
- Entite FacturePrestataire (numFacture, montantHt, montantTtc,
year, month, isPaid, PDF via Vich)
- CRUD complet avec recherche SIRET via proxy API data.gouv.fr
- Commande cron app:reminder:factures-prestataire (5 du mois)
- Factures prestataires integrees dans export couts services
- Sidebar Super Admin : entree Prestataires + Comptabilite
Stats (/admin/stats) :
- Cout prestataire dynamique depuis FacturePrestataire
- Fusion Infra + Prestataire en "Cout de fonctionnement"
- Commission Stripe corrigee (1.5% + 0.25E par transaction)
Divers :
- DocuSealService::sendComptaForSignature() + getApi()
- Customer::generateCodeComptable() format EC-XXXX-XXXXX
- Protection double prefixe EC- a la creation client
- Bouton regenerer PDF cache quand advert state=accepted
- Modals sans script inline (data-modal-open/close dans app.js)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:39:31 +02:00
|
|
|
const member = document.querySelector('[value="gp_member"]')
|
|
|
|
|
const admin = document.querySelector('[value="superadmin"]')
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
const esyWeb = document.querySelector('[value="esy-web"]')
|
|
|
|
|
|
|
|
|
|
admin.checked = true
|
|
|
|
|
admin.dispatchEvent(new Event('change'))
|
|
|
|
|
|
|
|
|
|
expect(member.checked).toBe(false)
|
|
|
|
|
expect(esyWeb.checked).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does nothing when admin is unchecked', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
feat: comptabilite + prestataires + rapport financier + stats dynamiques
Comptabilite (Super Admin) :
- ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE
(journal ventes, grand livre, FEC, balance agee, reglements,
commissions Stripe 1.5%+0.25E, couts services)
- Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli,
tableau pagine, champ signature DocuSeal
- Signature electronique DocuSeal + callback + envoi email signe
avec template dedie (compta_export_signed.html.twig)
- Rapport financier public (RapportFinancierPdf) : recettes par
service, depenses (Stripe, infra, prestataires), bilan excedent/deficit
- Codes comptables clients EC-XXXX (plus de 411xxx)
Prestataires (Super Admin) :
- Entite Prestataire (raisonSociale, siret, email, phone, adresse)
- Entite FacturePrestataire (numFacture, montantHt, montantTtc,
year, month, isPaid, PDF via Vich)
- CRUD complet avec recherche SIRET via proxy API data.gouv.fr
- Commande cron app:reminder:factures-prestataire (5 du mois)
- Factures prestataires integrees dans export couts services
- Sidebar Super Admin : entree Prestataires + Comptabilite
Stats (/admin/stats) :
- Cout prestataire dynamique depuis FacturePrestataire
- Fusion Infra + Prestataire en "Cout de fonctionnement"
- Commission Stripe corrigee (1.5% + 0.25E par transaction)
Divers :
- DocuSealService::sendComptaForSignature() + getApi()
- Customer::generateCodeComptable() format EC-XXXX-XXXXX
- Protection double prefixe EC- a la creation client
- Bouton regenerer PDF cache quand advert state=accepted
- Modals sans script inline (data-modal-open/close dans app.js)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:39:31 +02:00
|
|
|
const member = document.querySelector('[value="gp_member"]')
|
|
|
|
|
const admin = document.querySelector('[value="superadmin"]')
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
|
|
|
|
|
member.checked = true
|
|
|
|
|
admin.checked = false
|
|
|
|
|
admin.dispatchEvent(new Event('change'))
|
|
|
|
|
|
|
|
|
|
expect(member.checked).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('Stats period selector', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<select id="stats-period-select">
|
|
|
|
|
<option value="current">Mois en cours</option>
|
|
|
|
|
<option value="custom">Personnalise</option>
|
|
|
|
|
</select>
|
|
|
|
|
<div id="stats-custom-range" class="hidden"></div>
|
|
|
|
|
`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows custom range when custom is selected', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
const select = document.getElementById('stats-period-select')
|
|
|
|
|
const range = document.getElementById('stats-custom-range')
|
|
|
|
|
|
|
|
|
|
select.value = 'custom'
|
|
|
|
|
select.dispatchEvent(new Event('change'))
|
|
|
|
|
|
|
|
|
|
expect(range.classList.contains('hidden')).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides custom range when current is selected', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
const select = document.getElementById('stats-period-select')
|
|
|
|
|
const range = document.getElementById('stats-custom-range')
|
|
|
|
|
|
|
|
|
|
select.value = 'current'
|
|
|
|
|
select.dispatchEvent(new Event('change'))
|
|
|
|
|
|
|
|
|
|
expect(range.classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('data-confirm forms', () => {
|
2026-04-07 23:50:19 +02:00
|
|
|
it('prevents submission and shows confirm modal', async () => {
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
document.body.innerHTML = '<form data-confirm="Etes-vous sur ?"><button type="submit">Submit</button></form>'
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
const form = document.querySelector('form')
|
|
|
|
|
const event = new Event('submit', { cancelable: true })
|
|
|
|
|
form.dispatchEvent(event)
|
|
|
|
|
|
2026-04-07 23:50:19 +02:00
|
|
|
// The custom confirm modal should prevent default
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
expect(event.defaultPrevented).toBe(true)
|
2026-04-07 23:50:19 +02:00
|
|
|
|
|
|
|
|
// The confirm modal should be visible
|
|
|
|
|
const confirmModal = document.getElementById('confirm-modal')
|
|
|
|
|
expect(confirmModal).not.toBeNull()
|
|
|
|
|
expect(confirmModal.classList.contains('hidden')).toBe(false)
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
})
|
|
|
|
|
|
2026-04-07 23:50:19 +02:00
|
|
|
it('closes confirm modal on cancel click', async () => {
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
document.body.innerHTML = '<form data-confirm="Etes-vous sur ?"><button type="submit">Submit</button></form>'
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
const form = document.querySelector('form')
|
2026-04-07 23:50:19 +02:00
|
|
|
form.dispatchEvent(new Event('submit', { cancelable: true }))
|
|
|
|
|
|
|
|
|
|
const confirmModal = document.getElementById('confirm-modal')
|
|
|
|
|
expect(confirmModal.classList.contains('hidden')).toBe(false)
|
|
|
|
|
|
|
|
|
|
// Click cancel
|
|
|
|
|
document.getElementById('confirm-cancel').click()
|
|
|
|
|
expect(confirmModal.classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('closes confirm modal on overlay click', async () => {
|
|
|
|
|
document.body.innerHTML = '<form data-confirm="Etes-vous sur ?"><button type="submit">Submit</button></form>'
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
const form = document.querySelector('form')
|
|
|
|
|
form.dispatchEvent(new Event('submit', { cancelable: true }))
|
|
|
|
|
|
|
|
|
|
const confirmModal = document.getElementById('confirm-modal')
|
|
|
|
|
expect(confirmModal.classList.contains('hidden')).toBe(false)
|
|
|
|
|
|
|
|
|
|
document.getElementById('confirm-overlay').click()
|
|
|
|
|
expect(confirmModal.classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('closes confirm modal on Escape key', async () => {
|
|
|
|
|
document.body.innerHTML = '<form data-confirm="Etes-vous sur ?"><button type="submit">Submit</button></form>'
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
const form = document.querySelector('form')
|
|
|
|
|
form.dispatchEvent(new Event('submit', { cancelable: true }))
|
|
|
|
|
|
|
|
|
|
const confirmModal = document.getElementById('confirm-modal')
|
|
|
|
|
expect(confirmModal.classList.contains('hidden')).toBe(false)
|
|
|
|
|
|
|
|
|
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
|
|
|
expect(confirmModal.classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('submits form when confirm OK is clicked', async () => {
|
|
|
|
|
document.body.innerHTML = '<form data-confirm="Etes-vous sur ?"><button type="submit">Submit</button></form>'
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
const form = document.querySelector('form')
|
|
|
|
|
form.requestSubmit = vi.fn()
|
|
|
|
|
|
|
|
|
|
form.dispatchEvent(new Event('submit', { cancelable: true }))
|
|
|
|
|
|
|
|
|
|
// Click OK to confirm
|
|
|
|
|
document.getElementById('confirm-ok').click()
|
|
|
|
|
|
|
|
|
|
expect(form.requestSubmit).toHaveBeenCalled()
|
|
|
|
|
expect(document.getElementById('confirm-modal').classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('displays the confirm message from data attribute', async () => {
|
|
|
|
|
document.body.innerHTML = '<form data-confirm="Voulez-vous supprimer ?"><button type="submit">Submit</button></form>'
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
const form = document.querySelector('form')
|
|
|
|
|
form.dispatchEvent(new Event('submit', { cancelable: true }))
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
|
2026-04-07 23:50:19 +02:00
|
|
|
expect(document.getElementById('confirm-message').textContent).toBe('Voulez-vous supprimer ?')
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('Sidebar dropdown', () => {
|
2026-04-03 10:42:44 +02:00
|
|
|
it('toggles dropdown menu and arrow on click', async () => {
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
document.body.innerHTML = `<div><button class="sidebar-dropdown-btn"><span class="sidebar-dropdown-arrow"></span></button><ul class="hidden">Menu</ul></div>`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const btn = document.querySelector('.sidebar-dropdown-btn')
|
2026-04-03 10:42:44 +02:00
|
|
|
const menu = btn.nextElementSibling
|
|
|
|
|
const arrow = btn.querySelector('.sidebar-dropdown-arrow')
|
|
|
|
|
|
|
|
|
|
expect(menu.classList.contains('hidden')).toBe(true)
|
|
|
|
|
expect(arrow.classList.contains('rotate-180')).toBe(false)
|
|
|
|
|
|
2026-04-07 23:50:19 +02:00
|
|
|
btn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
2026-04-03 10:42:44 +02:00
|
|
|
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
expect(btn).not.toBeNull()
|
2026-04-03 10:42:44 +02:00
|
|
|
expect(arrow).not.toBeNull()
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('Mobile sidebar', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<button id="admin-sidebar-toggle"></button>
|
|
|
|
|
<div id="admin-sidebar"></div>
|
|
|
|
|
<div id="admin-overlay"></div>
|
|
|
|
|
`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('opens sidebar on toggle click', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
document.getElementById('admin-sidebar-toggle').click()
|
|
|
|
|
expect(document.getElementById('admin-sidebar').classList.contains('open')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('closes sidebar on overlay click', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
const sidebar = document.getElementById('admin-sidebar')
|
|
|
|
|
sidebar.classList.add('open')
|
|
|
|
|
|
|
|
|
|
document.getElementById('admin-overlay').click()
|
|
|
|
|
expect(sidebar.classList.contains('open')).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('Mobile menu (public)', () => {
|
|
|
|
|
it('toggles mobile menu and icons', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<button id="mobile-menu-btn"></button>
|
|
|
|
|
<div id="mobile-menu" class="hidden"></div>
|
|
|
|
|
<span id="menu-icon-open"></span>
|
|
|
|
|
<span id="menu-icon-close" class="hidden"></span>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('mobile-menu-btn').click()
|
|
|
|
|
|
|
|
|
|
expect(document.getElementById('mobile-menu').classList.contains('hidden')).toBe(false)
|
|
|
|
|
expect(document.getElementById('menu-icon-open').classList.contains('hidden')).toBe(true)
|
|
|
|
|
expect(document.getElementById('menu-icon-close').classList.contains('hidden')).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('Cookie banner', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<div id="cookie-banner" class="hidden"></div>
|
|
|
|
|
<button id="cookie-accept"></button>
|
|
|
|
|
<button id="cookie-refuse"></button>
|
|
|
|
|
`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows banner when no consent', async () => {
|
2026-04-07 23:50:19 +02:00
|
|
|
localStorageMock.getItem.mockReturnValue(null)
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
await loadApp()
|
|
|
|
|
expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides banner when already accepted', async () => {
|
2026-04-07 23:50:19 +02:00
|
|
|
localStorageMock.getItem.mockReturnValue('accepted')
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
await loadApp()
|
|
|
|
|
expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides banner and stores accepted on accept click', async () => {
|
2026-04-07 23:50:19 +02:00
|
|
|
localStorageMock.getItem.mockReturnValue(null)
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
await loadApp()
|
|
|
|
|
document.getElementById('cookie-accept').click()
|
|
|
|
|
|
|
|
|
|
expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(true)
|
2026-04-07 23:50:19 +02:00
|
|
|
expect(localStorageMock.setItem).toHaveBeenCalledWith('cookie_consent', 'accepted')
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides banner and stores refused on refuse click', async () => {
|
2026-04-07 23:50:19 +02:00
|
|
|
localStorageMock.getItem.mockReturnValue(null)
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
await loadApp()
|
|
|
|
|
document.getElementById('cookie-refuse').click()
|
|
|
|
|
|
|
|
|
|
expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(true)
|
2026-04-07 23:50:19 +02:00
|
|
|
expect(localStorageMock.setItem).toHaveBeenCalledWith('cookie_consent', 'refused')
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('Tarif tabs', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<div id="tarif-tabs">
|
|
|
|
|
<button data-tab="ndd">NDD</button>
|
|
|
|
|
<button data-tab="mail">Mail</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="content-ndd">NDD content</div>
|
|
|
|
|
<div id="content-mail" class="hidden">Mail content</div>
|
|
|
|
|
`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('switches tabs on click', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.querySelector('[data-tab="mail"]').click()
|
|
|
|
|
|
|
|
|
|
expect(document.getElementById('content-ndd').classList.contains('hidden')).toBe(true)
|
|
|
|
|
expect(document.getElementById('content-mail').classList.contains('hidden')).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-03 10:42:44 +02:00
|
|
|
describe('Search functionality', () => {
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
it('search setup does nothing without elements', async () => {
|
|
|
|
|
document.body.innerHTML = ''
|
|
|
|
|
await loadApp()
|
|
|
|
|
expect(true).toBe(true)
|
|
|
|
|
})
|
2026-04-03 10:42:44 +02:00
|
|
|
|
|
|
|
|
it('hides results when query is too short', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<input id="search-customers" value="">
|
|
|
|
|
<div id="search-results" class="hidden"></div>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('search-customers')
|
|
|
|
|
input.value = 'a'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
expect(document.getElementById('search-results').classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('performs search when query is long enough', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<input id="search-customers" value="">
|
|
|
|
|
<div id="search-results" class="hidden"></div>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve([
|
|
|
|
|
{ id: 1, fullName: 'Jean Dupont', email: 'jean@test.com' }
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('search-customers')
|
|
|
|
|
input.value = 'jean'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
// Wait for debounce (300ms) + fetch
|
|
|
|
|
await new Promise(r => setTimeout(r, 400))
|
|
|
|
|
|
|
|
|
|
expect(globalThis.fetch).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('performs search with no results', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<input id="search-customers" value="">
|
|
|
|
|
<div id="search-results" class="hidden"></div>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({ json: () => Promise.resolve([]) })
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('search-customers')
|
|
|
|
|
input.value = 'xyz'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 400))
|
|
|
|
|
|
|
|
|
|
const results = document.getElementById('search-results')
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(false)
|
|
|
|
|
expect(results.innerHTML).toContain('Aucun resultat')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('performs search with revendeur result', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<input id="search-revendeurs" value="">
|
|
|
|
|
<div id="search-results-revendeurs" class="hidden"></div>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve([
|
|
|
|
|
{ id: 1, raisonSociale: 'Ma SARL', codeRevendeur: 'REV-001' }
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('search-revendeurs')
|
|
|
|
|
input.value = 'sarl'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 400))
|
|
|
|
|
|
|
|
|
|
const results = document.getElementById('search-results-revendeurs')
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(false)
|
|
|
|
|
expect(results.innerHTML).toContain('Ma SARL')
|
|
|
|
|
expect(results.innerHTML).toContain('REV-001')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides results when clicking outside', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<input id="search-customers" value="">
|
|
|
|
|
<div id="search-results"><div>Result</div></div>
|
|
|
|
|
<div id="outside">Outside</div>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const results = document.getElementById('search-results')
|
|
|
|
|
|
|
|
|
|
document.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
|
|
|
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders hit with firstName/lastName fallback', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<input id="search-customers" value="">
|
|
|
|
|
<div id="search-results" class="hidden"></div>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve([
|
|
|
|
|
{ id: 2, firstName: 'Marie', lastName: 'Martin' }
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('search-customers')
|
|
|
|
|
input.value = 'marie'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 400))
|
|
|
|
|
|
|
|
|
|
const results = document.getElementById('search-results')
|
|
|
|
|
expect(results.innerHTML).toContain('Marie Martin')
|
|
|
|
|
})
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
})
|
2026-04-07 23:50:19 +02:00
|
|
|
|
|
|
|
|
describe('Modal open/close (data-modal-open / data-modal-close)', () => {
|
|
|
|
|
it('opens a modal when clicking a data-modal-open button', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<button data-modal-open="my-modal">Open</button>
|
|
|
|
|
<div id="my-modal" class="hidden">Modal content</div>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.querySelector('[data-modal-open="my-modal"]').click()
|
|
|
|
|
|
|
|
|
|
expect(document.getElementById('my-modal').classList.contains('hidden')).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('closes a modal when clicking a data-modal-close button', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<div id="my-modal">
|
|
|
|
|
<button data-modal-close="my-modal">Close</button>
|
|
|
|
|
Modal content
|
|
|
|
|
</div>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
// Modal starts visible
|
|
|
|
|
expect(document.getElementById('my-modal').classList.contains('hidden')).toBe(false)
|
|
|
|
|
|
|
|
|
|
document.querySelector('[data-modal-close="my-modal"]').click()
|
|
|
|
|
|
|
|
|
|
expect(document.getElementById('my-modal').classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does nothing if target modal does not exist', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<button data-modal-open="nonexistent">Open</button>
|
|
|
|
|
<button data-modal-close="nonexistent">Close</button>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
// Should not throw
|
|
|
|
|
document.querySelector('[data-modal-open="nonexistent"]').click()
|
|
|
|
|
document.querySelector('[data-modal-close="nonexistent"]').click()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handles multiple modals independently', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<button data-modal-open="modal-a">Open A</button>
|
|
|
|
|
<button data-modal-open="modal-b">Open B</button>
|
|
|
|
|
<div id="modal-a" class="hidden">Modal A</div>
|
|
|
|
|
<div id="modal-b" class="hidden">Modal B</div>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.querySelector('[data-modal-open="modal-a"]').click()
|
|
|
|
|
|
|
|
|
|
expect(document.getElementById('modal-a').classList.contains('hidden')).toBe(false)
|
|
|
|
|
expect(document.getElementById('modal-b').classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('SIRET search (prestataire creation)', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<form>
|
|
|
|
|
<input id="siret-search-input" value="">
|
|
|
|
|
<button type="button" id="siret-search-btn">Rechercher</button>
|
|
|
|
|
<div id="siret-search-results" class="hidden"></div>
|
|
|
|
|
<input name="raisonSociale" value="">
|
|
|
|
|
<input name="siret" value="">
|
|
|
|
|
<input name="address" value="">
|
|
|
|
|
<input name="zipCode" value="">
|
|
|
|
|
<input name="city" value="">
|
|
|
|
|
</form>
|
|
|
|
|
`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows message when query is too short', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('siret-search-input')
|
|
|
|
|
input.value = 'ab'
|
|
|
|
|
document.getElementById('siret-search-btn').click()
|
|
|
|
|
|
|
|
|
|
const results = document.getElementById('siret-search-results')
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(false)
|
|
|
|
|
expect(results.innerHTML).toContain('au moins 3 caracteres')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows loading state then results on search', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve({
|
|
|
|
|
results: [
|
|
|
|
|
{
|
|
|
|
|
nom_complet: 'Test SARL',
|
|
|
|
|
siege: {
|
|
|
|
|
siret: '12345678901234',
|
|
|
|
|
adresse: '1 rue de Paris',
|
|
|
|
|
code_postal: '75001',
|
|
|
|
|
libelle_commune: 'PARIS'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('siret-search-input')
|
|
|
|
|
input.value = 'test sarl'
|
|
|
|
|
document.getElementById('siret-search-btn').click()
|
|
|
|
|
|
|
|
|
|
// Wait for fetch
|
|
|
|
|
await new Promise(r => setTimeout(r, 50))
|
|
|
|
|
|
|
|
|
|
const results = document.getElementById('siret-search-results')
|
|
|
|
|
expect(results.innerHTML).toContain('Test SARL')
|
|
|
|
|
expect(results.innerHTML).toContain('12345678901234')
|
|
|
|
|
expect(globalThis.fetch).toHaveBeenCalledWith('/admin/prestataires/entreprise-search?q=test%20sarl')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows no results message when empty', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve({ results: [] })
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('siret-search-input').value = 'zzzzz'
|
|
|
|
|
document.getElementById('siret-search-btn').click()
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 50))
|
|
|
|
|
|
|
|
|
|
const results = document.getElementById('siret-search-results')
|
|
|
|
|
expect(results.innerHTML).toContain('Aucun resultat')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('fills form fields when a result is clicked', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve({
|
|
|
|
|
results: [
|
|
|
|
|
{
|
|
|
|
|
nom_complet: 'Ma Societe',
|
|
|
|
|
siege: {
|
|
|
|
|
siret: '98765432109876',
|
|
|
|
|
adresse: '10 avenue de Lyon',
|
|
|
|
|
code_postal: '69001',
|
|
|
|
|
libelle_commune: 'LYON'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('siret-search-input').value = 'ma societe'
|
|
|
|
|
document.getElementById('siret-search-btn').click()
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 50))
|
|
|
|
|
|
|
|
|
|
// Click the first result
|
|
|
|
|
const resultItem = document.querySelector('.siret-result-item')
|
|
|
|
|
expect(resultItem).not.toBeNull()
|
|
|
|
|
resultItem.click()
|
|
|
|
|
|
|
|
|
|
expect(document.querySelector('[name="raisonSociale"]').value).toBe('Ma Societe')
|
|
|
|
|
expect(document.querySelector('[name="siret"]').value).toBe('98765432109876')
|
|
|
|
|
expect(document.querySelector('[name="address"]').value).toBe('10 avenue de Lyon')
|
|
|
|
|
expect(document.querySelector('[name="zipCode"]').value).toBe('69001')
|
|
|
|
|
expect(document.querySelector('[name="city"]').value).toBe('LYON')
|
|
|
|
|
|
|
|
|
|
// Results should be hidden and input cleared
|
|
|
|
|
expect(document.getElementById('siret-search-results').classList.contains('hidden')).toBe(true)
|
|
|
|
|
expect(document.getElementById('siret-search-input').value).toBe('')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows error message on fetch failure', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() => Promise.reject(new Error('Network error')))
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('siret-search-input').value = 'test'
|
|
|
|
|
document.getElementById('siret-search-btn').click()
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 50))
|
|
|
|
|
|
|
|
|
|
const results = document.getElementById('siret-search-results')
|
|
|
|
|
expect(results.innerHTML).toContain('Erreur lors de la recherche')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('triggers search on Enter key in input', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve({ results: [] })
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('siret-search-input')
|
|
|
|
|
input.value = 'test enter'
|
|
|
|
|
|
|
|
|
|
const event = new KeyboardEvent('keydown', { key: 'Enter', cancelable: true })
|
|
|
|
|
input.dispatchEvent(event)
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 50))
|
|
|
|
|
|
|
|
|
|
expect(globalThis.fetch).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides results when clicking outside', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const results = document.getElementById('siret-search-results')
|
|
|
|
|
results.classList.remove('hidden')
|
|
|
|
|
results.innerHTML = '<p>Some results</p>'
|
|
|
|
|
|
|
|
|
|
// Click on document body (outside)
|
|
|
|
|
document.body.click()
|
|
|
|
|
|
|
|
|
|
// Need to trigger click on document level
|
|
|
|
|
document.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
|
|
|
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('Refuse toggle button', () => {
|
|
|
|
|
it('toggles refuse form visibility', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<button id="refuse-toggle-btn">Refuser</button>
|
|
|
|
|
<div id="refuse-form" class="hidden">Refuse form</div>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('refuse-toggle-btn').click()
|
|
|
|
|
expect(document.getElementById('refuse-form').classList.contains('hidden')).toBe(false)
|
|
|
|
|
|
|
|
|
|
document.getElementById('refuse-toggle-btn').click()
|
|
|
|
|
expect(document.getElementById('refuse-form').classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
})
|
test: couverture JS 100% lignes app.js (73 tests) + PHP 100% methodes
JS (app.js) :
- 73 tests (etait 39), 100% lignes, 98% statements, 99% fonctions
- initTabSearch : 7 tests (recherche devis/factures/avis par onglet,
query courte, resultats vides, click outside, Escape, labels etats)
- initDevisLines : 18 tests (ajout/suppression lignes, renumerotation,
recalcul total, quick-price-btn, validation formulaire esymail,
chargement services par type, drag & drop reordering, prefill initial)
- Recherche globale : 5 tests (query courte, resultats, type inconnu)
- initStripePayment : marque istanbul ignore (interaction Stripe)
PHP : 1179 tests, 2369 assertions, 100% methodes toutes classes App
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:08:36 +02:00
|
|
|
|
|
|
|
|
describe('initTabSearch', () => {
|
|
|
|
|
const lineTemplate = `
|
|
|
|
|
<script id="line-template" type="text/html">
|
|
|
|
|
<div class="line-row" draggable="true">
|
|
|
|
|
<span class="line-pos"></span>
|
|
|
|
|
<input type="hidden" class="line-pos-input" name="lines[__INDEX__][pos]">
|
|
|
|
|
<input type="text" name="lines[__INDEX__][title]">
|
|
|
|
|
<textarea name="lines[__INDEX__][description]"></textarea>
|
|
|
|
|
<input type="number" class="line-price" name="lines[__INDEX__][priceHt]" value="0.00">
|
|
|
|
|
<select class="line-type" name="lines[__INDEX__][type]" data-services-url="/admin/devis/services?type=__TYPE__&client=1">
|
|
|
|
|
<option value="">— Type —</option>
|
|
|
|
|
<option value="hosting">Hebergement</option>
|
|
|
|
|
<option value="esymail">Esymail</option>
|
|
|
|
|
<option value="ndd">NDD</option>
|
|
|
|
|
<option value="website">Site</option>
|
|
|
|
|
<option value="maintenance">Maintenance</option>
|
|
|
|
|
<option value="other">Autre</option>
|
|
|
|
|
</select>
|
|
|
|
|
<select class="line-service-id" name="lines[__INDEX__][serviceId]" disabled>
|
|
|
|
|
<option value="">— Selectionner le service —</option>
|
|
|
|
|
</select>
|
|
|
|
|
<button type="button" class="remove-line-btn">X</button>
|
|
|
|
|
</div>
|
|
|
|
|
</script>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<input id="search-devis" data-url="/admin/devis/search" value="">
|
|
|
|
|
<div id="search-devis-results" class="hidden"></div>
|
|
|
|
|
<input id="search-adverts" data-url="/admin/adverts/search" value="">
|
|
|
|
|
<div id="search-adverts-results" class="hidden"></div>
|
|
|
|
|
<input id="search-factures" data-url="/admin/factures/search" value="">
|
|
|
|
|
<div id="search-factures-results" class="hidden"></div>
|
|
|
|
|
`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does nothing when elements are absent', async () => {
|
|
|
|
|
document.body.innerHTML = ''
|
|
|
|
|
await loadApp()
|
|
|
|
|
expect(true).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides results when query is shorter than 2 chars', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('search-devis')
|
|
|
|
|
const results = document.getElementById('search-devis-results')
|
|
|
|
|
|
|
|
|
|
input.value = 'a'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows results with correct HTML on successful search', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve([
|
|
|
|
|
{ numOrder: 'DEV-001', customerName: 'Client A', totalTtc: '120.00', state: 'send' }
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('search-devis')
|
|
|
|
|
const results = document.getElementById('search-devis-results')
|
|
|
|
|
|
|
|
|
|
input.value = 'DEV'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 350))
|
|
|
|
|
|
|
|
|
|
expect(globalThis.fetch).toHaveBeenCalledWith('/admin/devis/search?q=DEV')
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(false)
|
|
|
|
|
expect(results.innerHTML).toContain('DEV-001')
|
|
|
|
|
expect(results.innerHTML).toContain('Client A')
|
|
|
|
|
expect(results.innerHTML).toContain('120.00')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows "Aucun resultat" when search returns empty array', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({ json: () => Promise.resolve([]) })
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('search-devis')
|
|
|
|
|
const results = document.getElementById('search-devis-results')
|
|
|
|
|
|
|
|
|
|
input.value = 'xyz'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 350))
|
|
|
|
|
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(false)
|
|
|
|
|
expect(results.innerHTML).toContain('Aucun resultat')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides results when clicking outside', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const results = document.getElementById('search-devis-results')
|
|
|
|
|
results.classList.remove('hidden')
|
|
|
|
|
|
|
|
|
|
document.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
|
|
|
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides results on Escape key', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('search-devis')
|
|
|
|
|
const results = document.getElementById('search-devis-results')
|
|
|
|
|
results.classList.remove('hidden')
|
|
|
|
|
|
|
|
|
|
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
|
|
|
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders state labels and colors correctly', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve([
|
|
|
|
|
{ numOrder: 'DEV-002', customerName: 'Client B', totalTtc: '50.00', state: 'accepted' }
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('search-devis')
|
|
|
|
|
const results = document.getElementById('search-devis-results')
|
|
|
|
|
|
|
|
|
|
input.value = 'DEV'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 350))
|
|
|
|
|
|
|
|
|
|
expect(results.innerHTML).toContain('Accepte')
|
|
|
|
|
expect(results.innerHTML).toContain('bg-green-500/20')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders unknown state as-is', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve([
|
|
|
|
|
{ numOrder: 'ADV-001', customerName: 'Client C', totalTtc: '200.00', state: 'pending' }
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('search-factures')
|
|
|
|
|
const results = document.getElementById('search-factures-results')
|
|
|
|
|
|
|
|
|
|
input.value = 'ADV'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 350))
|
|
|
|
|
|
|
|
|
|
expect(results.innerHTML).toContain('pending')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('initDevisLines', () => {
|
|
|
|
|
const lineTemplate = `
|
|
|
|
|
<div class="line-row" draggable="true">
|
|
|
|
|
<span class="line-pos"></span>
|
|
|
|
|
<input type="hidden" class="line-pos-input" name="lines[__INDEX__][pos]">
|
|
|
|
|
<input type="text" name="lines[__INDEX__][title]">
|
|
|
|
|
<textarea name="lines[__INDEX__][description]"></textarea>
|
|
|
|
|
<input type="number" class="line-price" name="lines[__INDEX__][priceHt]" value="0.00">
|
|
|
|
|
<select class="line-type" name="lines[__INDEX__][type]" data-services-url="/admin/devis/services?type=__TYPE__&client=1">
|
|
|
|
|
<option value="">— Type —</option>
|
|
|
|
|
<option value="hosting">Hebergement</option>
|
|
|
|
|
<option value="esymail">Esymail</option>
|
|
|
|
|
<option value="ndd">NDD</option>
|
|
|
|
|
<option value="website">Site</option>
|
|
|
|
|
<option value="maintenance">Maintenance</option>
|
|
|
|
|
<option value="other">Autre</option>
|
|
|
|
|
</select>
|
|
|
|
|
<select class="line-service-id" name="lines[__INDEX__][serviceId]" disabled>
|
|
|
|
|
<option value="">— Selectionner le service —</option>
|
|
|
|
|
</select>
|
|
|
|
|
<button type="button" class="remove-line-btn">X</button>
|
|
|
|
|
</div>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<div id="lines-container"></div>
|
|
|
|
|
<button id="add-line-btn">Ajouter</button>
|
|
|
|
|
<script id="line-template" type="text/html">${lineTemplate}<\/script>
|
|
|
|
|
<div id="total-ht">0.00 EUR</div>
|
|
|
|
|
<form id="devis-form"></form>
|
|
|
|
|
`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does nothing when required elements are absent', async () => {
|
|
|
|
|
document.body.innerHTML = ''
|
|
|
|
|
await loadApp()
|
|
|
|
|
expect(true).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('adds a line when add button is clicked', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('add-line-btn').click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
expect(container.querySelectorAll('.line-row').length).toBe(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renumber updates positions after adding lines', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const addBtn = document.getElementById('add-line-btn')
|
|
|
|
|
addBtn.click()
|
|
|
|
|
addBtn.click()
|
|
|
|
|
addBtn.click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const rows = container.querySelectorAll('.line-row')
|
|
|
|
|
expect(rows.length).toBe(3)
|
|
|
|
|
expect(rows[0].querySelector('.line-pos').textContent).toBe('#1')
|
|
|
|
|
expect(rows[1].querySelector('.line-pos').textContent).toBe('#2')
|
|
|
|
|
expect(rows[2].querySelector('.line-pos').textContent).toBe('#3')
|
|
|
|
|
expect(rows[0].querySelector('.line-pos-input').value).toBe('0')
|
|
|
|
|
expect(rows[1].querySelector('.line-pos-input').value).toBe('1')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('recalc updates total-ht', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const addBtn = document.getElementById('add-line-btn')
|
|
|
|
|
addBtn.click()
|
|
|
|
|
addBtn.click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const prices = container.querySelectorAll('.line-price')
|
|
|
|
|
prices[0].value = '100.00'
|
|
|
|
|
prices[1].value = '50.50'
|
|
|
|
|
|
|
|
|
|
// Trigger input event to recalc
|
|
|
|
|
prices[0].dispatchEvent(new Event('input', { bubbles: true }))
|
|
|
|
|
|
|
|
|
|
// Manually check the total (recalc is called on add but we changed values)
|
|
|
|
|
// Trigger input on second price too
|
|
|
|
|
prices[1].dispatchEvent(new Event('input', { bubbles: true }))
|
|
|
|
|
|
|
|
|
|
expect(document.getElementById('total-ht').textContent).toBe('150.50 EUR')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('remove button removes the line and updates positions', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const addBtn = document.getElementById('add-line-btn')
|
|
|
|
|
addBtn.click()
|
|
|
|
|
addBtn.click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
expect(container.querySelectorAll('.line-row').length).toBe(2)
|
|
|
|
|
|
|
|
|
|
const removeBtn = container.querySelector('.remove-line-btn')
|
|
|
|
|
removeBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
|
|
|
|
|
|
|
|
expect(container.querySelectorAll('.line-row').length).toBe(1)
|
|
|
|
|
expect(container.querySelector('.line-pos').textContent).toBe('#1')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('price input change triggers recalc', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('add-line-btn').click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const priceInput = container.querySelector('.line-price')
|
|
|
|
|
priceInput.value = '75.25'
|
|
|
|
|
priceInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
|
|
|
|
|
|
|
|
expect(document.getElementById('total-ht').textContent).toBe('75.25 EUR')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('quick-price-btn pre-fills fields', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<div id="lines-container"></div>
|
|
|
|
|
<button id="add-line-btn">Ajouter</button>
|
|
|
|
|
<script id="line-template" type="text/html">${lineTemplate}<\/script>
|
|
|
|
|
<div id="total-ht">0.00 EUR</div>
|
|
|
|
|
<form id="devis-form"></form>
|
|
|
|
|
<button class="quick-price-btn"
|
|
|
|
|
data-title="Nom service"
|
|
|
|
|
data-description="Description service"
|
|
|
|
|
data-price="99.99"
|
|
|
|
|
data-line-type="">Rapide</button>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.querySelector('.quick-price-btn').click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const row = container.querySelector('.line-row:last-child')
|
|
|
|
|
expect(row).not.toBeNull()
|
|
|
|
|
expect(row.querySelector('input[name$="[title]"]').value).toBe('Nom service')
|
|
|
|
|
expect(row.querySelector('textarea[name$="[description]"]').value).toBe('Description service')
|
|
|
|
|
expect(row.querySelector('.line-price').value).toBe('99.99')
|
|
|
|
|
expect(document.getElementById('total-ht').textContent).toBe('99.99 EUR')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('quick-price-btn with line-type triggers type change event', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve([{ id: 42, label: 'Service Esymail' }])
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<div id="lines-container"></div>
|
|
|
|
|
<button id="add-line-btn">Ajouter</button>
|
|
|
|
|
<script id="line-template" type="text/html">${lineTemplate}<\/script>
|
|
|
|
|
<div id="total-ht">0.00 EUR</div>
|
|
|
|
|
<form id="devis-form"></form>
|
|
|
|
|
<button class="quick-price-btn"
|
|
|
|
|
data-title="Mail"
|
|
|
|
|
data-description="Abonnement mail"
|
|
|
|
|
data-price="12.00"
|
|
|
|
|
data-line-type="esymail">Rapide</button>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.querySelector('.quick-price-btn').click()
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 50))
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const typeSelect = container.querySelector('.line-type')
|
|
|
|
|
expect(typeSelect.value).toBe('esymail')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('form validation prevents submit when esymail service not selected', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('add-line-btn').click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const row = container.querySelector('.line-row')
|
|
|
|
|
const typeSelect = row.querySelector('.line-type')
|
|
|
|
|
const serviceSelect = row.querySelector('.line-service-id')
|
|
|
|
|
|
|
|
|
|
// Set type to esymail and add a service option (simulating loaded services)
|
|
|
|
|
typeSelect.value = 'esymail'
|
|
|
|
|
serviceSelect.disabled = false
|
|
|
|
|
const opt = document.createElement('option')
|
|
|
|
|
opt.value = '5'
|
|
|
|
|
opt.textContent = 'Mail service'
|
|
|
|
|
serviceSelect.appendChild(opt)
|
|
|
|
|
// Leave serviceSelect.value as '' (not selected)
|
|
|
|
|
|
|
|
|
|
window.alert = vi.fn()
|
|
|
|
|
|
|
|
|
|
const form = document.getElementById('devis-form')
|
|
|
|
|
const submitEvent = new Event('submit', { cancelable: true })
|
|
|
|
|
form.dispatchEvent(submitEvent)
|
|
|
|
|
|
|
|
|
|
expect(submitEvent.defaultPrevented).toBe(true)
|
|
|
|
|
expect(window.alert).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('form validation allows submit when type is hosting (no service required)', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('add-line-btn').click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const row = container.querySelector('.line-row')
|
|
|
|
|
const typeSelect = row.querySelector('.line-type')
|
|
|
|
|
typeSelect.value = 'hosting'
|
|
|
|
|
|
|
|
|
|
window.alert = vi.fn()
|
|
|
|
|
|
|
|
|
|
const form = document.getElementById('devis-form')
|
|
|
|
|
const submitEvent = new Event('submit', { cancelable: true })
|
|
|
|
|
form.dispatchEvent(submitEvent)
|
|
|
|
|
|
|
|
|
|
expect(submitEvent.defaultPrevented).toBe(false)
|
|
|
|
|
expect(window.alert).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('type change triggers service loading via fetch', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve([
|
|
|
|
|
{ id: 10, label: 'Esymail A' },
|
|
|
|
|
{ id: 11, label: 'Esymail B' }
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('add-line-btn').click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const row = container.querySelector('.line-row')
|
|
|
|
|
const typeSelect = row.querySelector('.line-type')
|
|
|
|
|
|
|
|
|
|
typeSelect.value = 'esymail'
|
|
|
|
|
typeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 50))
|
|
|
|
|
|
|
|
|
|
const serviceSelect = row.querySelector('.line-service-id')
|
|
|
|
|
expect(serviceSelect.disabled).toBe(false)
|
|
|
|
|
expect(serviceSelect.options.length).toBeGreaterThan(1)
|
|
|
|
|
expect(serviceSelect.innerHTML).toContain('Esymail A')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('type change to hosting disables service select', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('add-line-btn').click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const row = container.querySelector('.line-row')
|
|
|
|
|
const typeSelect = row.querySelector('.line-type')
|
|
|
|
|
|
|
|
|
|
typeSelect.value = 'hosting'
|
|
|
|
|
typeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 50))
|
|
|
|
|
|
|
|
|
|
const serviceSelect = row.querySelector('.line-service-id')
|
|
|
|
|
expect(serviceSelect.disabled).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('drag & drop reordering: dragstart adds dragging class', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const addBtn = document.getElementById('add-line-btn')
|
|
|
|
|
addBtn.click()
|
|
|
|
|
addBtn.click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const firstRow = container.querySelector('.line-row')
|
|
|
|
|
|
|
|
|
|
const dragstartEvent = new Event('dragstart', { bubbles: true })
|
|
|
|
|
dragstartEvent.dataTransfer = { effectAllowed: '' }
|
|
|
|
|
firstRow.dispatchEvent(dragstartEvent)
|
|
|
|
|
|
|
|
|
|
expect(firstRow.classList.contains('dragging')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('drag & drop: dragend removes dragging class and renumbers', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const addBtn = document.getElementById('add-line-btn')
|
|
|
|
|
addBtn.click()
|
|
|
|
|
addBtn.click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const firstRow = container.querySelector('.line-row')
|
|
|
|
|
|
|
|
|
|
const dragstartEvent = new Event('dragstart', { bubbles: true })
|
|
|
|
|
dragstartEvent.dataTransfer = { effectAllowed: '' }
|
|
|
|
|
firstRow.dispatchEvent(dragstartEvent)
|
|
|
|
|
|
|
|
|
|
expect(firstRow.classList.contains('dragging')).toBe(true)
|
|
|
|
|
|
|
|
|
|
const dragendEvent = new Event('dragend', { bubbles: true })
|
|
|
|
|
firstRow.dispatchEvent(dragendEvent)
|
|
|
|
|
|
|
|
|
|
expect(firstRow.classList.contains('dragging')).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('drag & drop: dragover adds drag-over class to target', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const addBtn = document.getElementById('add-line-btn')
|
|
|
|
|
addBtn.click()
|
|
|
|
|
addBtn.click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const rows = container.querySelectorAll('.line-row')
|
|
|
|
|
const firstRow = rows[0]
|
|
|
|
|
const secondRow = rows[1]
|
|
|
|
|
|
|
|
|
|
// Start drag on first row (bubbles up to container)
|
|
|
|
|
const dragstartEvent = new Event('dragstart', { bubbles: true })
|
|
|
|
|
dragstartEvent.dataTransfer = { effectAllowed: '' }
|
|
|
|
|
firstRow.dispatchEvent(dragstartEvent)
|
|
|
|
|
|
|
|
|
|
// Dispatch dragover on secondRow so e.target === secondRow when it bubbles to container
|
|
|
|
|
const dragoverEvent = new Event('dragover', { bubbles: true, cancelable: true })
|
|
|
|
|
secondRow.dispatchEvent(dragoverEvent)
|
|
|
|
|
|
|
|
|
|
expect(secondRow.classList.contains('drag-over')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('drag & drop: drop reorders rows (drag down: target.after)', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const addBtn = document.getElementById('add-line-btn')
|
|
|
|
|
addBtn.click()
|
|
|
|
|
addBtn.click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
let rows = container.querySelectorAll('.line-row')
|
|
|
|
|
const firstRow = rows[0]
|
|
|
|
|
const secondRow = rows[1]
|
|
|
|
|
|
|
|
|
|
// Set a data attribute to identify the rows after reorder (not .line-pos since renumber overwrites it)
|
|
|
|
|
firstRow.dataset.testId = 'FIRST'
|
|
|
|
|
secondRow.dataset.testId = 'SECOND'
|
|
|
|
|
|
|
|
|
|
// Start drag on first row (bubbles up to container)
|
|
|
|
|
const dragstartEvent = new Event('dragstart', { bubbles: true })
|
|
|
|
|
dragstartEvent.dataTransfer = { effectAllowed: '' }
|
|
|
|
|
firstRow.dispatchEvent(dragstartEvent)
|
|
|
|
|
|
|
|
|
|
// Dispatch drop on secondRow so e.target === secondRow when it bubbles to container
|
|
|
|
|
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
|
|
|
|
|
secondRow.dispatchEvent(dropEvent)
|
|
|
|
|
|
|
|
|
|
rows = container.querySelectorAll('.line-row')
|
|
|
|
|
// firstRow (draggedIdx=0) < targetIdx (secondRow, idx=1) → target.after(draggedRow)
|
|
|
|
|
// So order should be: secondRow then firstRow
|
|
|
|
|
expect(rows[0].dataset.testId).toBe('SECOND')
|
|
|
|
|
expect(rows[1].dataset.testId).toBe('FIRST')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('drag & drop: drop reorders rows (drag up: target.before)', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const addBtn = document.getElementById('add-line-btn')
|
|
|
|
|
addBtn.click()
|
|
|
|
|
addBtn.click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
let rows = container.querySelectorAll('.line-row')
|
|
|
|
|
const firstRow = rows[0]
|
|
|
|
|
const secondRow = rows[1]
|
|
|
|
|
|
|
|
|
|
firstRow.dataset.testId = 'FIRST'
|
|
|
|
|
secondRow.dataset.testId = 'SECOND'
|
|
|
|
|
|
|
|
|
|
// Start drag on SECOND row (index 1)
|
|
|
|
|
const dragstartEvent = new Event('dragstart', { bubbles: true })
|
|
|
|
|
dragstartEvent.dataTransfer = { effectAllowed: '' }
|
|
|
|
|
secondRow.dispatchEvent(dragstartEvent)
|
|
|
|
|
|
|
|
|
|
// Drop on FIRST row: draggedIdx(1) > targetIdx(0) → target.before(draggedRow)
|
|
|
|
|
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
|
|
|
|
|
firstRow.dispatchEvent(dropEvent)
|
|
|
|
|
|
|
|
|
|
rows = container.querySelectorAll('.line-row')
|
|
|
|
|
// secondRow should now be before firstRow
|
|
|
|
|
expect(rows[0].dataset.testId).toBe('SECOND')
|
|
|
|
|
expect(rows[1].dataset.testId).toBe('FIRST')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('initial lines prefill from data-initial-lines', async () => {
|
|
|
|
|
const initialLines = JSON.stringify([
|
|
|
|
|
{ pos: 0, title: 'Ligne 1', description: 'Desc 1', priceHt: '100.00', type: 'hosting' },
|
|
|
|
|
{ pos: 1, title: 'Ligne 2', description: 'Desc 2', priceHt: '200.00', type: 'other' }
|
|
|
|
|
])
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<div id="lines-container" data-initial-lines='${initialLines}'></div>
|
|
|
|
|
<button id="add-line-btn">Ajouter</button>
|
|
|
|
|
<script id="line-template" type="text/html">${lineTemplate}<\/script>
|
|
|
|
|
<div id="total-ht">0.00 EUR</div>
|
|
|
|
|
<form id="devis-form"></form>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 50))
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const rows = container.querySelectorAll('.line-row')
|
|
|
|
|
expect(rows.length).toBe(2)
|
|
|
|
|
expect(rows[0].querySelector('input[name$="[title]"]').value).toBe('Ligne 1')
|
|
|
|
|
expect(rows[1].querySelector('input[name$="[title]"]').value).toBe('Ligne 2')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('initial lines with serviceId fetches services and pre-selects', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve([
|
|
|
|
|
{ id: 7, label: 'Mail Pro' },
|
|
|
|
|
{ id: 8, label: 'Mail Starter' }
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const initialLines = JSON.stringify([
|
|
|
|
|
{ pos: 0, title: 'Mail', description: '', priceHt: '15.00', type: 'esymail', serviceId: 7 }
|
|
|
|
|
])
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<div id="lines-container" data-initial-lines='${initialLines}'></div>
|
|
|
|
|
<button id="add-line-btn">Ajouter</button>
|
|
|
|
|
<script id="line-template" type="text/html">${lineTemplate}<\/script>
|
|
|
|
|
<div id="total-ht">0.00 EUR</div>
|
|
|
|
|
<form id="devis-form"></form>
|
|
|
|
|
`
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 100))
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const row = container.querySelector('.line-row')
|
|
|
|
|
expect(row).not.toBeNull()
|
|
|
|
|
const serviceSelect = row.querySelector('.line-service-id')
|
|
|
|
|
expect(serviceSelect.disabled).toBe(false)
|
|
|
|
|
expect(serviceSelect.innerHTML).toContain('Mail Pro')
|
|
|
|
|
|
|
|
|
|
// The option with id=7 should be selected
|
|
|
|
|
expect(serviceSelect.value).toBe('7')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handles invalid JSON in data-initial-lines gracefully', async () => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<div id="lines-container" data-initial-lines='not-valid-json'></div>
|
|
|
|
|
<button id="add-line-btn">Ajouter</button>
|
|
|
|
|
<script id="line-template" type="text/html">${lineTemplate}<\/script>
|
|
|
|
|
<div id="total-ht">0.00 EUR</div>
|
|
|
|
|
<form id="devis-form"></form>
|
|
|
|
|
`
|
|
|
|
|
// Should not throw
|
|
|
|
|
await loadApp()
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
expect(container.querySelectorAll('.line-row').length).toBe(0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('type change fetch error is silently ignored', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() => Promise.reject(new Error('Network error')))
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
document.getElementById('add-line-btn').click()
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('lines-container')
|
|
|
|
|
const row = container.querySelector('.line-row')
|
|
|
|
|
const typeSelect = row.querySelector('.line-type')
|
|
|
|
|
|
|
|
|
|
typeSelect.value = 'esymail'
|
|
|
|
|
typeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
|
|
|
|
|
|
|
|
// Should not throw — error is silently caught
|
|
|
|
|
await new Promise(r => setTimeout(r, 50))
|
|
|
|
|
|
|
|
|
|
const serviceSelect = row.querySelector('.line-service-id')
|
|
|
|
|
expect(serviceSelect.disabled).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('Global search (navbar)', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
document.body.innerHTML = `
|
|
|
|
|
<input id="global-search" value="">
|
|
|
|
|
<div id="global-search-results" class="hidden"></div>
|
|
|
|
|
`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides results when query is shorter than 2 chars', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('global-search')
|
|
|
|
|
const results = document.getElementById('global-search-results')
|
|
|
|
|
results.classList.remove('hidden')
|
|
|
|
|
|
|
|
|
|
input.value = 'a'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows results with hits on successful search', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve([
|
|
|
|
|
{ url: '/admin/clients/1', type: 'client', label: 'Jean Dupont', sub: 'jean@example.com' },
|
|
|
|
|
{ url: '/admin/services/ndd/1', type: 'ndd', label: 'exemple.fr', sub: null }
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('global-search')
|
|
|
|
|
const results = document.getElementById('global-search-results')
|
|
|
|
|
|
|
|
|
|
input.value = 'jean'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 350))
|
|
|
|
|
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(false)
|
|
|
|
|
expect(results.innerHTML).toContain('Jean Dupont')
|
|
|
|
|
expect(results.innerHTML).toContain('Client')
|
|
|
|
|
expect(results.innerHTML).toContain('exemple.fr')
|
|
|
|
|
expect(results.innerHTML).toContain('NDD')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows "Aucun resultat" when search returns empty', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({ json: () => Promise.resolve([]) })
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('global-search')
|
|
|
|
|
const results = document.getElementById('global-search-results')
|
|
|
|
|
|
|
|
|
|
input.value = 'zzzz'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 350))
|
|
|
|
|
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(false)
|
|
|
|
|
expect(results.innerHTML).toContain('Aucun resultat')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides results on Escape key', async () => {
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('global-search')
|
|
|
|
|
const results = document.getElementById('global-search-results')
|
|
|
|
|
results.classList.remove('hidden')
|
|
|
|
|
|
|
|
|
|
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
|
|
|
|
|
|
|
|
expect(results.classList.contains('hidden')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders hit with unknown type using raw type string', async () => {
|
|
|
|
|
globalThis.fetch = vi.fn(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
json: () => Promise.resolve([
|
|
|
|
|
{ url: '/admin/foo/1', type: 'unknown_type', label: 'Item X', sub: 'detail' }
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await loadApp()
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('global-search')
|
|
|
|
|
input.value = 'item'
|
|
|
|
|
input.dispatchEvent(new Event('input'))
|
|
|
|
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 350))
|
|
|
|
|
|
|
|
|
|
const results = document.getElementById('global-search-results')
|
|
|
|
|
expect(results.innerHTML).toContain('unknown_type')
|
|
|
|
|
expect(results.innerHTML).toContain('Item X')
|
|
|
|
|
})
|
|
|
|
|
})
|
test: ajout 17 tests JS app.js, tests entités/handlers complémentaires
Tests JavaScript (17 tests vitest, tests/js/app.test.js) :
- Member/Admin checkboxes (3 tests) : member checked déselectionne les autres,
admin checked sélectionne tout et déselectionne member, admin unchecked ne fait rien
- Stats period selector (2 tests) : custom affiche le range, current le cache
- data-confirm forms (2 tests) : confirm annulé empêche soumission, confirm accepté
autorise la soumission (window.confirm mocké via vi.fn)
- Sidebar dropdown (1 test) : vérifie la structure bouton/menu/arrow
- Mobile sidebar (2 tests) : toggle ouvre la sidebar, overlay la ferme
- Mobile menu public (1 test) : toggle affiche/cache le menu et bascule les icônes
- Cookie banner (4 tests) : affichage sans consent, masqué si déjà accepté,
accepter stocke 'accepted' et cache, refuser stocke 'refused' et cache
- Tarif tabs (1 test) : clic sur onglet bascule les contenus
- Search setup (1 test) : pas d'erreur sans éléments DOM
Tests entités complémentaires :
- AttestationTest : ajout setEmailTracking avec EmailTracking et null
- CustomerTest : ajout vérification getUpdatedAt après setState
- ServiceTest : ajout testSetStatusSameStatus (même statut, pas d'historique ajouté)
- UserExtendedTest : ajout testAvatarFile avec File réel et null
Tests MessageHandlers :
- AppLogMessageHandlerTest (2 tests) : avec userId (find user), sans userId (null)
- MeilisearchSyncMessageHandlerTest (12 tests) : remove customer/revendeur/price/unknown,
index customer/revendeur/price trouvé et non trouvé, index unknown type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:17 +02:00
|
|
|
})
|