Kurs HTML i CSS · Moduł 9: Nowoczesny CSS

Testowanie cross-browser

15 min czytania ·
W tej lekcji7

Twoja strona wygląda idealnie w Chrome na laptopie, a na iPhonie klienta siatka się rozpada, bo każda przeglądarka to inny zespół kamieniarzy czytający ten sam plan po swojemu. Testowanie cross-browser to proces, który zapewnia, że aplikacje webowe działają poprawnie we wszystkich obsługiwanych przeglądarkach i na różnych urządzeniach. Różnice w implementacji standardów mogą prowadzić do niespójności w wyglądzie i funkcjonalności.

Podstawy testowania cross-browser

Główne przeglądarki do testowania

Współczesne przeglądarki opierają się na trzech silnikach: Blink (Chrome, Edge, Opera, Samsung Internet), WebKit (Safari) i Gecko (Firefox):

// Statystyki użycia przeglądarek (2024)
const browserStats = {
  chrome: "65%",      // Blink engine
  safari: "19%",      // WebKit engine
  edge: "5%",         // Blink engine (Chromium-based)
  firefox: "3%",      // Gecko engine
  opera: "2%",        // Blink engine
  others: "6%"
};

// Mobilne przeglądarki
const mobileBrowsers = {
  chrome: "65%",
  safari: "25%",
  samsung: "4%",
  opera: "2%",
  firefox: "1%",
  others: "3%"
};

Liczby w przykładzie są orientacyjne, a aktualne udziały znajdziesz w serwisach takich jak StatCounter. Na iPhonie przeglądarki przez lata musiały korzystać z WebKit, więc testując silnik Safari, sprawdzasz w dużej mierze także Chrome na iOS.

Kluczowe różnice między przeglądarkami

1. Wsparcie CSS

Nowe właściwości trafiały kiedyś do przeglądarek z prefiksami producentów. Wersje z prefiksami piszemy najpierw, a standardową na końcu:

/* Prefiks vendorów dla nowszych funkcji */
.element {
  -webkit-transform: rotate(45deg);  /* Safari, Chrome */
  -moz-transform: rotate(45deg);     /* Firefox */
  -ms-transform: rotate(45deg);      /* Internet Explorer */
  transform: rotate(45deg);          /* Standard */
}

/* Różnice w implementacji Grid */
.grid-container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px; /* IE nie wspiera gap w Grid */
}

/* Fallback dla IE */
@supports not (gap: 20px) {
  .grid-container > * {
    margin: 10px;
  }
}

Kolejność ma znaczenie: przeglądarka, która zna standard, użyje ostatniej deklaracji. Dziś transform nie potrzebuje prefiksów, a potrzebne prefiksy dopisze Autoprefixer.

2. JavaScript API differences

W JavaScripcie przed użyciem nowego API sprawdzamy, czy ono istnieje (feature detection):

// Fetch API - nie wspierane w IE
function fetchDataCompatible(url) {
  if (window.fetch) {
    // Nowoczesne przeglądarki
    return fetch(url).then(response => response.json());
  } else {
    // Fallback dla starszych przeglądarek
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.onload = () => {
        if (xhr.status === 200) {
          resolve(JSON.parse(xhr.responseText));
        } else {
          reject(new Error('Request failed'));
        }
      };
      xhr.onerror = () => reject(new Error('Network error'));
      xhr.send();
    });
  }
}

// Intersection Observer - wymaga polyfill w IE
function observeElementsCompatible(elements, callback) {
  if ('IntersectionObserver' in window) {
    const observer = new IntersectionObserver(callback);
    elements.forEach(el => observer.observe(el));
  } else {
    // Fallback - scroll listener
    function checkVisibility() {
      elements.forEach(element => {
        const rect = element.getBoundingClientRect();
        const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
        if (isVisible) callback([{ target: element, isIntersecting: true }]);
      });
    }

    window.addEventListener('scroll', checkVisibility);
    window.addEventListener('resize', checkVisibility);
    checkVisibility();
  }
}

Gdy fetch lub IntersectionObserver nie istnieje, kod przechodzi na starsze rozwiązanie. Sprawdzamy funkcję, a nie nazwę przeglądarki.

Narzędzia do testowania cross-browser

1. BrowserStack

Chmurowe farmy urządzeń udostępniają prawdziwe przeglądarki i systemy do testów automatycznych:

// Konfiguracja BrowserStack dla automatycznych testów
const browserStackConfig = {
  user: process.env.BROWSERSTACK_USERNAME,
  key: process.env.BROWSERSTACK_ACCESS_KEY,

  capabilities: [
    {
      browserName: 'Chrome',
      browserVersion: 'latest',
      os: 'Windows',
      osVersion: '10'
    },
    {
      browserName: 'Safari',
      browserVersion: 'latest',
      os: 'OS X',
      osVersion: 'Big Sur'
    },
    {
      browserName: 'Firefox',
      browserVersion: 'latest',
      os: 'Windows',
      osVersion: '10'
    }
  ]
};

Dane logowania pochodzą ze zmiennych środowiskowych, nigdy z kodu.

2. Sauce Labs

Sauce Labs działa podobnie, a konfiguracja w YAML wskazuje zestawy testów Cypress i przeglądarki:

# .sauce/config.yml
apiVersion: v1alpha
kind: cypress
sauce:
  region: eu-central-1

suites:
  - name: "Cross Browser Tests"
    browser: "chrome"
    browserVersion: "latest"
    platformName: "Windows 10"

  - name: "Safari Tests"
    browser: "safari"
    browserVersion: "latest"
    platformName: "macOS 11.00"

cypress:
  configFile: "cypress.config.js"
  version: "12.0.0"

Każdy zestaw ruszy w chmurze na swojej przeglądarce.

3. Playwright

Playwright uruchamia jeden zestaw testów w trzech silnikach: Chromium, Firefox i WebKit, a do tego emuluje telefony:

// playwright.config.js - konfiguracja do testowania cross-browser
const { defineConfig, devices } = require('@playwright/test');

module.exports = defineConfig({
  testDir: './tests',

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],
});

// Przykład testu cross-browser
// tests/cross-browser.spec.js
const { test, expect } = require('@playwright/test');

test.describe('Cross-browser compatibility', () => {
  test('CSS Grid layout works correctly', async ({ page }) => {
    await page.goto('/grid-layout');

    // Sprawdź czy grid jest poprawnie wyświetlany
    const gridContainer = page.locator('.grid-container');
    await expect(gridContainer).toBeVisible();

    // Sprawdź liczbę kolumn
    const gridItems = page.locator('.grid-item');
    const count = await gridItems.count();
    expect(count).toBeGreaterThan(0);

    // Sprawdź responsywność
    await page.setViewportSize({ width: 768, height: 1024 });
    await expect(gridContainer).toBeVisible();
  });

  test('JavaScript features work across browsers', async ({ page }) => {
    await page.goto('/js-features');

    // Test Fetch API lub XHR fallback
    const result = await page.evaluate(async () => {
      const response = await fetchDataCompatible('/api/test');
      return response.status === 'ok';
    });

    expect(result).toBe(true);
  });
});

Projekty nazywają się chromium, firefox i webkit, a polecenie npx playwright test --project=webkit uruchomi tylko jeden z nich. Na start polecam Ci właśnie Playwrighta bo działa lokalnie i za darmo.

4. Lokalne środowiska testowe

Własną farmę postawisz w Dockerze. Selenium Grid składa się z huba i węzłów z przeglądarkami:

# Selenium Grid setup dla lokalnego testowania
# docker-compose.yml
version: '3'
services:
  selenium-hub:
    image: selenium/hub:4.0.0
    container_name: selenium-hub
    ports:
      - "4444:4444"

  chrome:
    image: selenium/node-chrome:4.0.0
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - HUB_HOST=selenium-hub
      - HUB_PORT=4444

  firefox:
    image: selenium/node-firefox:4.0.0
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - HUB_HOST=selenium-hub
      - HUB_PORT=4444

Hub rozdziela testy między węzły Chrome i Firefox. Safari w tych obrazach nie ma, do niego potrzebujesz Maca lub chmury.

Strategie kompatybilności

1. Progressive Enhancement

Progressive enhancement polega na tym, że zaczynamy od działającej bazy dla wszystkich, a ulepszenia dodajemy przeglądarkom, które je obsługują:

/* Podstawowe style dla wszystkich przeglądarek */
.card {
  border: 1px solid #ddd;
  padding: 16px;
  margin-bottom: 16px;
  background: white;
}

/* Ulepszenia dla nowoczesnych przeglądarek */
@supports (display: grid) {
  .card-container {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 20px;
  }

  .card {
    margin-bottom: 0;
  }
}

/* Dalsze ulepszenia */
@supports (backdrop-filter: blur(10px)) {
  .card--glass {
    background: rgba(255, 255, 255, 0.8);
    backdrop-filter: blur(10px);
  }
}

Każdy dostaje czytelne karty, przeglądarki z Gridem dostają siatkę, a te z backdrop-filter efekt szkła. Tę samą strategię stosujesz w JavaScripcie:

// JavaScript Progressive Enhancement
class FeatureDetector {
  static supportsIntersectionObserver() {
    return 'IntersectionObserver' in window;
  }

  static supportsWebP() {
    const canvas = document.createElement('canvas');
    canvas.width = 1;
    canvas.height = 1;
    return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
  }

  static supportsCustomProperties() {
    return CSS.supports && CSS.supports('(--foo: red)');
  }

  static init() {
    // Lazy loading z fallbackiem
    if (this.supportsIntersectionObserver()) {
      this.initLazyLoadingModern();
    } else {
      this.initLazyLoadingFallback();
    }

    // WebP support detection
    if (this.supportsWebP()) {
      document.documentElement.classList.add('webp');
    } else {
      document.documentElement.classList.add('no-webp');
    }
  }

  static initLazyLoadingModern() {
    const images = document.querySelectorAll('img[data-src]');
    const imageObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          img.classList.remove('lazy');
          imageObserver.unobserve(img);
        }
      });
    });

    images.forEach(img => imageObserver.observe(img));
  }

  static initLazyLoadingFallback() {
    const images = document.querySelectorAll('img[data-src]');

    function loadImagesInViewport() {
      images.forEach(img => {
        if (img.dataset.src) {
          const rect = img.getBoundingClientRect();
          if (rect.top < window.innerHeight + 100) {
            img.src = img.dataset.src;
            delete img.dataset.src;
            img.classList.remove('lazy');
          }
        }
      });
    }

    window.addEventListener('scroll', loadImagesInViewport);
    window.addEventListener('resize', loadImagesInViewport);
    loadImagesInViewport();
  }
}

// Inicjalizacja przy ładowaniu strony
document.addEventListener('DOMContentLoaded', () => {
  FeatureDetector.init();
});

Klasy webp i no-webp na elemencie html pozwalają dobrać obrazy w CSS. Do leniwego ładowania obrazów wystarczy dziś atrybut loading="lazy".

2. Graceful Degradation

Graceful degradation idzie w drugą stronę: projektujesz dla nowoczesnych przeglądarek i pilnujesz, by starsze wciąż dawały akceptowalny wynik:

/* Nowoczesne style z fallbackami */
.hero-section {
  /* Fallback dla starych przeglądarek */
  background: linear-gradient(to right, #667eea, #764ba2);

  /* Nowoczesne style */
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Fallback dla flexbox */
.hero-section {
  text-align: center;
  padding-top: 20vh;
}

@supports (display: flex) {
  .hero-section {
    padding-top: 0;
    text-align: left;
  }
}

/* CSS Variables z fallbackiem */
.theme-primary {
  color: #007bff; /* Fallback */
  color: var(--primary-color, #007bff);
}

Działa tu prosta zasada CSS: przeglądarka, która nie zrozumie drugiej deklaracji, zostanie przy pierwszej. Dlatego zwykły kolor stoi przed wersją z var().

3. Polyfills i shims

Polyfill to kod, który dostarcza brakującą funkcjonalność w starszych przeglądarkach, które nie wspierają jej natywnie:

// Polyfill dla Array.includes (IE)
if (!Array.prototype.includes) {
  Array.prototype.includes = function(searchElement, fromIndex) {
    return this.indexOf(searchElement, fromIndex) !== -1;
  };
}

// Polyfill dla Object.assign
if (!Object.assign) {
  Object.assign = function(target, ...sources) {
    if (target == null) {
      throw new TypeError('Cannot convert undefined or null to object');
    }

    const to = Object(target);

    for (let index = 0; index < sources.length; index++) {
      const nextSource = sources[index];
      if (nextSource != null) {
        for (const nextKey in nextSource) {
          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
    }

    return to;
  };
}

// Intersection Observer Polyfill
(function() {
  if ('IntersectionObserver' in window) return;

  // Implementacja podstawowego polyfill
  window.IntersectionObserver = class {
    constructor(callback, options = {}) {
      this.callback = callback;
      this.options = options;
      this.observedElements = new Set();
      this.checkIntersections = this.checkIntersections.bind(this);

      this.startListening();
    }

    observe(element) {
      this.observedElements.add(element);
      this.checkIntersections();
    }

    unobserve(element) {
      this.observedElements.delete(element);
    }

    disconnect() {
      this.observedElements.clear();
      this.stopListening();
    }

    startListening() {
      window.addEventListener('scroll', this.checkIntersections);
      window.addEventListener('resize', this.checkIntersections);
    }

    stopListening() {
      window.removeEventListener('scroll', this.checkIntersections);
      window.removeEventListener('resize', this.checkIntersections);
    }

    checkIntersections() {
      const entries = [];

      this.observedElements.forEach(element => {
        const rect = element.getBoundingClientRect();
        const isIntersecting = (
          rect.top < window.innerHeight &&
          rect.bottom > 0 &&
          rect.left < window.innerWidth &&
          rect.right > 0
        );

        entries.push({
          target: element,
          isIntersecting,
          boundingClientRect: rect,
          intersectionRatio: isIntersecting ? 1 : 0
        });
      });

      if (entries.length > 0) {
        this.callback(entries);
      }
    }
  };
})();

Każdy polyfill najpierw sprawdza, czy funkcja już istnieje, i jeśli tak, niczego nie nadpisuje. Internet Explorer odszedł w 2022 roku, więc wiele takich łatek jest dziś zbędnych.

Automatyzacja testów cross-browser

1. GitHub Actions workflow

Testy Playwrighta uruchomisz automatycznie przy każdym pushu, a macierz tworzy zadanie dla każdego silnika:

# .github/workflows/cross-browser-tests.yml
name: Cross-Browser Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        browser: ['chromium', 'firefox', 'webkit']

    steps:
    - uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '22'
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Install Playwright
      run: npx playwright install --with-deps

    - name: Run tests
      run: npx playwright test --project=${{ matrix.browser }}

    - name: Upload test results
      uses: actions/upload-artifact@v4
      if: failure()
      with:
        name: test-results-${{ matrix.browser }}
        path: test-results/

Nazwy w macierzy odpowiadają projektom Playwrighta, więc powstają trzy równoległe przebiegi.

2. Jest configuration dla różnych środowisk

Testy jednostkowe w Jest działają w jsdom, czyli symulacji przeglądarki w Node.js, bez prawdziwego silnika renderującego:

// jest.config.js
module.exports = {
  projects: [
    {
      displayName: 'jsdom',
      testEnvironment: 'jsdom',
      testMatch: ['<rootDir>/src/**/*.test.js'],
      setupFilesAfterEnv: ['<rootDir>/src/setupTests.js']
    },
    {
      displayName: 'node',
      testEnvironment: 'node',
      testMatch: ['<rootDir>/server/**/*.test.js']
    }
  ],

  // Global setup dla cross-browser testów
  globalSetup: '<rootDir>/test/global-setup.js',
  globalTeardown: '<rootDir>/test/global-teardown.js'
};

// setupTests.js - polyfills dla testów
import 'intersection-observer';
import 'whatwg-fetch';

// Mock dla matchMedia (nie wspierane w jsdom)
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

jsdom nie liczy układu strony i nie zna matchMedia, dlatego ją mockujemy, a wygląd CSS sprawdzasz w prawdziwych przeglądarkach.

Debugowanie problemów cross-browser

1. DevTools differences

Przy debugowaniu przydaje się bezpieczny logger i tabela funkcji wspieranych przez przeglądarkę:

// Uniwersalne debugowanie
const Logger = {
  log: (...args) => {
    if (console && console.log) {
      console.log(...args);
    }
  },

  warn: (...args) => {
    if (console && console.warn) {
      console.warn(...args);
    } else if (console && console.log) {
      console.log('WARNING:', ...args);
    }
  },

  error: (...args) => {
    if (console && console.error) {
      console.error(...args);
    } else if (console && console.log) {
      console.log('ERROR:', ...args);
    }
  },

  table: (data) => {
    if (console && console.table) {
      console.table(data);
    } else {
      Logger.log('TABLE:', JSON.stringify(data, null, 2));
    }
  }
};

// Browser detection dla debugowania
const BrowserDetector = {
  isIE: () => navigator.userAgent.indexOf('MSIE') !== -1 || navigator.userAgent.indexOf('Trident') !== -1,
  isEdge: () => navigator.userAgent.indexOf('Edge') !== -1,
  isChrome: () => navigator.userAgent.indexOf('Chrome') !== -1 && navigator.userAgent.indexOf('Edge') === -1,
  isFirefox: () => navigator.userAgent.indexOf('Firefox') !== -1,
  isSafari: () => navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1,

  getInfo: () => ({
    userAgent: navigator.userAgent,
    vendor: navigator.vendor,
    platform: navigator.platform,
    language: navigator.language,
    cookieEnabled: navigator.cookieEnabled,
    onLine: navigator.onLine
  })
};

// Debugging w różnych przeglądarkach
function debugCompatibility() {
  Logger.log('Browser Info:', BrowserDetector.getInfo());

  // Test CSS features
  const cssFeatures = {
    flexbox: CSS.supports('display', 'flex'),
    grid: CSS.supports('display', 'grid'),
    customProperties: CSS.supports('--foo', 'red'),
    backdropFilter: CSS.supports('backdrop-filter', 'blur(10px)'),
    objectFit: CSS.supports('object-fit', 'cover')
  };

  Logger.table(cssFeatures);

  // Test JavaScript features
  const jsFeatures = {
    fetch: typeof fetch !== 'undefined',
    intersectionObserver: 'IntersectionObserver' in window,
    customElements: 'customElements' in window,
    serviceWorker: 'serviceWorker' in navigator,
    webp: (() => {
      const canvas = document.createElement('canvas');
      return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
    })()
  };

  Logger.table(jsFeatures);
}

// Wywołaj debugging w development mode
if (process.env.NODE_ENV === 'development') {
  debugCompatibility();
}

CSS.supports() to najpewniejszy test. Wykrywanie po userAgent jest kruche: nowy Edge przedstawia się jako "Edg", więc isEdge go nie rozpozna.

2. Remote debugging

Błędy z telefonów trudno zobaczyć, dlatego ten skrypt przechwytuje je i wysyła na serwer:

// Remote debugging setup dla urządzeń mobilnych
class RemoteDebugger {
  constructor() {
    this.logs = [];
    this.errors = [];
    this.init();
  }

  init() {
    // Przechwytywanie błędów JavaScript
    window.addEventListener('error', (event) => {
      const error = {
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        stack: event.error ? event.error.stack : null,
        timestamp: new Date().toISOString(),
        userAgent: navigator.userAgent
      };

      this.errors.push(error);
      this.sendToRemoteDebugger('error', error);
    });

    // Przechwytywanie rejected promises
    window.addEventListener('unhandledrejection', (event) => {
      const error = {
        reason: event.reason.toString(),
        stack: event.reason.stack,
        timestamp: new Date().toISOString(),
        userAgent: navigator.userAgent
      };

      this.errors.push(error);
      this.sendToRemoteDebugger('rejection', error);
    });

    // Override console methods
    this.interceptConsole();
  }

  interceptConsole() {
    const originalLog = console.log;
    const originalError = console.error;
    const originalWarn = console.warn;

    console.log = (...args) => {
      originalLog.apply(console, args);
      this.sendToRemoteDebugger('log', args);
    };

    console.error = (...args) => {
      originalError.apply(console, args);
      this.sendToRemoteDebugger('error', args);
    };

    console.warn = (...args) => {
      originalWarn.apply(console, args);
      this.sendToRemoteDebugger('warn', args);
    };
  }

  sendToRemoteDebugger(type, data) {
    // Wyślij dane do remote debugging service
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/debug', JSON.stringify({
        type,
        data,
        timestamp: new Date().toISOString(),
        url: window.location.href,
        userAgent: navigator.userAgent
      }));
    }
  }

  getDeviceInfo() {
    return {
      screen: {
        width: screen.width,
        height: screen.height,
        availWidth: screen.availWidth,
        availHeight: screen.availHeight,
        pixelRatio: window.devicePixelRatio
      },
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight
      },
      browser: navigator.userAgent,
      language: navigator.language,
      platform: navigator.platform,
      touchSupport: 'ontouchstart' in window,
      orientation: screen.orientation ? screen.orientation.angle : 'unknown'
    };
  }
}

// Inicjalizacja tylko w development
if (process.env.NODE_ENV === 'development') {
  new RemoteDebugger();
}

Do ręcznego debugowania masz też narzędzia wbudowane: Safari na Macu podłączysz do iPhone'a, a Chrome do Androida przez chrome://inspect.

Best practices

1. Testowanie na prawdziwych urządzeniach

Emulator nie odda dotyku, wydajności ani czcionek systemowych, dlatego testuj też na prawdziwych urządzeniach:

// Device farm setup
const testDevices = [
  { name: 'iPhone 13', browser: 'Safari', os: 'iOS 15' },
  { name: 'Samsung Galaxy S21', browser: 'Chrome', os: 'Android 11' },
  { name: 'iPad Pro', browser: 'Safari', os: 'iPadOS 15' },
  { name: 'Desktop', browser: 'Chrome', os: 'Windows 10' },
  { name: 'Desktop', browser: 'Firefox', os: 'Windows 10' },
  { name: 'Desktop', browser: 'Safari', os: 'macOS Big Sur' }
];

// Automatyczne testowanie na device farm
async function runDeviceFarmTests() {
  for (const device of testDevices) {
    try {
      await runTestOnDevice(device);
      console.log(`Tests passed on ${device.name} - ${device.browser}`);
    } catch (error) {
      console.error(`Tests failed on ${device.name} - ${device.browser}:`, error);
    }
  }
}

Listę urządzeń dobieraj według danych o swoich użytkownikach.

2. Performance considerations

Wydajność też bywa różna, a ten monitor zbiera czasy ładowania i metryki Core Web Vitals:

// Monitoring wydajności cross-browser
class PerformanceMonitor {
  constructor() {
    this.metrics = {};
    this.init();
  }

  init() {
    if ('performance' in window) {
      this.collectNavigationMetrics();
      this.collectResourceMetrics();
      this.observeLargestContentfulPaint();
      this.observeFirstInputDelay();
    }
  }

  collectNavigationMetrics() {
    if (performance.getEntriesByType) {
      const navigation = performance.getEntriesByType('navigation')[0];
      if (navigation) {
        this.metrics.navigation = {
          domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
          loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
          domInteractive: navigation.domInteractive - navigation.fetchStart,
          firstByte: navigation.responseStart - navigation.requestStart
        };
      }
    }
  }

  collectResourceMetrics() {
    if (performance.getEntriesByType) {
      const resources = performance.getEntriesByType('resource');
      this.metrics.resources = resources.map(resource => ({
        name: resource.name,
        duration: resource.duration,
        size: resource.transferSize,
        type: resource.initiatorType
      }));
    }
  }

  observeLargestContentfulPaint() {
    if ('PerformanceObserver' in window) {
      try {
        const observer = new PerformanceObserver((list) => {
          const entries = list.getEntries();
          const lastEntry = entries[entries.length - 1];
          this.metrics.lcp = lastEntry.startTime;
        });
        observer.observe({ entryTypes: ['largest-contentful-paint'] });
      } catch (e) {
        // Fallback for browsers that don't support LCP
        console.log('LCP not supported');
      }
    }
  }

  observeFirstInputDelay() {
    if ('PerformanceObserver' in window) {
      try {
        const observer = new PerformanceObserver((list) => {
          const entries = list.getEntries();
          entries.forEach(entry => {
            this.metrics.fid = entry.processingStart - entry.startTime;
          });
        });
        observer.observe({ entryTypes: ['first-input'] });
      } catch (e) {
        // Fallback for browsers that don't support FID
        console.log('FID not supported');
      }
    }
  }

  report() {
    return {
      ...this.metrics,
      browser: navigator.userAgent,
      timestamp: new Date().toISOString()
    };
  }
}

// Użycie
const monitor = new PerformanceMonitor();

window.addEventListener('load', () => {
  setTimeout(() => {
    const report = monitor.report();
    // Wyślij raport do analytics
    console.log('Performance Report:', report);
  }, 1000);
});

Od marca 2024 roku w Core Web Vitals miejsce FID zajęła metryka INP (Interaction to Next Paint).

Podsumowanie

Testowanie cross-browser to:

  • Systematyczny proces zapewniający kompatybilność aplikacji
  • Kombinacja narzędzi automatycznych i manualnych testów
  • Strategia progressive enhancement dla lepszego UX
  • Monitoring rzeczywistych użytkowników na różnych platformach
  • Ciągły proces wymagający regularnych aktualizacji

Kluczem do sukcesu jest wczesne planowanie wsparcia przeglądarek, automatyzacja testów i monitorowanie wydajności w różnych środowiskach. Moja rada: zanim użyjesz nowej funkcji, sprawdź ją na caniuse.com i przetestuj w trzech silnikach.

Pamiętaj: dobra świątynia stoi tak samo pewnie w każdym mieście, a dobra strona działa w każdej przeglądarce.

Kod do tej lekcji: index.html
<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Scroll Snap Gallery</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>Scroll Snap - Galeria Egipskich Komnat</h1>

    <section>
        <h2>Galeria pozioma (x mandatory)</h2>
        <div class="gallery-horizontal">
            <div class="gallery-item" style="background: linear-gradient(135deg, #d4af37, #8b6914)">
                <h3>Komnata Faraona</h3>
                <p>Zlote skarby i hieroglify</p>
            </div>
            <div class="gallery-item" style="background: linear-gradient(135deg, #1e3a5f, #30d5c8)">
                <h3>Sala Tronu</h3>
                <p>Lapis lazuli i turkus</p>
            </div>
            <div class="gallery-item" style="background: linear-gradient(135deg, #b87333, #d4a574)">
                <h3>Skarbiec</h3>
                <p>Miedziane naczynia rytualne</p>
            </div>
            <div class="gallery-item" style="background: linear-gradient(135deg, #8b6914, #c9a66b)">
                <h3>Biblioteka Papirusow</h3>
                <p>Zwoje madrosci Totha</p>
            </div>
            <div class="gallery-item" style="background: linear-gradient(135deg, #1a1a2e, #3d2914)">
                <h3>Krypta</h3>
                <p>Tajemnice starozytnosci</p>
            </div>
        </div>
    </section>

    <section>
        <h2>Galeria pionowa (y mandatory)</h2>
        <div class="gallery-vertical">
            <div class="v-item" style="background: #d4af37">Poziom 1 - Wejscie</div>
            <div class="v-item" style="background: #8b6914">Poziom 2 - Korytarz</div>
            <div class="v-item" style="background: #1e3a5f; color: #d4af37">Poziom 3 - Komnata</div>
            <div class="v-item" style="background: #1a1a2e; color: #d4af37">Poziom 4 - Sarkofag</div>
        </div>
    </section>
</body>
</html>

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Którego silnika renderującego używa przeglądarka Safari?

  2. 2. Na czym polega strategia progressive enhancement?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Układanie w pionie

    Ułóż deklaracje transform z prefiksami tak, aby standardowa wersja była na końcu (fallback przed standardem):

Przydatne artykuły