Cross-browser testing is a critical process in front-end development that ensures web applications work correctly across all supported browsers and on various devices. Differences in web standards implementation between browsers can lead to inconsistencies in appearance and functionality.
The modern browser landscape consists of several major engines:
1// Browser usage statistics (2024)
2const browserStats = {
3 chrome: "65%", // Blink engine
4 safari: "19%", // WebKit engine
5 edge: "5%", // Blink engine (Chromium-based)
6 firefox: "3%", // Gecko engine
7 opera: "2%", // Blink engine
8 others: "6%"
9};
10
11// Mobile browsers
12const mobileBrowsers = {
13 chrome: "65%",
14 safari: "25%",
15 samsung: "4%",
16 opera: "2%",
17 firefox: "1%",
18 others: "3%"
19};1/* Vendor prefixes for newer features */
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/* Grid implementation differences */
10.grid-container {
11 display: grid;
12 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
13 gap: 20px; /* IE doesn't support gap in Grid */
14}
15
16/* Fallback for IE */
17@supports not (gap: 20px) {
18 .grid-container > * {
19 margin: 10px;
20 }
21}1// Fetch API - not supported in IE
2function fetchDataCompatible(url) {
3 if (window.fetch) {
4 // Modern browsers
5 return fetch(url).then(response => response.json());
6 } else {
7 // Fallback for older browsers
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 - requires polyfill in 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 // Fallback - scroll listener
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}1// BrowserStack configuration for automated tests
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};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"1// playwright.config.js - cross-browser testing configuration
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// Example cross-browser test
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 // Check if grid is displayed correctly
40 const gridContainer = page.locator('.grid-container');
41 await expect(gridContainer).toBeVisible();
42
43 // Check number of columns
44 const gridItems = page.locator('.grid-item');
45 const count = await gridItems.count();
46 expect(count).toBeGreaterThan(0);
47
48 // Check responsiveness
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 // Test Fetch API or XHR fallback
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});1# Selenium Grid setup for local testing
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=44441/* Basic styles for all browsers */
2.card {
3 border: 1px solid #ddd;
4 padding: 16px;
5 margin-bottom: 16px;
6 background: white;
7}
8
9/* Enhancements for modern browsers */
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/* Further enhancements */
23@supports (backdrop-filter: blur(10px)) {
24 .card--glass {
25 background: rgba(255, 255, 255, 0.8);
26 backdrop-filter: blur(10px);
27 }
28}1// JavaScript Progressive Enhancement
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 with fallback
20 if (this.supportsIntersectionObserver()) {
21 this.initLazyLoadingModern();
22 } else {
23 this.initLazyLoadingFallback();
24 }
25
26 // WebP support detection
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// Initialize on page load
73document.addEventListener('DOMContentLoaded', () => {
74 FeatureDetector.init();
75});1/* Modern styles with fallbacks */
2.hero-section {
3 /* Fallback for old browsers */
4 background: linear-gradient(to right, #667eea, #764ba2);
5
6 /* Modern styles */
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/* Fallback for 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/* CSS Variables with fallback */
28.theme-primary {
29 color: #007bff; /* Fallback */
30 color: var(--primary-color, #007bff);
31}1// Polyfill for 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 for 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// Intersection Observer Polyfill
33(function() {
34 if ('IntersectionObserver' in window) return;
35
36 // Basic polyfill implementation
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})();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: ['chrome', 'firefox', 'safari']
17
18 steps:
19 - uses: actions/checkout@v3
20
21 - name: Setup Node.js
22 uses: actions/setup-node@v3
23 with:
24 node-version: '18'
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@v3
38 if: failure()
39 with:
40 name: test-results-${{ matrix.browser }}
41 path: test-results/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 // Global setup for cross-browser tests
18 globalSetup: '<rootDir>/test/global-setup.js',
19 globalTeardown: '<rootDir>/test/global-teardown.js'
20};
21
22// setupTests.js - polyfills for tests
23import 'intersection-observer';
24import 'whatwg-fetch';
25
26// Mock for matchMedia (not supported in 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});1// Universal debugging
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// Browser detection for debugging
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// Debugging across different browsers
53function debugCompatibility() {
54 Logger.log('Browser Info:', BrowserDetector.getInfo());
55
56 // Test CSS features
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 // Test JavaScript features
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// Call debugging in development mode
83if (process.env.NODE_ENV === 'development') {
84 debugCompatibility();
85}1// Remote debugging setup for mobile devices
2class RemoteDebugger {
3 constructor() {
4 this.logs = [];
5 this.errors = [];
6 this.init();
7 }
8
9 init() {
10 // Capturing JavaScript errors
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 // Capturing rejected promises
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 // Override console methods
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 // Send data to remote debugging service
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// Initialize only in development
100if (process.env.NODE_ENV === 'development') {
101 new RemoteDebugger();
102}1// Device farm setup
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// Automated testing on 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}1// Cross-browser performance monitoring
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 // Fallback for browsers that don't support 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 // Fallback for browsers that don't support 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// Usage
86const monitor = new PerformanceMonitor();
87
88window.addEventListener('load', () => {
89 setTimeout(() => {
90 const report = monitor.report();
91 // Send report to analytics
92 console.log('Performance Report:', report);
93 }, 1000);
94});Cross-browser testing involves:
The key to success is early planning for browser support, using modern tools for test automation, and systematic performance monitoring across different environments.