Cours HTML et CSS · Module 9: CSS moderne

Tests multi-navigateurs

17 min de lecture
Dans cette leçon7

Ta page s'affiche parfaitement dans Chrome sur ton ordinateur portable, mais sur l'iPhone d'un client, la grille s'effondre, car chaque navigateur est une équipe de tailleurs de pierre différente qui lit le même plan à sa façon. Les tests multi-navigateurs sont un processus qui garantit que les applications web fonctionnent correctement sur tous les navigateurs pris en charge et sur divers appareils. Les différences d'implémentation des standards peuvent entraîner des incohérences d'apparence et de fonctionnalité.

Les bases des tests multi-navigateurs

Principaux navigateurs à tester

Les navigateurs modernes reposent sur trois moteurs : Blink (Chrome, Edge, Opera, Samsung Internet), WebKit (Safari) et Gecko (Firefox) :

1// Statistiques d'utilisation des navigateurs (2024)
2const browserStats = {
3  chrome: "65%",      // Moteur Blink
4  safari: "19%",      // Moteur WebKit
5  edge: "5%",         // Moteur Blink (basé sur Chromium)
6  firefox: "3%",      // Moteur Gecko
7  opera: "2%",        // Moteur Blink
8  others: "6%"
9};
10
11// Navigateurs mobiles
12const mobileBrowsers = {
13  chrome: "65%",
14  safari: "25%",
15  samsung: "4%",
16  opera: "2%",
17  firefox: "1%",
18  others: "3%"
19};

Les chiffres de l'exemple sont indicatifs, et tu trouveras les parts de marché actuelles sur des services comme StatCounter. Sur iPhone, les navigateurs ont dû pendant des années utiliser WebKit, donc en testant le moteur de Safari, tu vérifies en grande partie aussi Chrome sur iOS.

Différences clés entre les navigateurs

1. Support du CSS

Autrefois, les nouvelles propriétés arrivaient dans les navigateurs avec des préfixes propres à chaque éditeur. On écrit d'abord les versions préfixées, et la version standard à la fin :

1/* Préfixes vendeurs pour les fonctionnalités plus récentes */
2.element {
3  -webkit-transform: rotate(45deg);  /* Safari, Chrome */
4  -moz-transform: rotate(45deg);     /* Firefox */
5  -ms-transform: rotate(45deg);      /* Internet Explorer */
6  transform: rotate(45deg);          /* Standard */
7}
8
9/* Différences d'implémentation de Grid */
10.grid-container {
11  display: grid;
12  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
13  gap: 20px; /* IE ne prend pas en charge gap dans Grid */
14}
15
16/* Repli pour IE */
17@supports not (gap: 20px) {
18  .grid-container > * {
19    margin: 10px;
20  }
21}

L'ordre compte : un navigateur qui connaît le standard utilisera la dernière déclaration. Aujourd'hui, transform n'a plus besoin de préfixes, et Autoprefixer ajoutera ceux qui restent nécessaires.

2. Différences entre les API JavaScript

En JavaScript, avant d'utiliser une nouvelle API, on vérifie qu'elle existe (feature detection) :

1// Fetch API - non prise en charge dans IE
2function fetchDataCompatible(url) {
3  if (window.fetch) {
4    // Navigateurs modernes
5    return fetch(url).then(response => response.json());
6  } else {
7    // Repli pour les navigateurs plus anciens
8    return new Promise((resolve, reject) => {
9      const xhr = new XMLHttpRequest();
10      xhr.open('GET', url);
11      xhr.onload = () => {
12        if (xhr.status === 200) {
13          resolve(JSON.parse(xhr.responseText));
14        } else {
15          reject(new Error('Request failed'));
16        }
17      };
18      xhr.onerror = () => reject(new Error('Network error'));
19      xhr.send();
20    });
21  }
22}
23
24// Intersection Observer - nécessite un polyfill dans IE
25function observeElementsCompatible(elements, callback) {
26  if ('IntersectionObserver' in window) {
27    const observer = new IntersectionObserver(callback);
28    elements.forEach(el => observer.observe(el));
29  } else {
30    // Repli - écouteur de défilement
31    function checkVisibility() {
32      elements.forEach(element => {
33        const rect = element.getBoundingClientRect();
34        const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
35        if (isVisible) callback([{ target: element, isIntersecting: true }]);
36      });
37    }
38
39    window.addEventListener('scroll', checkVisibility);
40    window.addEventListener('resize', checkVisibility);
41    checkVisibility();
42  }
43}

Quand fetch ou IntersectionObserver n'existe pas, le code se rabat sur une solution plus ancienne. On vérifie la fonctionnalité, et non le nom du navigateur.

Outils de tests multi-navigateurs

1. BrowserStack

Les fermes d'appareils dans le cloud mettent à disposition de vrais navigateurs et systèmes pour les tests automatisés :

1// Configuration de BrowserStack pour les tests automatisés
2const browserStackConfig = {
3  user: process.env.BROWSERSTACK_USERNAME,
4  key: process.env.BROWSERSTACK_ACCESS_KEY,
5
6  capabilities: [
7    {
8      browserName: 'Chrome',
9      browserVersion: 'latest',
10      os: 'Windows',
11      osVersion: '10'
12    },
13    {
14      browserName: 'Safari',
15      browserVersion: 'latest',
16      os: 'OS X',
17      osVersion: 'Big Sur'
18    },
19    {
20      browserName: 'Firefox',
21      browserVersion: 'latest',
22      os: 'Windows',
23      osVersion: '10'
24    }
25  ]
26};

Les identifiants proviennent des variables d'environnement, jamais du code.

2. Sauce Labs

Sauce Labs fonctionne de la même manière, et la configuration en YAML indique les suites de tests Cypress et les navigateurs :

1# .sauce/config.yml
2apiVersion: v1alpha
3kind: cypress
4sauce:
5  region: eu-central-1
6
7suites:
8  - name: "Cross Browser Tests"
9    browser: "chrome"
10    browserVersion: "latest"
11    platformName: "Windows 10"
12
13  - name: "Safari Tests"
14    browser: "safari"
15    browserVersion: "latest"
16    platformName: "macOS 11.00"
17
18cypress:
19  configFile: "cypress.config.js"
20  version: "12.0.0"

Chaque suite se lancera dans le cloud sur son propre navigateur.

3. Playwright

Playwright exécute une même suite de tests dans trois moteurs, Chromium, Firefox et WebKit, et émule en plus des téléphones :

1// playwright.config.js - configuration des tests multi-navigateurs
2const { defineConfig, devices } = require('@playwright/test');
3
4module.exports = defineConfig({
5  testDir: './tests',
6
7  projects: [
8    {
9      name: 'chromium',
10      use: { ...devices['Desktop Chrome'] },
11    },
12    {
13      name: 'firefox',
14      use: { ...devices['Desktop Firefox'] },
15    },
16    {
17      name: 'webkit',
18      use: { ...devices['Desktop Safari'] },
19    },
20    {
21      name: 'Mobile Chrome',
22      use: { ...devices['Pixel 5'] },
23    },
24    {
25      name: 'Mobile Safari',
26      use: { ...devices['iPhone 12'] },
27    },
28  ],
29});
30
31// Exemple de test multi-navigateurs
32// tests/cross-browser.spec.js
33const { test, expect } = require('@playwright/test');
34
35test.describe('Cross-browser compatibility', () => {
36  test('CSS Grid layout works correctly', async ({ page }) => {
37    await page.goto('/grid-layout');
38
39    // Vérifier que la grille s'affiche correctement
40    const gridContainer = page.locator('.grid-container');
41    await expect(gridContainer).toBeVisible();
42
43    // Vérifier le nombre de colonnes
44    const gridItems = page.locator('.grid-item');
45    const count = await gridItems.count();
46    expect(count).toBeGreaterThan(0);
47
48    // Vérifier la réactivité
49    await page.setViewportSize({ width: 768, height: 1024 });
50    await expect(gridContainer).toBeVisible();
51  });
52
53  test('JavaScript features work across browsers', async ({ page }) => {
54    await page.goto('/js-features');
55
56    // Tester la Fetch API ou le repli XHR
57    const result = await page.evaluate(async () => {
58      const response = await fetchDataCompatible('/api/test');
59      return response.status === 'ok';
60    });
61
62    expect(result).toBe(true);
63  });
64});

Les projets s'appellent chromium, firefox et webkit, et la commande npx playwright test --project=webkit n'en lance qu'un seul. Pour débuter, je te recommande justement Playwright car il fonctionne en local et gratuitement.

4. Environnements de test locaux

Tu peux monter ta propre ferme avec Docker. Selenium Grid se compose d'un hub et de nœuds équipés de navigateurs :

1# Configuration de Selenium Grid pour les tests locaux
2# docker-compose.yml
3version: '3'
4services:
5  selenium-hub:
6    image: selenium/hub:4.0.0
7    container_name: selenium-hub
8    ports:
9      - "4444:4444"
10
11  chrome:
12    image: selenium/node-chrome:4.0.0
13    shm_size: 2gb
14    depends_on:
15      - selenium-hub
16    environment:
17      - HUB_HOST=selenium-hub
18      - HUB_PORT=4444
19
20  firefox:
21    image: selenium/node-firefox:4.0.0
22    shm_size: 2gb
23    depends_on:
24      - selenium-hub
25    environment:
26      - HUB_HOST=selenium-hub
27      - HUB_PORT=4444

Le hub répartit les tests entre les nœuds Chrome et Firefox. Safari n'est pas présent dans ces images, et pour lui, tu as besoin d'un Mac ou du cloud.

Stratégies de compatibilité

1. Amélioration progressive

L'amélioration progressive (progressive enhancement) consiste à partir d'une base qui fonctionne pour tout le monde, puis à ajouter des améliorations pour les navigateurs qui les prennent en charge :

1/* Styles de base pour tous les navigateurs */
2.card {
3  border: 1px solid #ddd;
4  padding: 16px;
5  margin-bottom: 16px;
6  background: white;
7}
8
9/* Améliorations pour les navigateurs modernes */
10@supports (display: grid) {
11  .card-container {
12    display: grid;
13    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
14    gap: 20px;
15  }
16
17  .card {
18    margin-bottom: 0;
19  }
20}
21
22/* Améliorations supplémentaires */
23@supports (backdrop-filter: blur(10px)) {
24  .card--glass {
25    background: rgba(255, 255, 255, 0.8);
26    backdrop-filter: blur(10px);
27  }
28}

Tout le monde reçoit des cartes lisibles, les navigateurs qui gèrent Grid reçoivent la grille, et ceux qui gèrent backdrop-filter l'effet de verre. Tu appliques la même stratégie en JavaScript :

1// Amélioration progressive en JavaScript
2class FeatureDetector {
3  static supportsIntersectionObserver() {
4    return 'IntersectionObserver' in window;
5  }
6
7  static supportsWebP() {
8    const canvas = document.createElement('canvas');
9    canvas.width = 1;
10    canvas.height = 1;
11    return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
12  }
13
14  static supportsCustomProperties() {
15    return CSS.supports && CSS.supports('(--foo: red)');
16  }
17
18  static init() {
19    // Lazy loading avec repli
20    if (this.supportsIntersectionObserver()) {
21      this.initLazyLoadingModern();
22    } else {
23      this.initLazyLoadingFallback();
24    }
25
26    // Détection du support WebP
27    if (this.supportsWebP()) {
28      document.documentElement.classList.add('webp');
29    } else {
30      document.documentElement.classList.add('no-webp');
31    }
32  }
33
34  static initLazyLoadingModern() {
35    const images = document.querySelectorAll('img[data-src]');
36    const imageObserver = new IntersectionObserver((entries) => {
37      entries.forEach(entry => {
38        if (entry.isIntersecting) {
39          const img = entry.target;
40          img.src = img.dataset.src;
41          img.classList.remove('lazy');
42          imageObserver.unobserve(img);
43        }
44      });
45    });
46
47    images.forEach(img => imageObserver.observe(img));
48  }
49
50  static initLazyLoadingFallback() {
51    const images = document.querySelectorAll('img[data-src]');
52
53    function loadImagesInViewport() {
54      images.forEach(img => {
55        if (img.dataset.src) {
56          const rect = img.getBoundingClientRect();
57          if (rect.top < window.innerHeight + 100) {
58            img.src = img.dataset.src;
59            delete img.dataset.src;
60            img.classList.remove('lazy');
61          }
62        }
63      });
64    }
65
66    window.addEventListener('scroll', loadImagesInViewport);
67    window.addEventListener('resize', loadImagesInViewport);
68    loadImagesInViewport();
69  }
70}
71
72// Initialisation au chargement de la page
73document.addEventListener('DOMContentLoaded', () => {
74  FeatureDetector.init();
75});

Les classes webp et no-webp sur l'élément html permettent de choisir les images en CSS. Pour le chargement différé des images, l'attribut loading="lazy" suffit aujourd'hui.

2. Dégradation gracieuse

La dégradation gracieuse (graceful degradation) va dans l'autre sens : tu conçois pour les navigateurs modernes et tu veilles à ce que les plus anciens donnent encore un résultat acceptable :

1/* Styles modernes avec replis */
2.hero-section {
3  /* Repli pour les anciens navigateurs */
4  background: linear-gradient(to right, #667eea, #764ba2);
5
6  /* Styles modernes */
7  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
8  min-height: 100vh;
9  display: flex;
10  align-items: center;
11  justify-content: center;
12}
13
14/* Repli pour flexbox */
15.hero-section {
16  text-align: center;
17  padding-top: 20vh;
18}
19
20@supports (display: flex) {
21  .hero-section {
22    padding-top: 0;
23    text-align: left;
24  }
25}
26
27/* Variables CSS avec repli */
28.theme-primary {
29  color: #007bff; /* Repli */
30  color: var(--primary-color, #007bff);
31}

Une règle CSS simple s'applique ici : un navigateur qui ne comprend pas la seconde déclaration s'en tient à la première. C'est pourquoi la couleur ordinaire se trouve avant la version avec var().

3. Polyfills et shims

Un polyfill est un code qui fournit une fonctionnalité manquante aux navigateurs plus anciens qui ne la prennent pas en charge nativement :

1// Polyfill pour Array.includes (IE)
2if (!Array.prototype.includes) {
3  Array.prototype.includes = function(searchElement, fromIndex) {
4    return this.indexOf(searchElement, fromIndex) !== -1;
5  };
6}
7
8// Polyfill pour Object.assign
9if (!Object.assign) {
10  Object.assign = function(target, ...sources) {
11    if (target == null) {
12      throw new TypeError('Cannot convert undefined or null to object');
13    }
14
15    const to = Object(target);
16
17    for (let index = 0; index < sources.length; index++) {
18      const nextSource = sources[index];
19      if (nextSource != null) {
20        for (const nextKey in nextSource) {
21          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
22            to[nextKey] = nextSource[nextKey];
23          }
24        }
25      }
26    }
27
28    return to;
29  };
30}
31
32// Polyfill pour Intersection Observer
33(function() {
34  if ('IntersectionObserver' in window) return;
35
36  // Implémentation de base du polyfill
37  window.IntersectionObserver = class {
38    constructor(callback, options = {}) {
39      this.callback = callback;
40      this.options = options;
41      this.observedElements = new Set();
42      this.checkIntersections = this.checkIntersections.bind(this);
43
44      this.startListening();
45    }
46
47    observe(element) {
48      this.observedElements.add(element);
49      this.checkIntersections();
50    }
51
52    unobserve(element) {
53      this.observedElements.delete(element);
54    }
55
56    disconnect() {
57      this.observedElements.clear();
58      this.stopListening();
59    }
60
61    startListening() {
62      window.addEventListener('scroll', this.checkIntersections);
63      window.addEventListener('resize', this.checkIntersections);
64    }
65
66    stopListening() {
67      window.removeEventListener('scroll', this.checkIntersections);
68      window.removeEventListener('resize', this.checkIntersections);
69    }
70
71    checkIntersections() {
72      const entries = [];
73
74      this.observedElements.forEach(element => {
75        const rect = element.getBoundingClientRect();
76        const isIntersecting = (
77          rect.top < window.innerHeight &&
78          rect.bottom > 0 &&
79          rect.left < window.innerWidth &&
80          rect.right > 0
81        );
82
83        entries.push({
84          target: element,
85          isIntersecting,
86          boundingClientRect: rect,
87          intersectionRatio: isIntersecting ? 1 : 0
88        });
89      });
90
91      if (entries.length > 0) {
92        this.callback(entries);
93      }
94    }
95  };
96})();

Chaque polyfill vérifie d'abord si la fonctionnalité existe déjà, et si c'est le cas, il n'écrase rien. Internet Explorer a pris sa retraite en 2022, donc beaucoup de ces correctifs sont aujourd'hui inutiles.

Automatisation des tests multi-navigateurs

1. Workflow GitHub Actions

Tu lances automatiquement les tests Playwright à chaque push, et la matrice crée une tâche pour chaque moteur :

1# .github/workflows/cross-browser-tests.yml
2name: Cross-Browser Tests
3
4on:
5  push:
6    branches: [ main, develop ]
7  pull_request:
8    branches: [ main ]
9
10jobs:
11  test:
12    runs-on: ubuntu-latest
13
14    strategy:
15      matrix:
16        browser: ['chromium', 'firefox', 'webkit']
17
18    steps:
19    - uses: actions/checkout@v4
20
21    - name: Setup Node.js
22      uses: actions/setup-node@v4
23      with:
24        node-version: '22'
25        cache: 'npm'
26
27    - name: Install dependencies
28      run: npm ci
29
30    - name: Install Playwright
31      run: npx playwright install --with-deps
32
33    - name: Run tests
34      run: npx playwright test --project=${{ matrix.browser }}
35
36    - name: Upload test results
37      uses: actions/upload-artifact@v4
38      if: failure()
39      with:
40        name: test-results-${{ matrix.browser }}
41        path: test-results/

Les noms de la matrice correspondent aux projets Playwright, ce qui donne trois exécutions en parallèle.

2. Configuration Jest pour différents environnements

Les tests unitaires avec Jest s'exécutent dans jsdom, une simulation de navigateur dans Node.js, sans véritable moteur de rendu :

1// jest.config.js
2module.exports = {
3  projects: [
4    {
5      displayName: 'jsdom',
6      testEnvironment: 'jsdom',
7      testMatch: ['<rootDir>/src/**/*.test.js'],
8      setupFilesAfterEnv: ['<rootDir>/src/setupTests.js']
9    },
10    {
11      displayName: 'node',
12      testEnvironment: 'node',
13      testMatch: ['<rootDir>/server/**/*.test.js']
14    }
15  ],
16
17  // Configuration globale pour les tests multi-navigateurs
18  globalSetup: '<rootDir>/test/global-setup.js',
19  globalTeardown: '<rootDir>/test/global-teardown.js'
20};
21
22// setupTests.js - polyfills pour les tests
23import 'intersection-observer';
24import 'whatwg-fetch';
25
26// Mock pour matchMedia (non pris en charge dans jsdom)
27Object.defineProperty(window, 'matchMedia', {
28  writable: true,
29  value: jest.fn().mockImplementation(query => ({
30    matches: false,
31    media: query,
32    onchange: null,
33    addListener: jest.fn(),
34    removeListener: jest.fn(),
35    addEventListener: jest.fn(),
36    removeEventListener: jest.fn(),
37    dispatchEvent: jest.fn(),
38  })),
39});

jsdom ne calcule pas la mise en page et ne connaît pas matchMedia, c'est pourquoi on la simule avec un mock, et tu vérifies l'apparence CSS dans de vrais navigateurs.

Déboguer les problèmes multi-navigateurs

1. Différences entre les DevTools

Pour le débogage, un logger sûr et un tableau des fonctionnalités prises en charge par le navigateur sont bien utiles :

1// Débogage universel
2const Logger = {
3  log: (...args) => {
4    if (console && console.log) {
5      console.log(...args);
6    }
7  },
8
9  warn: (...args) => {
10    if (console && console.warn) {
11      console.warn(...args);
12    } else if (console && console.log) {
13      console.log('WARNING:', ...args);
14    }
15  },
16
17  error: (...args) => {
18    if (console && console.error) {
19      console.error(...args);
20    } else if (console && console.log) {
21      console.log('ERROR:', ...args);
22    }
23  },
24
25  table: (data) => {
26    if (console && console.table) {
27      console.table(data);
28    } else {
29      Logger.log('TABLE:', JSON.stringify(data, null, 2));
30    }
31  }
32};
33
34// Détection du navigateur pour le débogage
35const BrowserDetector = {
36  isIE: () => navigator.userAgent.indexOf('MSIE') !== -1 || navigator.userAgent.indexOf('Trident') !== -1,
37  isEdge: () => navigator.userAgent.indexOf('Edge') !== -1,
38  isChrome: () => navigator.userAgent.indexOf('Chrome') !== -1 && navigator.userAgent.indexOf('Edge') === -1,
39  isFirefox: () => navigator.userAgent.indexOf('Firefox') !== -1,
40  isSafari: () => navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1,
41
42  getInfo: () => ({
43    userAgent: navigator.userAgent,
44    vendor: navigator.vendor,
45    platform: navigator.platform,
46    language: navigator.language,
47    cookieEnabled: navigator.cookieEnabled,
48    onLine: navigator.onLine
49  })
50};
51
52// Débogage sur différents navigateurs
53function debugCompatibility() {
54  Logger.log('Browser Info:', BrowserDetector.getInfo());
55
56  // Tester les fonctionnalités CSS
57  const cssFeatures = {
58    flexbox: CSS.supports('display', 'flex'),
59    grid: CSS.supports('display', 'grid'),
60    customProperties: CSS.supports('--foo', 'red'),
61    backdropFilter: CSS.supports('backdrop-filter', 'blur(10px)'),
62    objectFit: CSS.supports('object-fit', 'cover')
63  };
64
65  Logger.table(cssFeatures);
66
67  // Tester les fonctionnalités JavaScript
68  const jsFeatures = {
69    fetch: typeof fetch !== 'undefined',
70    intersectionObserver: 'IntersectionObserver' in window,
71    customElements: 'customElements' in window,
72    serviceWorker: 'serviceWorker' in navigator,
73    webp: (() => {
74      const canvas = document.createElement('canvas');
75      return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
76    })()
77  };
78
79  Logger.table(jsFeatures);
80}
81
82// Appeler le débogage en mode développement
83if (process.env.NODE_ENV === 'development') {
84  debugCompatibility();
85}

CSS.supports() est le test le plus fiable. La détection par userAgent est fragile : le nouvel Edge se présente comme « Edg », donc isEdge ne le reconnaîtra pas.

2. Débogage à distance

Les erreurs survenues sur les téléphones sont difficiles à voir, c'est pourquoi ce script les intercepte et les envoie à un serveur :

1// Configuration du débogage à distance pour les appareils mobiles
2class RemoteDebugger {
3  constructor() {
4    this.logs = [];
5    this.errors = [];
6    this.init();
7  }
8
9  init() {
10    // Capturer les erreurs JavaScript
11    window.addEventListener('error', (event) => {
12      const error = {
13        message: event.message,
14        filename: event.filename,
15        lineno: event.lineno,
16        colno: event.colno,
17        stack: event.error ? event.error.stack : null,
18        timestamp: new Date().toISOString(),
19        userAgent: navigator.userAgent
20      };
21
22      this.errors.push(error);
23      this.sendToRemoteDebugger('error', error);
24    });
25
26    // Capturer les promesses rejetées
27    window.addEventListener('unhandledrejection', (event) => {
28      const error = {
29        reason: event.reason.toString(),
30        stack: event.reason.stack,
31        timestamp: new Date().toISOString(),
32        userAgent: navigator.userAgent
33      };
34
35      this.errors.push(error);
36      this.sendToRemoteDebugger('rejection', error);
37    });
38
39    // Remplacer les méthodes de la console
40    this.interceptConsole();
41  }
42
43  interceptConsole() {
44    const originalLog = console.log;
45    const originalError = console.error;
46    const originalWarn = console.warn;
47
48    console.log = (...args) => {
49      originalLog.apply(console, args);
50      this.sendToRemoteDebugger('log', args);
51    };
52
53    console.error = (...args) => {
54      originalError.apply(console, args);
55      this.sendToRemoteDebugger('error', args);
56    };
57
58    console.warn = (...args) => {
59      originalWarn.apply(console, args);
60      this.sendToRemoteDebugger('warn', args);
61    };
62  }
63
64  sendToRemoteDebugger(type, data) {
65    // Envoyer les données à un service de débogage à distance
66    if (navigator.sendBeacon) {
67      navigator.sendBeacon('/api/debug', JSON.stringify({
68        type,
69        data,
70        timestamp: new Date().toISOString(),
71        url: window.location.href,
72        userAgent: navigator.userAgent
73      }));
74    }
75  }
76
77  getDeviceInfo() {
78    return {
79      screen: {
80        width: screen.width,
81        height: screen.height,
82        availWidth: screen.availWidth,
83        availHeight: screen.availHeight,
84        pixelRatio: window.devicePixelRatio
85      },
86      viewport: {
87        width: window.innerWidth,
88        height: window.innerHeight
89      },
90      browser: navigator.userAgent,
91      language: navigator.language,
92      platform: navigator.platform,
93      touchSupport: 'ontouchstart' in window,
94      orientation: screen.orientation ? screen.orientation.angle : 'unknown'
95    };
96  }
97}
98
99// Initialiser uniquement en développement
100if (process.env.NODE_ENV === 'development') {
101  new RemoteDebugger();
102}

Pour le débogage manuel, tu as aussi des outils intégrés : tu connectes Safari sur Mac à un iPhone, et Chrome à un appareil Android via chrome://inspect.

Bonnes pratiques

1. Tester sur de vrais appareils

Un émulateur ne reproduit ni le toucher, ni les performances, ni les polices système, alors teste aussi sur de vrais appareils :

1// Configuration d'une device farm
2const testDevices = [
3  { name: 'iPhone 13', browser: 'Safari', os: 'iOS 15' },
4  { name: 'Samsung Galaxy S21', browser: 'Chrome', os: 'Android 11' },
5  { name: 'iPad Pro', browser: 'Safari', os: 'iPadOS 15' },
6  { name: 'Desktop', browser: 'Chrome', os: 'Windows 10' },
7  { name: 'Desktop', browser: 'Firefox', os: 'Windows 10' },
8  { name: 'Desktop', browser: 'Safari', os: 'macOS Big Sur' }
9];
10
11// Tests automatisés sur la device farm
12async function runDeviceFarmTests() {
13  for (const device of testDevices) {
14    try {
15      await runTestOnDevice(device);
16      console.log(`Tests passed on ${device.name} - ${device.browser}`);
17    } catch (error) {
18      console.error(`Tests failed on ${device.name} - ${device.browser}:`, error);
19    }
20  }
21}

Choisis la liste des appareils en fonction des données sur tes utilisateurs.

2. Considérations de performance

Les performances varient elles aussi, et ce moniteur collecte les temps de chargement et les métriques Core Web Vitals :

1// Surveillance des performances multi-navigateurs
2class PerformanceMonitor {
3  constructor() {
4    this.metrics = {};
5    this.init();
6  }
7
8  init() {
9    if ('performance' in window) {
10      this.collectNavigationMetrics();
11      this.collectResourceMetrics();
12      this.observeLargestContentfulPaint();
13      this.observeFirstInputDelay();
14    }
15  }
16
17  collectNavigationMetrics() {
18    if (performance.getEntriesByType) {
19      const navigation = performance.getEntriesByType('navigation')[0];
20      if (navigation) {
21        this.metrics.navigation = {
22          domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
23          loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
24          domInteractive: navigation.domInteractive - navigation.fetchStart,
25          firstByte: navigation.responseStart - navigation.requestStart
26        };
27      }
28    }
29  }
30
31  collectResourceMetrics() {
32    if (performance.getEntriesByType) {
33      const resources = performance.getEntriesByType('resource');
34      this.metrics.resources = resources.map(resource => ({
35        name: resource.name,
36        duration: resource.duration,
37        size: resource.transferSize,
38        type: resource.initiatorType
39      }));
40    }
41  }
42
43  observeLargestContentfulPaint() {
44    if ('PerformanceObserver' in window) {
45      try {
46        const observer = new PerformanceObserver((list) => {
47          const entries = list.getEntries();
48          const lastEntry = entries[entries.length - 1];
49          this.metrics.lcp = lastEntry.startTime;
50        });
51        observer.observe({ entryTypes: ['largest-contentful-paint'] });
52      } catch (e) {
53        // Repli pour les navigateurs qui ne prennent pas en charge LCP
54        console.log('LCP not supported');
55      }
56    }
57  }
58
59  observeFirstInputDelay() {
60    if ('PerformanceObserver' in window) {
61      try {
62        const observer = new PerformanceObserver((list) => {
63          const entries = list.getEntries();
64          entries.forEach(entry => {
65            this.metrics.fid = entry.processingStart - entry.startTime;
66          });
67        });
68        observer.observe({ entryTypes: ['first-input'] });
69      } catch (e) {
70        // Repli pour les navigateurs qui ne prennent pas en charge FID
71        console.log('FID not supported');
72      }
73    }
74  }
75
76  report() {
77    return {
78      ...this.metrics,
79      browser: navigator.userAgent,
80      timestamp: new Date().toISOString()
81    };
82  }
83}
84
85// Utilisation
86const monitor = new PerformanceMonitor();
87
88window.addEventListener('load', () => {
89  setTimeout(() => {
90    const report = monitor.report();
91    // Envoyer le rapport aux analytics
92    console.log('Performance Report:', report);
93  }, 1000);
94});

Depuis mars 2024, la métrique INP (Interaction to Next Paint) a remplacé FID dans les Core Web Vitals.

Résumé

Les tests multi-navigateurs impliquent :

  • Un processus systématique garantissant la compatibilité de l'application
  • Une combinaison d'outils pour les tests automatisés et manuels
  • Une stratégie d'amélioration progressive pour une meilleure UX
  • La surveillance des utilisateurs réels sur différentes plateformes
  • Un processus continu nécessitant des mises à jour régulières

La clé du succès est de planifier tôt le support des navigateurs, d'automatiser les tests et de surveiller les performances dans différents environnements. Mon conseil : avant d'utiliser une nouvelle fonctionnalité, vérifie-la sur caniuse.com et teste-la dans les trois moteurs.

Souviens-toi : un bon temple se dresse aussi solidement dans chaque ville, et une bonne page fonctionne dans chaque navigateur.

Code de cette leçon : index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Galerie Scroll Snap</title>
7    <link rel="stylesheet" href="style.css">
8</head>
9<body>
10    <h1>Scroll Snap - Galerie des chambres égyptiennes</h1>
11
12    <section>
13        <h2>Galerie horizontale (x mandatory)</h2>
14        <div class="gallery-horizontal">
15            <div class="gallery-item" style="background: linear-gradient(135deg, #d4af37, #8b6914)">
16                <h3>Chambre du Pharaon</h3>
17                <p>Trésors dorés et hiéroglyphes</p>
18            </div>
19            <div class="gallery-item" style="background: linear-gradient(135deg, #1e3a5f, #30d5c8)">
20                <h3>Salle du Trône</h3>
21                <p>Lapis-lazuli et turquoise</p>
22            </div>
23            <div class="gallery-item" style="background: linear-gradient(135deg, #b87333, #d4a574)">
24                <h3>Trésor</h3>
25                <p>Vases rituels en cuivre</p>
26            </div>
27            <div class="gallery-item" style="background: linear-gradient(135deg, #8b6914, #c9a66b)">
28                <h3>Bibliothèque des Papyrus</h3>
29                <p>Rouleaux de la sagesse de Thot</p>
30            </div>
31            <div class="gallery-item" style="background: linear-gradient(135deg, #1a1a2e, #3d2914)">
32                <h3>Crypte</h3>
33                <p>Secrets de l'Antiquité</p>
34            </div>
35        </div>
36    </section>
37
38    <section>
39        <h2>Galerie verticale (y mandatory)</h2>
40        <div class="gallery-vertical">
41            <div class="v-item" style="background: #d4af37">Niveau 1 - Entrée</div>
42            <div class="v-item" style="background: #8b6914">Niveau 2 - Couloir</div>
43            <div class="v-item" style="background: #1e3a5f; color: #d4af37">Niveau 3 - Chambre</div>
44            <div class="v-item" style="background: #1a1a2e; color: #d4af37">Niveau 4 - Sarcophage</div>
45        </div>
46    </section>
47</body>
48</html>

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quel moteur de rendu utilise le navigateur Safari ?

  2. 2. En quoi consiste la stratégie d'amélioration progressive ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Mets dans l'ordre les déclarations transform avec préfixes de sorte que la version standard soit en dernier (les solutions de repli avant la version standard) :

Articles utiles