Spread and rest share the same ... syntax but do opposite things: spread expands an array or object in place, rest collects multiple values into one. The practical payoff in test automation is test data factories: one base object spread with overrides creates every variant without repeating all fields. This guide covers both operators with the patterns that appear in Playwright configs, fixture definitions, and request headers, plus the shallow-copy trap that corrupts test data when nested objects are shared between variants.
Spread Operator: Expanding Things
The spread operator (...) expands an iterable (array or object) in place.
Spreading arrays
const part1 = [1, 2, 3];
const part2 = [4, 5, 6];
const combined = [...part1, ...part2];
// [1, 2, 3, 4, 5, 6]
// Add items before or after
const withExtra = [0, ...part1, ...part2, 7];
// [0, 1, 2, 3, 4, 5, 6, 7]Copying an array
const original = ['alice', 'bob', 'charlie'];
const copy = [...original];
copy.push('dave'); // Only modifies copy
console.log(original); // ['alice', 'bob', 'charlie'] — unchangedSpreading objects
const baseConfig = { timeout: 30000, headless: true };
const ciConfig = { ...baseConfig, timeout: 60000 };
// { timeout: 60000, headless: true }
// Note: timeout was overridden by the later valueThe later key wins when there's a conflict.
Spread in Test Data: The Big Use Case
The most valuable pattern for QA engineers: creating object variants from a base:
const defaultUser = {
email: 'test@example.com',
password: 'ValidPass1',
role: 'member',
isActive: true,
emailVerified: true,
};
// Create variants without repeating all fields
const adminUser = { ...defaultUser, role: 'admin' };
const inactiveUser = { ...defaultUser, isActive: false };
const unverified = { ...defaultUser, emailVerified: false };
const customEmail = { ...defaultUser, email: 'custom@test.com' };This is the foundation of test data factories. Define the happy-path state once, create all your edge cases from it.
Merging test configs
const basePlaywrightConfig = {
use: {
baseURL: 'https://lab.becomeqa.com',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
timeout: 30_000,
};
// CI environment overrides
const ciOverrides = {
timeout: 60_000,
retries: 2,
};
const finalConfig = { ...basePlaywrightConfig, ...ciOverrides };Building test case lists
const happyPathCase = {
email: 'valid@example.com',
password: 'ValidPass1',
expectedResult: 'success',
expectedStatus: 200,
};
const testCases = [
happyPathCase,
{ ...happyPathCase, email: 'another@test.com' },
{ ...happyPathCase, password: 'AnotherValidPass2' },
{ ...happyPathCase, email: 'not-valid', expectedResult: 'error', expectedStatus: 422 },
];Much cleaner than repeating all fields for every test case.
Rest Parameters: Collecting Things
Rest parameters collect multiple items into a single parameter. It uses the same ... syntax but in a function definition, not a call:
// REST: collects multiple arguments into an array
function logTestResults(testName: string, ...results: string[]) {
console.log(`Test: ${testName}`);
results.forEach(r => console.log(` - ${r}`));
}
logTestResults('Login test', 'email validated', 'redirect worked', 'session created');
// Test: Login test
// - email validated
// - redirect worked
// - session createdThe ...results collects all arguments after testName into an array.
Rest in destructuring
Rest can also be used in destructuring to collect "everything else":
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first = 1, second = 2, rest = [3, 4, 5]
const { email, password, ...otherFields } = user;
// email and password are extracted
// otherFields = { role: 'member', isActive: true, ... }This is useful when you need specific fields and want to pass the rest along:
async function createUserAndGetToken({ email, password, ...profileData }: UserCreationData) {
// Use email and password for auth
const token = await auth.login(email, password);
// Use the rest for profile setup (without auth fields)
await api.updateProfile(token, profileData);
return token;
}Practical Patterns in Playwright
Pattern 1: Flexible test helper
type ClickOptions = {
timeout?: number;
force?: boolean;
};
async function clickAndWait(
page: Page,
selector: string,
{ timeout = 5000, force = false, ...options }: ClickOptions = {}
) {
await page.locator(selector).click({ timeout, force, ...options });
await page.waitForLoadState('networkidle');
}Pattern 2: Building request headers
const defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
const authHeaders = {
...defaultHeaders,
'Authorization': `Bearer ${token}`,
};
const adminHeaders = {
...authHeaders,
'X-Admin-Key': process.env.ADMIN_KEY,
};Pattern 3: Combining test data from multiple sources
const baseData = await readFixtureFile('base-user.json');
const envSpecific = await readFixtureFile(`${process.env.ENV}-overrides.json`);
const testData = { ...baseData, ...envSpecific };
// Environment-specific values override base valuesPattern 4: Collecting failed cases
async function runAllCases(cases: TestCase[]): Promise<string[]> {
const failures: string[] = [];
for (const testCase of cases) {
try {
await runCase(testCase);
} catch (error) {
failures.push(`${testCase.name}: ${error.message}`);
}
}
return failures;
}
// Call site:
const [firstFailure, ...otherFailures] = await runAllCases(testCases);
if (firstFailure) {
console.log('First failure:', firstFailure);
console.log('Additional failures:', otherFailures);
}Spread vs. Rest: Tell Them Apart
Spread — you're putting things in (expanding into an array/object literal):const arr = [...items]; // expanding items into array
const obj = { ...config }; // expanding config into object
func(...args); // expanding args as function argumentsfunction fn(...args) {} // collecting call arguments
const [a, ...rest] = array; // collecting remainder of array
const { x, ...others } = obj; // collecting remainder of objectSame ... symbol. Context determines the meaning.
A Common Mistake: Shallow Copy
Spread creates a shallow copy — nested objects are still shared references:
const user = { name: 'Alice', address: { city: 'NYC' } };
const copy = { ...user };
copy.name = 'Bob'; // ✅ Only changes copy
copy.address.city = 'London'; // ⚠️ Changes BOTH user and copy
console.log(user.address.city); // 'London' — oopsIf you need a deep copy (nested objects are also independent), you need a different approach:
// Simple deep copy (works for JSON-serializable data)
const deepCopy = JSON.parse(JSON.stringify(user));
// Or use structuredClone (modern JS)
const deepCopy2 = structuredClone(user);For most test data objects that are simple key-value pairs without nested mutable objects, spread is fine. But know the limitation.
Summary
| Syntax | Context | What it does |
|--------|---------|-------------|
| [...arr] | Array literal | Expands arr into the new array |
| {...obj} | Object literal | Copies all properties of obj |
| fn(...args) | Function call | Passes array elements as separate arguments |
| function fn(...params) | Function definition | Collects multiple arguments into array |
| const [a, ...rest] = arr | Array destructuring | Collects remaining items |
| const {x, ...rest} = obj | Object destructuring | Collects remaining properties |
Once the pattern clicks, you'll recognize it everywhere — in Playwright's use(), in test fixtures, in config files, and in every test data factory you'll ever read.