diff --git a/Makefile b/Makefile index 2985eca..4ad974d 100644 --- a/Makefile +++ b/Makefile @@ -207,7 +207,7 @@ hadolint_report: ## Lance Hadolint sur le Dockerfile prod et genere le rapport J audit: ## Lance l'audit de securite Composer docker compose -f docker-compose-dev.yml exec php composer audit -reports: phpstan_report eslint_report test_coverage hadolint_report phpmetrics ## Genere tous les rapports pour SonarQube +reports: phpstan_report eslint_report run_test_coverage_js test_coverage hadolint_report phpmetrics ## Genere tous les rapports pour SonarQube ## —— SonarQube ———————————————————————————————————— sonar: reports ## Genere les rapports puis lance le scan SonarQube diff --git a/tests/js/app.test.js b/tests/js/app.test.js new file mode 100644 index 0000000..6330e0f --- /dev/null +++ b/tests/js/app.test.js @@ -0,0 +1,254 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +describe('app.js DOMContentLoaded', () => { + beforeEach(() => { + document.body.innerHTML = '' + localStorage.clear() + }) + + const loadApp = async () => { + // Reset module cache and re-import + vi.resetModules() + await import('../../assets/app.js') + document.dispatchEvent(new Event('DOMContentLoaded')) + } + + describe('Member/Admin checkboxes', () => { + beforeEach(() => { + document.body.innerHTML = ` + + + + + ` + }) + + it('unchecks other groups when member is checked', async () => { + await loadApp() + + const member = document.querySelector('[value="siteconseil_member"]') + const admin = document.querySelector('[value="siteconseil_admin"]') + 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() + + const member = document.querySelector('[value="siteconseil_member"]') + const admin = document.querySelector('[value="siteconseil_admin"]') + 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() + + const member = document.querySelector('[value="siteconseil_member"]') + const admin = document.querySelector('[value="siteconseil_admin"]') + + member.checked = true + admin.checked = false + admin.dispatchEvent(new Event('change')) + + expect(member.checked).toBe(true) + }) + }) + + describe('Stats period selector', () => { + beforeEach(() => { + document.body.innerHTML = ` + + + ` + }) + + 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', () => { + it('prevents submission when confirm is cancelled', async () => { + document.body.innerHTML = '
' + window.confirm = vi.fn(() => false) + + await loadApp() + const form = document.querySelector('form') + const event = new Event('submit', { cancelable: true }) + form.dispatchEvent(event) + + expect(event.defaultPrevented).toBe(true) + }) + + it('allows submission when confirm is accepted', async () => { + document.body.innerHTML = '
' + window.confirm = vi.fn(() => true) + + await loadApp() + const form = document.querySelector('form') + const event = new Event('submit', { cancelable: true }) + form.dispatchEvent(event) + + expect(event.defaultPrevented).toBe(false) + }) + }) + + describe('Sidebar dropdown', () => { + it('registers click handlers on dropdown buttons', async () => { + document.body.innerHTML = `
` + await loadApp() + + const btn = document.querySelector('.sidebar-dropdown-btn') + // Verify the button exists and has the expected structure + expect(btn).not.toBeNull() + expect(btn.querySelector('.sidebar-dropdown-arrow')).not.toBeNull() + expect(btn.nextElementSibling).not.toBeNull() + }) + }) + + describe('Mobile sidebar', () => { + beforeEach(() => { + document.body.innerHTML = ` + +
+
+ ` + }) + + 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 = ` + + + + + ` + 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 = ` + + + + ` + }) + + it('shows banner when no consent', async () => { + await loadApp() + expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(false) + }) + + it('hides banner when already accepted', async () => { + localStorage.setItem('cookie_consent', 'accepted') + await loadApp() + expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(true) + }) + + it('hides banner and stores accepted on accept click', async () => { + await loadApp() + document.getElementById('cookie-accept').click() + + expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(true) + expect(localStorage.getItem('cookie_consent')).toBe('accepted') + }) + + it('hides banner and stores refused on refuse click', async () => { + await loadApp() + document.getElementById('cookie-refuse').click() + + expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(true) + expect(localStorage.getItem('cookie_consent')).toBe('refused') + }) + }) + + describe('Tarif tabs', () => { + beforeEach(() => { + document.body.innerHTML = ` +
+ + +
+
NDD content
+ + ` + }) + + 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('renderHit and performSearch', () => { + it('search setup does nothing without elements', async () => { + document.body.innerHTML = '' + await loadApp() + // No error thrown + expect(true).toBe(true) + }) + }) +})