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 как аргументы функции

Rest работает на сбор: забираешь вещи из источника (собираешь в параметр):

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, в фикстурах, в конфиг-файлах и в каждой фабрике тест-данных.

→ See also: JavaScript объекты и деструктуризация для QA-инженеров | JavaScript для QA-инженеров: необходимый минимум для автоматизации | TypeScript для QA: почему статическая типизация улучшает тесты