Testing and Quality Assurance
1. Unit Testing Polyfill Implementation
| Test Framework | Features | Polyfill Testing | Best For |
|---|---|---|---|
| Jest | Built-in assertions, mocking | jsdom environment, code coverage | Modern projects |
| Mocha | Flexible, BDD/TDD style | Chai assertions, Sinon mocks | Node.js, flexible setups |
| Jasmine | All-in-one framework | Spies, matchers included | Standalone, no dependencies |
| AVA | Concurrent execution | Fast, isolated tests | Performance-focused |
| QUnit | Simple, minimal | Browser and Node | jQuery projects |
Example: Jest unit tests for polyfills
// array-includes.polyfill.js
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
if (this == null) {
throw new TypeError('Array.prototype.includes called on null or undefined');
}
var O = Object(this);
var len = parseInt(O.length) || 0;
if (len === 0) {
return false;
}
var n = parseInt(fromIndex) || 0;
var k;
if (n >= 0) {
k = n;
} else {
k = len + n;
if (k < 0) {
k = 0;
}
}
while (k < len) {
var currentElement = O[k];
if (searchElement === currentElement ||
(searchElement !== searchElement && currentElement !== currentElement)) {
return true;
}
k++;
}
return false;
};
}
// array-includes.test.js
describe('Array.prototype.includes polyfill', () => {
beforeAll(() => {
// Remove native implementation to test polyfill
delete Array.prototype.includes;
require('./array-includes.polyfill');
});
afterAll(() => {
// Restore native implementation
delete Array.prototype.includes;
});
test('should find element in array', () => {
expect([1, 2, 3].includes(2)).toBe(true);
expect([1, 2, 3].includes(4)).toBe(false);
});
test('should handle fromIndex parameter', () => {
expect([1, 2, 3, 2].includes(2, 2)).toBe(true);
expect([1, 2, 3].includes(2, 2)).toBe(false);
});
test('should handle negative fromIndex', () => {
expect([1, 2, 3].includes(3, -1)).toBe(true);
expect([1, 2, 3].includes(2, -1)).toBe(false);
});
test('should handle NaN correctly', () => {
expect([1, NaN, 3].includes(NaN)).toBe(true);
expect([1, 2, 3].includes(NaN)).toBe(false);
});
test('should handle sparse arrays', () => {
var sparseArray = [1, , 3];
expect(sparseArray.includes(undefined)).toBe(true);
});
test('should throw TypeError on null/undefined', () => {
expect(() => {
Array.prototype.includes.call(null, 1);
}).toThrow(TypeError);
expect(() => {
Array.prototype.includes.call(undefined, 1);
}).toThrow(TypeError);
});
test('should handle array-like objects', () => {
var arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
expect(Array.prototype.includes.call(arrayLike, 'b')).toBe(true);
});
test('should match native behavior', () => {
// Compare with native on arrays that support both
var testArray = [1, 2, 3, NaN, undefined];
var testCases = [
[2, undefined],
[NaN, undefined],
[undefined, undefined],
[2, 1],
[2, -2]
];
testCases.forEach(([val, fromIdx]) => {
var polyfillResult = testArray.includes(val, fromIdx);
// Would compare with native if available
expect(typeof polyfillResult).toBe('boolean');
});
});
});
Example: Testing Promise polyfill
// promise.test.js
describe('Promise polyfill', () => {
test('should resolve with value', (done) => {
new Promise((resolve) => {
resolve('success');
}).then((value) => {
expect(value).toBe('success');
done();
});
});
test('should reject with error', (done) => {
new Promise((resolve, reject) => {
reject(new Error('failed'));
}).catch((error) => {
expect(error.message).toBe('failed');
done();
});
});
test('should chain promises', (done) => {
Promise.resolve(1)
.then((val) => val + 1)
.then((val) => val * 2)
.then((val) => {
expect(val).toBe(4);
done();
});
});
test('Promise.all should wait for all', (done) => {
Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]).then((values) => {
expect(values).toEqual([1, 2, 3]);
done();
});
});
test('Promise.race should return first', (done) => {
Promise.race([
new Promise((resolve) => setTimeout(() => resolve(1), 100)),
new Promise((resolve) => setTimeout(() => resolve(2), 50))
]).then((value) => {
expect(value).toBe(2);
done();
});
});
});
Note: Remove native implementations in tests to ensure polyfill code is
actually tested. Use beforeAll/afterAll for setup and teardown.
2. Cross-browser Compatibility Testing
| Tool/Service | Browsers Supported | Features | Cost |
|---|---|---|---|
| BrowserStack | 3000+ browser/device combos | Live testing, screenshots, automation | Paid (free tier available) |
| Sauce Labs | 800+ browser/OS combinations | Selenium, Appium, manual testing | Paid |
| LambdaTest | 3000+ browsers/devices | Live, screenshot, responsive testing | Paid (free tier available) |
| Playwright | Chrome, Firefox, Safari (WebKit) | Local automated testing | Free |
| TestCafe | All major browsers | No WebDriver, easy setup | Free |
Example: Playwright cross-browser tests
// playwright.config.js
module.exports = {
projects: [
{
name: 'chromium',
use: { browserName: 'chromium' }
},
{
name: 'firefox',
use: { browserName: 'firefox' }
},
{
name: 'webkit',
use: { browserName: 'webkit' }
}
]
};
// polyfills.spec.js
const { test, expect } = require('@playwright/test');
test.describe('Polyfill cross-browser tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:3000/test.html');
});
test('Array.includes should work', async ({ page }) => {
const result = await page.evaluate(() => {
return [1, 2, 3].includes(2);
});
expect(result).toBe(true);
});
test('Promise should work', async ({ page }) => {
const result = await page.evaluate(() => {
return new Promise((resolve) => {
resolve('success');
});
});
expect(result).toBe('success');
});
test('fetch API should work', async ({ page }) => {
const result = await page.evaluate(async () => {
if (typeof fetch !== 'function') {
return 'fetch not available';
}
return 'fetch available';
});
expect(result).toBe('fetch available');
});
test('IntersectionObserver should work', async ({ page }) => {
const result = await page.evaluate(() => {
return typeof IntersectionObserver !== 'undefined';
});
expect(result).toBe(true);
});
test('Custom Elements should work', async ({ page }) => {
const result = await page.evaluate(() => {
return typeof customElements !== 'undefined';
});
expect(result).toBe(true);
});
});
Example: BrowserStack configuration
// browserstack.config.js
exports.config = {
user: process.env.BROWSERSTACK_USERNAME,
key: process.env.BROWSERSTACK_ACCESS_KEY,
capabilities: [
{
browserName: 'Chrome',
browser_version: '91.0',
os: 'Windows',
os_version: '10'
},
{
browserName: 'Firefox',
browser_version: '89.0',
os: 'Windows',
os_version: '10'
},
{
browserName: 'Safari',
browser_version: '14.0',
os: 'OS X',
os_version: 'Big Sur'
},
{
browserName: 'IE',
browser_version: '11.0',
os: 'Windows',
os_version: '10'
},
{
browserName: 'Edge',
browser_version: '91.0',
os: 'Windows',
os_version: '10'
}
],
specs: [
'./test/polyfills/**/*.spec.js'
],
maxInstances: 5,
logLevel: 'info',
baseUrl: 'http://localhost:3000',
waitforTimeout: 10000
};
Note: Test on real browsers and devices, not just emulators.
Focus on target browsers (IE11, older Safari) that need polyfills most.
3. Feature Parity Testing and Validation
| Validation Type | What to Test | Method | Tools |
|---|---|---|---|
| API Signature | Function parameters, return type | Compare with spec | TypeScript, JSDoc |
| Behavior Parity | Same results as native | Side-by-side comparison | Test262, Custom tests |
| Edge Cases | Null, undefined, NaN, empty | Boundary testing | Property-based testing |
| Error Handling | Same errors as native | Exception testing | Jest, Mocha assertions |
| Spec Compliance | ECMAScript specification | Test262 suite | Test262 runner |
Example: Feature parity test suite
// feature-parity.test.js
describe('Feature parity validation', () => {
// Test native vs polyfill
function testParity(nativeMethod, polyfillMethod, testCases) {
testCases.forEach(({ input, description }) => {
test(description, () => {
var nativeResult, polyfillResult;
var nativeError, polyfillError;
// Test native
try {
nativeResult = nativeMethod.apply(null, input);
} catch (e) {
nativeError = e;
}
// Test polyfill
try {
polyfillResult = polyfillMethod.apply(null, input);
} catch (e) {
polyfillError = e;
}
// Compare results
if (nativeError && polyfillError) {
expect(polyfillError.constructor).toBe(nativeError.constructor);
expect(polyfillError.message).toBe(nativeError.message);
} else if (nativeError || polyfillError) {
fail('Error mismatch: native=' + nativeError + ', polyfill=' + polyfillError);
} else {
expect(polyfillResult).toEqual(nativeResult);
}
});
});
}
describe('Object.assign parity', () => {
var nativeAssign = Object.assign.bind(Object);
// Save and replace with polyfill
var originalAssign = Object.assign;
Object.assign = function(target, ...sources) {
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var i = 0; i < sources.length; i++) {
var nextSource = sources[i];
if (nextSource != null) {
for (var key in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, key)) {
to[key] = nextSource[key];
}
}
}
}
return to;
};
var testCases = [
{
input: [{}, { a: 1 }],
description: 'should copy properties'
},
{
input: [{ a: 1 }, { a: 2, b: 3 }],
description: 'should overwrite properties'
},
{
input: [{}, null, { a: 1 }],
description: 'should skip null sources'
},
{
input: [null, { a: 1 }],
description: 'should throw on null target'
},
{
input: [undefined, { a: 1 }],
description: 'should throw on undefined target'
}
];
testParity(originalAssign, Object.assign, testCases);
// Restore
Object.assign = originalAssign;
});
describe('Array.find parity', () => {
var testArray = [1, 2, 3, 4, 5];
test('should find first matching element', () => {
var native = testArray.find(x => x > 2);
expect(native).toBe(3);
});
test('should return undefined when not found', () => {
var native = testArray.find(x => x > 10);
expect(native).toBeUndefined();
});
test('should pass element, index, array to callback', () => {
var calls = [];
testArray.find((el, idx, arr) => {
calls.push({ el, idx, arr });
return false;
});
expect(calls.length).toBe(5);
expect(calls[0]).toEqual({ el: 1, idx: 0, arr: testArray });
});
test('should use thisArg', () => {
var context = { threshold: 3 };
var result = testArray.find(function(x) {
return x > this.threshold;
}, context);
expect(result).toBe(4);
});
});
});
Example: Property-based testing
// property-based-tests.js (using fast-check)
const fc = require('fast-check');
describe('Property-based polyfill tests', () => {
test('Array.includes should be consistent', () => {
fc.assert(
fc.property(
fc.array(fc.integer()),
fc.integer(),
(arr, val) => {
var includesResult = arr.includes(val);
var indexOfResult = arr.indexOf(val) !== -1;
// includes and indexOf should agree (except for NaN)
if (!Number.isNaN(val)) {
return includesResult === indexOfResult;
}
return true;
}
)
);
});
test('Object.assign should merge objects', () => {
fc.assert(
fc.property(
fc.object(),
fc.object(),
(obj1, obj2) => {
var result = Object.assign({}, obj1, obj2);
// All keys from obj2 should be in result
return Object.keys(obj2).every(key => {
return result[key] === obj2[key];
});
}
)
);
});
test('String.padStart should maintain length', () => {
fc.assert(
fc.property(
fc.string(),
fc.integer({ min: 0, max: 100 }),
fc.string({ maxLength: 10 }),
(str, targetLength, padString) => {
var padded = str.padStart(targetLength, padString || ' ');
// Result should be at least targetLength
return padded.length >= Math.min(str.length, targetLength);
}
)
);
});
});
Note: Use Test262 suite for comprehensive ECMAScript spec
compliance testing. Property-based testing finds edge cases automatically.
4. Performance Impact Measurement
| Metric | Native Baseline | Acceptable Impact | Measurement Tool |
|---|---|---|---|
| Execution Time | 1x | <2x native speed | performance.now() |
| Memory Usage | Baseline | <20% increase | performance.memory |
| Bundle Size | 0 KB (native) | <5 KB per polyfill | webpack-bundle-analyzer |
| Parse Time | 0 ms | <10 ms total | DevTools Performance |
| FCP Impact | Baseline | <100 ms delay | Lighthouse |
Example: Performance benchmarking
// performance-benchmark.js
function benchmark(name, fn, iterations) {
// Warm up
for (var i = 0; i < 100; i++) {
fn();
}
// Measure
var start = performance.now();
for (var i = 0; i < iterations; i++) {
fn();
}
var end = performance.now();
var totalTime = end - start;
var avgTime = totalTime / iterations;
console.log(name + ':');
console.log(' Total: ' + totalTime.toFixed(2) + 'ms');
console.log(' Average: ' + (avgTime * 1000).toFixed(2) + 'μs');
console.log(' Ops/sec: ' + Math.round(iterations / (totalTime / 1000)));
return avgTime;
}
// Test Array.includes performance
var testArray = Array.from({ length: 1000 }, (_, i) => i);
var iterations = 10000;
var nativeTime = benchmark('Native Array.includes', function() {
testArray.includes(500);
}, iterations);
// Test polyfill performance
delete Array.prototype.includes;
require('./array-includes.polyfill');
var polyfillTime = benchmark('Polyfill Array.includes', function() {
testArray.includes(500);
}, iterations);
var slowdown = polyfillTime / nativeTime;
console.log('\nSlowdown factor: ' + slowdown.toFixed(2) + 'x');
if (slowdown > 2) {
console.warn('WARNING: Polyfill is more than 2x slower than native!');
}
Example: Memory usage tracking
// memory-usage.test.js
describe('Memory usage tests', () => {
// Chrome-specific memory measurement
function measureMemory(fn) {
if (!performance.memory) {
console.warn('performance.memory not available');
return null;
}
// Force garbage collection (requires --expose-gc flag)
if (global.gc) {
global.gc();
}
var before = performance.memory.usedJSHeapSize;
fn();
var after = performance.memory.usedJSHeapSize;
return after - before;
}
test('Promise polyfill memory usage', () => {
var memoryUsed = measureMemory(() => {
var promises = [];
for (var i = 0; i < 1000; i++) {
promises.push(new Promise(resolve => resolve(i)));
}
return Promise.all(promises);
});
if (memoryUsed !== null) {
var memoryMB = memoryUsed / (1024 * 1024);
console.log('Memory used: ' + memoryMB.toFixed(2) + ' MB');
// Should use less than 5 MB for 1000 promises
expect(memoryMB).toBeLessThan(5);
}
});
test('Map polyfill memory efficiency', () => {
var memoryUsed = measureMemory(() => {
var map = new Map();
for (var i = 0; i < 10000; i++) {
map.set('key' + i, { value: i });
}
});
if (memoryUsed !== null) {
var memoryKB = memoryUsed / 1024;
console.log('Memory used: ' + memoryKB.toFixed(2) + ' KB');
// Should be reasonably efficient
expect(memoryKB).toBeLessThan(2000); // < 2 MB
}
});
});
Warning: Polyfills typically run 1.5-3x slower than native
implementations. If performance is critical, consider dropping support for older browsers.
5. Automated Browser Testing with Polyfills
| Tool | Type | Browsers | CI Integration |
|---|---|---|---|
| Selenium WebDriver | End-to-end | All major browsers | Excellent |
| Playwright | End-to-end | Chromium, Firefox, WebKit | Excellent |
| Puppeteer | End-to-end | Chrome/Chromium only | Good |
| TestCafe | End-to-end | All major browsers | Good |
| Karma | Test runner | Multiple via launchers | Excellent |
Example: Karma configuration for multi-browser testing
// karma.conf.js
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: [
// Load polyfills first
'polyfills/es5-shim.js',
'polyfills/es6-shim.js',
'polyfills/fetch.js',
// Then test files
'src/**/*.js',
'test/**/*.spec.js'
],
preprocessors: {
'src/**/*.js': ['webpack', 'coverage'],
'test/**/*.spec.js': ['webpack']
},
webpack: {
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
},
// Test against multiple browsers
browsers: [
'Chrome',
'Firefox',
'Safari',
'IE11' // Requires IE11 launcher
],
// Custom launchers for specific configurations
customLaunchers: {
ChromeHeadless_Custom: {
base: 'ChromeHeadless',
flags: [
'--no-sandbox',
'--disable-gpu',
'--disable-dev-shm-usage'
]
},
IE11: {
base: 'IE',
'x-ua-compatible': 'IE=EmulateIE11'
}
},
reporters: ['progress', 'coverage'],
coverageReporter: {
type: 'html',
dir: 'coverage/'
},
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: false,
singleRun: true,
concurrency: Infinity
});
};
Example: GitHub Actions CI workflow
# .github/workflows/test.yml
name: Cross-browser Testing
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
browser: [chrome, firefox, webkit]
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run tests
run: npm test -- --project=${{ matrix.browser }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results-${{ matrix.os }}-${{ matrix.browser }}
path: test-results/
- name: Upload coverage
if: matrix.os == 'ubuntu-latest' && matrix.browser == 'chrome'
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
Note: Automate testing across multiple browsers and OS
combinations in CI/CD. Use headless browsers for speed, real browsers for final validation.
6. Mock and Stub Patterns for Testing
| Pattern | Use Case | Implementation | Restore Method |
|---|---|---|---|
| Mock | Replace entire object/function | jest.mock(), sinon.mock() | Automatic in afterEach |
| Stub | Replace specific method | jest.fn(), sinon.stub() | Restore original |
| Spy | Track calls without replacing | jest.spyOn(), sinon.spy() | Restore or remove spy |
| Fake | Working simplified implementation | Custom implementation | Manual restore |
| Dummy | Placeholder, never used | Empty object/function | N/A |
Example: Mocking browser APIs
// mock-apis.test.js
describe('API mocking for polyfill tests', () => {
describe('fetch polyfill', () => {
var originalFetch;
beforeEach(() => {
// Save and mock fetch
originalFetch = global.fetch;
global.fetch = jest.fn((url) => {
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({ data: 'mocked' }),
text: () => Promise.resolve('mocked text')
});
});
});
afterEach(() => {
// Restore original
global.fetch = originalFetch;
});
test('should make request', async () => {
var response = await fetch('/api/test');
var data = await response.json();
expect(fetch).toHaveBeenCalledWith('/api/test');
expect(data).toEqual({ data: 'mocked' });
});
});
describe('IntersectionObserver polyfill', () => {
var mockObserve;
var mockUnobserve;
var mockDisconnect;
beforeEach(() => {
mockObserve = jest.fn();
mockUnobserve = jest.fn();
mockDisconnect = jest.fn();
global.IntersectionObserver = jest.fn(function(callback) {
this.observe = mockObserve;
this.unobserve = mockUnobserve;
this.disconnect = mockDisconnect;
// Simulate intersection
setTimeout(() => {
callback([
{
isIntersecting: true,
target: document.createElement('div'),
intersectionRatio: 1
}
]);
}, 0);
});
});
test('should observe elements', (done) => {
var element = document.createElement('div');
var observer = new IntersectionObserver((entries) => {
expect(entries[0].isIntersecting).toBe(true);
expect(mockObserve).toHaveBeenCalledWith(element);
done();
});
observer.observe(element);
});
});
describe('localStorage mock', () => {
var localStorageMock;
beforeEach(() => {
localStorageMock = (function() {
var store = {};
return {
getItem: function(key) {
return store[key] || null;
},
setItem: function(key, value) {
store[key] = String(value);
},
removeItem: function(key) {
delete store[key];
},
clear: function() {
store = {};
},
get length() {
return Object.keys(store).length;
},
key: function(index) {
var keys = Object.keys(store);
return keys[index] || null;
}
};
})();
Object.defineProperty(global, 'localStorage', {
value: localStorageMock,
writable: true
});
});
test('should store and retrieve items', () => {
localStorage.setItem('key', 'value');
expect(localStorage.getItem('key')).toBe('value');
localStorage.removeItem('key');
expect(localStorage.getItem('key')).toBeNull();
});
test('should track length', () => {
expect(localStorage.length).toBe(0);
localStorage.setItem('key1', 'value1');
localStorage.setItem('key2', 'value2');
expect(localStorage.length).toBe(2);
});
});
});
Example: Sinon stubs for complex scenarios
// sinon-stubs.test.js
var sinon = require('sinon');
describe('Sinon stub patterns', () => {
describe('Date polyfill testing', () => {
var clock;
beforeEach(() => {
// Fake timers
clock = sinon.useFakeTimers(new Date('2025-01-01').getTime());
});
afterEach(() => {
clock.restore();
});
test('should use fixed date', () => {
var now = Date.now();
expect(now).toBe(new Date('2025-01-01').getTime());
// Advance time
clock.tick(1000);
expect(Date.now()).toBe(new Date('2025-01-01').getTime() + 1000);
});
});
describe('Performance API stubbing', () => {
var performanceStub;
beforeEach(() => {
performanceStub = sinon.stub(performance, 'now');
performanceStub.returns(1000.5);
});
afterEach(() => {
performanceStub.restore();
});
test('should return stubbed time', () => {
expect(performance.now()).toBe(1000.5);
// Change stub behavior
performanceStub.returns(2000.5);
expect(performance.now()).toBe(2000.5);
});
});
describe('Network request stubbing', () => {
var xhr, requests;
beforeEach(() => {
xhr = sinon.useFakeXMLHttpRequest();
requests = [];
xhr.onCreate = function(req) {
requests.push(req);
};
});
afterEach(() => {
xhr.restore();
});
test('should capture XHR requests', () => {
var callback = sinon.spy();
var req = new XMLHttpRequest();
req.open('GET', '/api/test');
req.onload = callback;
req.send();
expect(requests.length).toBe(1);
expect(requests[0].url).toBe('/api/test');
// Respond to request
requests[0].respond(200,
{ 'Content-Type': 'application/json' },
JSON.stringify({ success: true })
);
expect(callback.calledOnce).toBe(true);
});
});
});
Key Takeaways - Testing & Quality Assurance
- Unit Tests: Remove native implementations to test polyfill code directly
- Cross-browser: Test on real browsers (IE11, Safari) not just modern ones
- Feature Parity: Compare with native behavior, use Test262 for spec compliance
- Performance: Polyfills should be <2x slower, monitor bundle size impact
- Automation: Integrate multi-browser testing in CI/CD pipeline
- Mocking: Use stubs/mocks to test polyfills in isolation