Files
crm_ecosplay/tests/js/app.test.js

1517 lines
60 KiB
JavaScript
Raw Normal View History

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
// 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(() => {
// 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', () => {
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')
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 }))
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()
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)
})
})
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)
})
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)
})
})
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
})