Spread и rest используют одинаковый синтаксис ..., но делают противоположное: spread разворачивает массив или объект на месте, rest собирает несколько значений в одно. Практическая польза для тест-автоматизации: фабрики тест-данных. Один базовый объект, развёрнутый с переопределениями, даёт любой вариант без повторения всех полей. Гайд охватывает оба оператора с паттернами из конфигов Playwright, определений фикстур и заголовков запросов, плюс ловушку с поверхностным копированием, которая портит тест-данные при совместном использовании вложенных объектов между вариантами.
Spread: разворачивание
Оператор spread (...) разворачивает итерируемое значение (массив или объект) на месте.
Spread для массивов
const part1 = [1, 2, 3];
const part2 = [4, 5, 6];
const combined = [...part1, ...part2];
// [1, 2, 3, 4, 5, 6]
// Добавить элементы до или после
const withExtra = [0, ...part1, ...part2, 7];
// [0, 1, 2, 3, 4, 5, 6, 7]Копирование массива
const original = ['alice', 'bob', 'charlie'];
const copy = [...original];
copy.push('dave'); // Изменяет только копию
console.log(original); // ['alice', 'bob', 'charlie'] — без измененийSpread для объектов
const baseConfig = { timeout: 30000, headless: true };
const ciConfig = { ...baseConfig, timeout: 60000 };
// { timeout: 60000, headless: true }
// timeout перекрыт более поздним значениемПри конфликте побеждает более поздний ключ.
Spread в тест-данных
Самый ценный паттерн для QA-инженера: создание вариантов объекта из базы:
const defaultUser = {
email: 'test@example.com',
password: 'ValidPass1',
role: 'member',
isActive: true,
emailVerified: true,
};
// Варианты без повторения всех полей
const adminUser = { ...defaultUser, role: 'admin' };
const inactiveUser = { ...defaultUser, isActive: false };
const unverified = { ...defaultUser, emailVerified: false };
const customEmail = { ...defaultUser, email: 'custom@test.com' };Это основа фабрик тест-данных. Описываешь happy-path один раз, все граничные случаи создаёшь из него.
Слияние конфигов тестов
const basePlaywrightConfig = {
use: {
baseURL: 'https://lab.becomeqa.com',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
timeout: 30_000,
};
// Переопределения для CI
const ciOverrides = {
timeout: 60_000,
retries: 2,
};
const finalConfig = { ...basePlaywrightConfig, ...ciOverrides };Построение списков тест-кейсов
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 },
];Намного чище чем повторять все поля для каждого тест-кейса.
Rest: сбор в одно
Rest-параметры собирают несколько значений в один параметр. Тот же синтаксис ..., но в определении функции, а не при вызове:
// REST: собирает несколько аргументов в массив
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 created...results собирает все аргументы после testName в массив.
Rest в деструктуризации
Rest можно использовать при деструктуризации чтобы собрать «всё остальное»:
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first = 1, second = 2, rest = [3, 4, 5]
const { email, password, ...otherFields } = user;
// email и password извлечены
// otherFields = { role: 'member', isActive: true, ... }Полезно когда нужны конкретные поля, а остальное нужно передать дальше:
async function createUserAndGetToken({ email, password, ...profileData }: UserCreationData) {
// email и password используем для авторизации
const token = await auth.login(email, password);
// Остальное используем для настройки профиля (без полей авторизации)
await api.updateProfile(token, profileData);
return token;
}Практические паттерны в Playwright
Паттерн 1: гибкий хелпер для кликов
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');
}Паттерн 2: построение заголовков запросов
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,
};Паттерн 3: объединение тест-данных из нескольких источников
const baseData = await readFixtureFile('base-user.json');
const envSpecific = await readFixtureFile(`${process.env.ENV}-overrides.json`);
const testData = { ...baseData, ...envSpecific };
// Значения из envSpecific перекрывают базовыеПаттерн 4: сбор упавших кейсов
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;
}
// На месте вызова:
const [firstFailure, ...otherFailures] = await runAllCases(testCases);
if (firstFailure) {
console.log('First failure:', firstFailure);
console.log('Additional failures:', otherFailures);
}Spread vs. rest: как различить
Spread работает на расширение: кладёшь вещи внутрь (разворачиваешь в литерал массива или объекта):const arr = [...items]; // разворачиваем items в массив
const obj = { ...config }; // разворачиваем config в объект
func(...args); // разворачиваем args как аргументы функцииfunction fn(...args) {} // собираем аргументы вызова
const [a, ...rest] = array; // собираем остаток массива
const { x, ...others } = obj; // собираем остаток объектаОдин символ .... Смысл определяет контекст.
Частая ошибка: поверхностное копирование
Spread создаёт поверхностную копию: вложенные объекты остаются общими ссылками:
const user = { name: 'Alice', address: { city: 'NYC' } };
const copy = { ...user };
copy.name = 'Bob'; // Изменяет только копию
copy.address.city = 'London'; // Изменяет и user, и copy!
console.log(user.address.city); // 'London' — сюрпризЕсли нужна глубокая копия (вложенные объекты тоже независимы), нужен другой подход:
// Простое глубокое копирование (для JSON-сериализуемых данных)
const deepCopy = JSON.parse(JSON.stringify(user));
// Или через structuredClone (современный JS)
const deepCopy2 = structuredClone(user);Для большинства объектов с тест-данными, которые представляют простые пары ключ-значение без вложенных изменяемых объектов, spread работает нормально. Но ограничение стоит знать.
Краткая справка
| Синтаксис | Контекст | Что делает |
|-----------|---------|-----------|
| [...arr] | Литерал массива | Разворачивает arr в новый массив |
| {...obj} | Литерал объекта | Копирует все свойства obj |
| fn(...args) | Вызов функции | Передаёт элементы массива как отдельные аргументы |
| function fn(...params) | Определение функции | Собирает несколько аргументов в массив |
| const [a, ...rest] = arr | Деструктуризация массива | Собирает оставшиеся элементы |
| const {x, ...rest} = obj | Деструктуризация объекта | Собирает оставшиеся свойства |
Когда паттерн щёлкнет, он будет виден везде: в use() Playwright, в фикстурах, в конфиг-файлах и в каждой фабрике тест-данных.