Testowanie cross-browser to krytyczny proces w developmencie front-end, 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 webowych między przeglądarkami mogą prowadzić do niespójności w wyglądzie i funkcjonalności.
Współczesny landscape przeglądarek składa się z kilku głównych silników:
1// Statystyki użycia przeglądarek (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// Mobilne przeglądarki
12const mobileBrowsers = {
13 chrome: "65%",
14 safari: "25%",
15 samsung: "4%",
16 opera: "2%",
17 firefox: "1%",
18 others: "3%"
19};1/* Prefiks vendorów dla nowszych funkcji */
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/* Różnice w implementacji Grid */
10.grid-container {
11 display: grid;
12 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
13 gap: 20px; /* IE nie wspiera gap w Grid */
14}
15
16/* Fallback dla IE */
17@supports not (gap: 20px) {
18 .grid-container > * {
19 margin: 10px;
20 }
21}1// Fetch API - nie wspierane w IE
2function fetchDataCompatible(url) {
3 if (window.fetch) {
4 // Nowoczesne przeglądarki
5 return fetch(url).then(response => response.json());
6 } else {
7 // Fallback dla starszych przeglądarek
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 - wymaga polyfill w 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// Konfiguracja BrowserStack dla automatycznych testów
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 - konfiguracja do testowania cross-browser
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// Przykład testu cross-browser
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 // Sprawdź czy grid jest poprawnie wyświetlany
40 const gridContainer = page.locator('.grid-container');
41 await expect(gridContainer).toBeVisible();
42
43 // Sprawdź liczbę kolumn
44 const gridItems = page.locator('.grid-item');
45 const count = await gridItems.count();
46 expect(count).toBeGreaterThan(0);
47
48 // Sprawdź responsywność
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 lub 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 dla lokalnego testowania
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/* Podstawowe style dla wszystkich przeglądarek */
2.card {
3 border: 1px solid #ddd;
4 padding: 16px;
5 margin-bottom: 16px;
6 background: white;
7}
8
9/* Ulepszenia dla nowoczesnych przeglądarek */
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/* Dalsze ulepszenia */
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 z fallbackiem
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// Inicjalizacja przy ładowaniu strony
73document.addEventListener('DOMContentLoaded', () => {
74 FeatureDetector.init();
75});1/* Nowoczesne style z fallbackami */
2.hero-section {
3 /* Fallback dla starych przeglądarek */
4 background: linear-gradient(to right, #667eea, #764ba2);
5
6 /* Nowoczesne style */
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 dla 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 z fallbackiem */
28.theme-primary {
29 color: #007bff; /* Fallback */
30 color: var(--primary-color, #007bff);
31}1// Polyfill dla 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 dla 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 // Implementacja podstawowego 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})();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 dla cross-browser testów
18 globalSetup: '<rootDir>/test/global-setup.js',
19 globalTeardown: '<rootDir>/test/global-teardown.js'
20};
21
22// setupTests.js - polyfills dla testów
23import 'intersection-observer';
24import 'whatwg-fetch';
25
26// Mock dla matchMedia (nie wspierane w 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// Uniwersalne debugowanie
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 dla debugowania
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 w różnych przeglądarkach
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// Wywołaj debugging w development mode
83if (process.env.NODE_ENV === 'development') {
84 debugCompatibility();
85}1// Remote debugging setup dla urządzeń mobilnych
2class RemoteDebugger {
3 constructor() {
4 this.logs = [];
5 this.errors = [];
6 this.init();
7 }
8
9 init() {
10 // Przechwytywanie błędów 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 // Przechwytywanie 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 // Wyślij dane do 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// Inicjalizacja tylko w 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// Automatyczne testowanie na 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// Monitoring wydajności cross-browser
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// Użycie
86const monitor = new PerformanceMonitor();
87
88window.addEventListener('load', () => {
89 setTimeout(() => {
90 const report = monitor.report();
91 // Wyślij raport do analytics
92 console.log('Performance Report:', report);
93 }, 1000);
94});Testowanie cross-browser to:
Kluczem do sukcesu jest wczesne planowanie wsparcia przeglądarek, używanie nowoczesnych narzędzi do automatyzacji testów oraz systematyczne monitorowanie wydajności w różnych środowiskach.