Python 68.8%
TypeScript 18.6%
CSS 8.7%
JavaScript 3.3%
HTML 0.6%
1// -----------------------------------------------------------------------------2// Rent-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// scripts/test-sync.mjs : validation E2E de la synchronisation liste ↔ carte5// (critères d'acceptation 1-9 du chantier « sync légendaire »).6// Usage : node scripts/test-sync.mjs (RENTKA_BASE pour cibler un déploiement)7// Captures : reports/sync-validation/*.png8// -----------------------------------------------------------------------------9import { chromium, devices } from 'playwright';10import { mkdirSync } from 'fs';1112const BASE = process.env.RENTKA_BASE || 'http://localhost:8125';13const OUT = new URL('../reports/sync-validation/', import.meta.url).pathname;14mkdirSync(OUT, { recursive: true });1516let passed = 0, failed = 0;17const results = [];18function check(name, ok, detail = '') {19 results.push({ name, ok, detail });20 if (ok) { passed++; console.log(` ✓ ${name}${detail ? ` — ${detail}` : ''}`); }21 else { failed++; console.log(` ✗ ${name}${detail ? ` — ${detail}` : ''}`); }22}2324const browser = await chromium.launch();2526const initScript = () => {27 localStorage.setItem('rentka-consent-v1',28 JSON.stringify({ essential: true, preferences: true, statistics: false, ts: 1 }));29 sessionStorage.setItem('rentka_geo_session', '1'); // pas de prompt géoloc ici30};3132async function newDesktop() {33 const ctx = await browser.newContext({34 viewport: { width: 1440, height: 900 }, locale: 'fr-CA',35 });36 await ctx.addInitScript(initScript);37 return ctx;38}3940const engineState = (page) => page.evaluate(() => {41 const m = globalThis.__kaMap;42 if (!m) return null;43 const st = m.getState();44 return { ...st, nProps: m.getState().activeLayers.length >= 045 ? (m.map.querySourceFeatures ? undefined : undefined) : undefined };46});4748const engineCount = (page) => page.evaluate(() => {49 const m = globalThis.__kaMap;50 // byId est privé : compter via les données GeoJSON poussées51 return m ? m['byId'].size : -1;52});5354const counterTotal = async (page) => {55 const txt = await page.locator('.ms-count b').first().textContent({ timeout: 15000 });56 return Number(txt.replace(/\s| |,/g, ''));57};5859const canvasBox = async (page) => {60 await page.locator('.mapview').scrollIntoViewIfNeeded();61 await page.waitForTimeout(400);62 return page.locator('.ka-map-canvas').boundingBox();63};6465const waitSettled = async (page, ms = 1200) => {66 await page.waitForTimeout(ms);67 await page.waitForFunction(() => !document.querySelector('.ms-progress'), null, { timeout: 20000 })68 .catch(() => {});69};7071// =============================== TESTS =====================================72console.log('— Vue carte desktop —');73{74 const ctx = await newDesktop();75 const page = await ctx.newPage();76 await page.goto(`${BASE}/?view=carte`, { waitUntil: 'domcontentloaded' });77 await page.waitForSelector('.ms-count b', { timeout: 30000 });78 await page.waitForFunction(() => {79 const m = globalThis.__kaMap;80 return m && m['byId'].size > 0;81 }, null, { timeout: 30000 });82 await waitSettled(page, 2500);8384 // C1 — compteur liste = points carte85 const total = await counterTotal(page);86 const nPts = await engineCount(page);87 check('C1 compteur liste = points carte (arrivée)', total === nPts, `${total} = ${nPts}`);88 await page.screenshot({ path: `${OUT}01-carte-desktop.png` });8990 // C2/C5 — déplacement carte → liste mise à jour, jamais de résultat périmé91 const urlBefore = page.url();92 const cb = await canvasBox(page);93 const mx = cb.x + cb.width / 2, my = cb.y + cb.height / 2;94 await page.mouse.move(mx, my);95 await page.mouse.down();96 await page.mouse.move(mx - 260, my - 60, { steps: 12 });97 await page.mouse.up();98 await waitSettled(page, 1800);99 const total2 = await counterTotal(page);100 const nPts2 = await engineCount(page);101 check('C2 déplacement → compteur/liste rafraîchis, égalité conservée',102 total2 === nPts2, `${total2} = ${nPts2}`);103 check('C7 caméra dans l’URL après déplacement',104 page.url() !== urlBefore && /lat=/.test(page.url()));105106 // rafale de mouvements rapides : la dernière réponse gagne toujours107 for (let i = 0; i < 4; i++) {108 await page.mouse.move(mx, my);109 await page.mouse.down();110 await page.mouse.move(mx - 70 * (i + 1), my + 20, { steps: 4 });111 await page.mouse.up();112 await page.waitForTimeout(120);113 }114 await waitSettled(page, 2200);115 const total3 = await counterTotal(page);116 const nPts3 = await engineCount(page);117 check('C5 mouvements rapides successifs → aucun résultat périmé',118 total3 === nPts3, `${total3} = ${nPts3}`);119120 // retour dans une zone dense avant les tests de miroirs121 await page.goto(`${BASE}/?view=carte&lat=46.81&lng=-71.23&zoom=13`,122 { waitUntil: 'domcontentloaded' });123 await page.waitForSelector('.map-card', { timeout: 30000 });124 await waitSettled(page, 2500);125126 // C3 — survol d'une carte de liste → marqueur en survol < 100 ms127 const firstCard = page.locator('.map-card').first();128 const uid = await firstCard.getAttribute('data-uid');129 await firstCard.hover();130 await page.waitForTimeout(90);131 const hovered = await page.evaluate(() => globalThis.__kaMap.getState().hoveredPropertyId);132 check('C3 survol liste → marqueur illuminé (< 100 ms)', hovered === uid, `${hovered}`);133134 // C3 inverse — survol venu de la carte → carte de liste surlignée135 await page.mouse.move(10, 10); // souris hors de la liste136 await page.evaluate(() => globalThis.__kaMap.setHovered(null, 'map'));137 await page.waitForTimeout(60);138 await page.evaluate((u) => globalThis.__kaMap.setHovered(u, 'map'), uid);139 await page.waitForTimeout(90);140 const hl = await page.locator(`.map-card[data-uid="${uid}"]`).getAttribute('class');141 check('C3 survol carte → annonce surlignée dans la liste', hl.includes('map-card-hover'));142 await page.evaluate(() => globalThis.__kaMap.setHovered(null, 'map'));143144 // C4 — clic marqueur (simulé moteur) → liste défile vers l'annonce + mini-fiche145 const farUid = await page.evaluate(() => {146 const m = globalThis.__kaMap;147 const ids = [...m['byId'].keys()];148 return ids[Math.min(ids.length - 1, 30)];149 });150 await page.evaluate((u) => globalThis.__kaMap.select(u, 'map'), farUid);151 await page.waitForTimeout(1400);152 const selVisible = await page.evaluate((u) => {153 const el = document.querySelector(`.map-card[data-uid="${CSS.escape(u)}"]`);154 if (!el) return 'absente';155 const r = el.getBoundingClientRect();156 const c = el.closest('.ms-list').getBoundingClientRect();157 return r.bottom > c.top && r.top < c.bottom ? 'visible' : 'hors-vue';158 }, farUid);159 check('C4 clic marqueur → défilement de la liste jusqu’à l’annonce',160 selVisible === 'visible', selVisible);161 const preview = await page.locator('.ka-preview').count();162 check('C4 mini-fiche ouverte au clic marqueur', preview === 1);163 await page.screenshot({ path: `${OUT}02-selection-miroir.png` });164165 // C4 inverse — clic annonce → carte recentrée sur le marqueur + sélection166 await page.evaluate(() => globalThis.__kaMap.select(null, 'app'));167 const cardUid = await page.locator('.map-card').nth(2).getAttribute('data-uid');168 await page.locator('.map-card').nth(2).click();169 await page.waitForTimeout(900);170 const selId = await page.evaluate(() => globalThis.__kaMap.getSelectedId());171 check('C4 clic annonce → marqueur sélectionné sur la carte', selId === cardUid);172173 // C2 — filtre resserré depuis l'URL partagée → les deux vues suivent174 await ctx.close();175}176177console.log('— Filtres → carte (compteur partagé) —');178{179 const ctx = await newDesktop();180 const page = await ctx.newPage();181 await page.goto(`${BASE}/?view=carte&price_max=900`, { waitUntil: 'domcontentloaded' });182 await page.waitForSelector('.ms-count b', { timeout: 30000 });183 await waitSettled(page, 2500);184 const total = await counterTotal(page);185 const nPts = await engineCount(page);186 check('C1 filtre loyer ≤ 900 $ : compteur = points', total === nPts, `${total} = ${nPts}`);187188 // tri partagé : passer en « Meilleures affaires » réordonne liste + points189 await page.locator('.ms-sort select').selectOption('deal');190 await waitSettled(page, 1800);191 const firstUid = await page.locator('.map-card').first().getAttribute('data-uid');192 const firstPointUid = await page.evaluate(() => [...globalThis.__kaMap['byId'].keys()][0]);193 check('C1 tri partagé : 1er de la liste = 1er point du tri', firstUid === firstPointUid,194 `${firstUid}`);195 await page.screenshot({ path: `${OUT}03-filtre-tri.png` });196 await ctx.close();197}198199console.log('— Zone dessinée (polygone) —');200{201 const ctx = await newDesktop();202 const page = await ctx.newPage();203 await page.goto(`${BASE}/?view=carte&lat=46.81&lng=-71.23&zoom=13`,204 { waitUntil: 'domcontentloaded' });205 await page.waitForSelector('.ka-draw-btn', { timeout: 30000 });206 await waitSettled(page, 2500);207 const box = await canvasBox(page);208 await page.locator('.ka-draw-btn').click();209 const cx = box.x + box.width / 2, cy = box.y + box.height / 2;210 await page.mouse.click(cx - 160, cy - 80);211 await page.mouse.click(cx + 160, cy - 60);212 await page.mouse.click(cx + 40, cy + 140);213 await page.mouse.click(cx - 160, cy - 80); // fermeture sur le 1er sommet214 await waitSettled(page, 2000);215 const chip = await page.locator('.ms-pills .pill').count();216 check('Polygone → puce « Zone dessinée » visible', chip >= 1);217 const total = await counterTotal(page);218 const nPts = await engineCount(page);219 check('C1 polygone : compteur = points', total === nPts && total > 0, `${total} = ${nPts}`);220 check('C7 zone dans l’URL', /zone=/.test(page.url()));221 await page.screenshot({ path: `${OUT}04-polygone.png` });222 // retrait de la puce → retour à la zone visible223 await page.locator('.ms-pills .pill').first().click();224 await waitSettled(page, 1500);225 check('Puce retirée → polygone effacé', !/zone=/.test(page.url()));226 await ctx.close();227}228229console.log('— Mode manuel & zéro résultat —');230{231 const ctx = await newDesktop();232 const page = await ctx.newPage();233 await page.goto(`${BASE}/?view=carte&lat=46.81&lng=-71.23&zoom=12`,234 { waitUntil: 'domcontentloaded' });235 await page.waitForSelector('.ka-search-area-auto input', { timeout: 30000 });236 await waitSettled(page, 2500);237 await page.locator('.ka-search-area-auto input').setChecked(false);238 const mb = await canvasBox(page);239 const mmx = mb.x + mb.width / 2, mmy = mb.y + mb.height / 2;240 await page.mouse.move(mmx, mmy);241 await page.mouse.down();242 await page.mouse.move(mmx - 320, mmy - 30, { steps: 10 });243 await page.mouse.up();244 await page.waitForTimeout(900);245 const btn = await page.locator('.ka-search-area-btn').count();246 check('Case décochée + déplacement → « Rechercher dans cette zone »', btn === 1);247 check('C7 move=0 dans l’URL', /move=0/.test(page.url()));248249 // zéro résultat : plein nord250 await page.goto(`${BASE}/?view=carte&lat=55.5&lng=-70.0&zoom=9`,251 { waitUntil: 'domcontentloaded' });252 await page.waitForSelector('.ms-empty, .ka-empty', { timeout: 30000 });253 await waitSettled(page, 2000);254 const emptyTxt = await page.locator('.ms-empty h3').first().textContent().catch(() => '');255 check('C9 zone vide → état vide propre + « Élargir la zone »',256 (emptyTxt || '').includes('Aucun logement'),257 emptyTxt?.trim());258 const widen = await page.locator('.ms-empty .btn-primary').count();259 check('C9 bouton « Élargir la zone » présent', widen === 1);260 await page.screenshot({ path: `${OUT}05-zero-resultat.png` });261 await ctx.close();262}263264console.log('— URL partagée = recherche reproduite —');265{266 const ctx = await newDesktop();267 const page = await ctx.newPage();268 const shared = `${BASE}/?view=carte&price_max=1200&unit_type=3%C2%BD&lat=46.80&lng=-71.24&zoom=13&tri=recent`;269 await page.goto(shared, { waitUntil: 'domcontentloaded' });270 await page.waitForSelector('.ms-count b', { timeout: 30000 });271 await waitSettled(page, 2500);272 const total = await counterTotal(page);273 const nPts = await engineCount(page);274 const sortVal = await page.locator('.ms-sort select').inputValue();275 const zoom = await page.evaluate(() => Math.round(globalThis.__kaMap.getState().zoom * 10) / 10);276 check('C7 URL partagée : filtres + zone + tri reproduits',277 total === nPts && sortVal === 'recent' && Math.abs(zoom - 13) < 0.6,278 `${total} résultats, tri=${sortVal}, zoom=${zoom}`);279 await page.screenshot({ path: `${OUT}06-url-partagee.png` });280 await ctx.close();281}282283console.log('— Mobile : bascule + carrousel synchronisé —');284{285 const ctx = await browser.newContext({ ...devices['iPhone 14'], locale: 'fr-CA' });286 await ctx.addInitScript(initScript);287 const page = await ctx.newPage();288 await page.goto(`${BASE}/?view=carte&lat=46.81&lng=-71.23&zoom=13`,289 { waitUntil: 'domcontentloaded' });290 await page.waitForSelector('.ms-carousel', { timeout: 30000 });291 await waitSettled(page, 3000);292 const items = await page.locator('.ms-car-item').count();293 check('C8 carrousel mobile présent', items > 0, `${items} cartes`);294295 // marqueur → carrousel : sélection du 5e point → carte correspondante mise en avant296 const uid5 = await page.evaluate(() => [...globalThis.__kaMap['byId'].keys()][4]);297 await page.evaluate((u) => globalThis.__kaMap.select(u, 'map'), uid5);298 await page.waitForTimeout(1200);299 const selCar = await page.locator('.ms-car-item.sel').getAttribute('data-uid').catch(() => null);300 check('C8 taper un marqueur → carrousel défile sur l’annonce', selCar === uid5, `${selCar}`);301302 // carrousel → marqueur : balayage → sélection carte303 await page.locator('.ms-carousel').evaluate((el) => { el.scrollLeft += el.clientWidth * 2; });304 await page.waitForTimeout(700);305 const selId = await page.evaluate(() => globalThis.__kaMap.getSelectedId());306 check('C8 balayer le carrousel → sélection déplacée sur la carte',307 selId !== null && selId !== uid5, `${selId}`);308 await page.screenshot({ path: `${OUT}07-mobile-carrousel.png` });309310 // bascule Liste ↔ Carte sans perte d'état311 const urlCarte = page.url();312 await page.locator('.view-toggle button').first().click(); // Liste313 await page.waitForTimeout(800);314 await page.locator('.view-toggle button').nth(1).click(); // Carte315 await page.waitForSelector('.ms-carousel', { timeout: 20000 });316 await waitSettled(page, 2500);317 check('C8 bascule Liste ↔ Carte : état conservé (URL stable)',318 /view=carte/.test(page.url()) && /lat=/.test(page.url()),319 page.url().replace(BASE, ''));320 await page.screenshot({ path: `${OUT}08-mobile-carte.png` });321 await ctx.close();322}323324console.log('— Géolocalisation d’accueil —');325{326 const ctx = await browser.newContext({327 viewport: { width: 1440, height: 900 }, locale: 'fr-CA',328 geolocation: { latitude: 46.8139, longitude: -71.2080 },329 permissions: ['geolocation'],330 });331 await ctx.addInitScript(() => {332 localStorage.setItem('rentka-consent-v1',333 JSON.stringify({ essential: true, preferences: true, statistics: false, ts: 1 }));334 });335 const page = await ctx.newPage();336 await page.goto(`${BASE}/`, { waitUntil: 'domcontentloaded' });337 await page.waitForFunction(() => /view=carte/.test(location.search), null, { timeout: 20000 })338 .catch(() => {});339 const ok = /view=carte/.test(page.url()) && /lat=46\.81/.test(page.url());340 check('Arrivée + géoloc accordée → vue carte centrée sur l’utilisateur', ok,341 page.url().replace(BASE, ''));342 await page.waitForSelector('.ms-count b', { timeout: 30000 });343 await waitSettled(page, 2500);344 await page.screenshot({ path: `${OUT}09-geolocalisation.png` });345 await ctx.close();346}347348await browser.close();349console.log(`\n${passed} réussis, ${failed} échoués`);350process.exit(failed > 0 ? 1 : 0);351