Большинство задач с тест-данными решается четырьмя методами JavaScript: map, filter, find и forEach. Они покрывают извлечение текста из элементов страницы, фильтрацию API-ответов и построение наборов тестовых данных. Самая частая ошибка в Playwright: вызов forEach с async-колбэком. Он запускает промисы без ожидания, и проверки выполняются раньше чем приходят данные. В этом гайде: каждый метод с паттернами из реальных тест-сьютов, включая как Promise.all с map заменяет forEach при асинхронной работе с элементами.

Массивы: краткое напоминание

Массив хранит упорядоченный список значений:

const users = ['Alice', 'Bob', 'Charlie'];
const prices = [12.99, 5.50, 29.00, 8.75];
const testCases = [
  { input: 'valid@email.com', shouldPass: true },
  { input: 'not-an-email',    shouldPass: false },
  { input: '',                shouldPass: false },
];

Доступ к элементам по индексу (начиная с 0):

console.log(users[0]);     // 'Alice'
console.log(users[2]);     // 'Charlie'
console.log(users.length); // 3

Всё остальное в этом гайде строится на этом.

forEach: сделать что-то с каждым элементом

forEach запускает функцию один раз для каждого элемента массива. Используй когда хочешь что-то сделать с каждым элементом, но результат не нужен.

const usernames = ['alice', 'bob', 'charlie'];

usernames.forEach((name) => {
  console.log(`Testing user: ${name}`);
});

// Вывод:
// Testing user: alice
// Testing user: bob
// Testing user: charlie

В тестах Playwright

const loginUrls = [
  '/en/login',
  '/ru/login',
  '/es/login',
];

// Проверяем что все страницы логина для разных локалей загружаются
loginUrls.forEach(async (url) => {
  await page.goto(url);
  await expect(page.locator('h1')).toBeVisible();
});

Когда использовать forEach: когда нужны побочные эффекты: логирование, клики по кнопкам, заполнение форм. Не используй его когда нужно трансформировать данные или фильтровать. Для этого есть более подходящий инструмент.

map: трансформировать каждый элемент

map создаёт новый массив применяя функцию к каждому элементу. Исходный массив не изменяется.

Думай об этом так: «для каждого элемента верни мне что-то другое».

const prices = [10, 20, 30];
const discounted = prices.map((price) => price * 0.9);

console.log(discounted); // [9, 18, 27]
console.log(prices);     // [10, 20, 30]  <- без изменений

Практический пример: извлечение текста из списка элементов

Часто получаешь массив локаторов от Playwright и нужно извлечь текст из каждого:

// Получаем текст всех ячеек таблицы
const cells = await page.locator('table tbody td').all();
const texts = await Promise.all(
  cells.map((cell) => cell.textContent())
);
// texts = ['Alice', '28', 'Admin', 'Bob', '34', 'User', ...]

Построение тестовых данных через map

const userIds = [1, 2, 3, 4, 5];

const testUsers = userIds.map((id) => ({
  id,
  username: `user_${id}`,
  email: `user${id}@test.com`,
  role: id === 1 ? 'admin' : 'member',
}));

/*
[
  { id: 1, username: 'user_1', email: 'user1@test.com', role: 'admin' },
  { id: 2, username: 'user_2', email: 'user2@test.com', role: 'member' },
  ...
]
*/

Трансформация данных API-ответа

// API возвращает сырые данные
const apiUsers = [
  { first_name: 'Alice', last_name: 'Smith', is_active: 1 },
  { first_name: 'Bob',   last_name: 'Jones', is_active: 0 },
];

// Трансформируем под то что показывает UI
const displayUsers = apiUsers.map((u) => ({
  name: `${u.first_name} ${u.last_name}`,
  active: u.is_active === 1,
}));

// [{ name: 'Alice Smith', active: true }, { name: 'Bob Jones', active: false }]

Когда использовать map: когда нужно трансформировать каждый элемент массива во что-то другое. Результат всегда той же длины что и входной массив.

filter: оставить только нужное

filter создаёт новый массив содержащий только элементы которые прошли проверку. Элементы не прошедшие проверку отбрасываются.

const prices = [5, 12, 3, 25, 8, 40, 1];
const expensive = prices.filter((price) => price > 10);

console.log(expensive); // [12, 25, 40]
console.log(prices);    // [5, 12, 3, 25, 8, 40, 1]  <- без изменений

Фильтрация тест-кейсов

Здесь filter раскрывается в тест-автоматизации. Когда есть большой список тест-кейсов, можно разделить их:

const allTestCases = [
  { input: 'valid@email.com', shouldPass: true },
  { input: 'another@test.org', shouldPass: true },
  { input: 'not-an-email',    shouldPass: false },
  { input: '',                shouldPass: false },
  { input: 'missing@',       shouldPass: false },
];

const validCases   = allTestCases.filter((tc) => tc.shouldPass);
const invalidCases = allTestCases.filter((tc) => !tc.shouldPass);

// validCases содержит 2 элемента, invalidCases - 3

Фильтрация результатов API

// Все заказы из API
const orders = [
  { id: 1, status: 'completed', amount: 99 },
  { id: 2, status: 'pending',   amount: 45 },
  { id: 3, status: 'completed', amount: 12 },
  { id: 4, status: 'cancelled', amount: 75 },
];

// Оставляем только завершённые заказы для проверки
const completedOrders = orders.filter((o) => o.status === 'completed');
// [{ id: 1, ... }, { id: 3, ... }]

Фильтрация элементов страницы по тексту

// Все строки таблицы
const rows = await page.locator('table tbody tr').all();

// Только строки содержащие 'Admin'
const adminRows = [];
for (const row of rows) {
  const text = await row.textContent();
  if (text?.includes('Admin')) {
    adminRows.push(row);
  }
}

Когда использовать filter: когда нужно подмножество массива. Результат может быть короче входного (или даже пустым).

find: получить первое совпадение

find возвращает первый элемент прошедший проверку. Если ничего не совпало, возвращает undefined. В отличие от filter, прекращает поиск как только находит совпадение.

const users = [
  { id: 1, name: 'Alice', role: 'admin' },
  { id: 2, name: 'Bob',   role: 'member' },
  { id: 3, name: 'Carol', role: 'admin' },
];

const firstAdmin = users.find((u) => u.role === 'admin');
// { id: 1, name: 'Alice', role: 'admin' }

const missingUser = users.find((u) => u.name === 'Dave');
// undefined

Поиск тестовых данных по ID

const products = [
  { id: 'PROD-001', name: 'Laptop', price: 999 },
  { id: 'PROD-002', name: 'Mouse',  price: 29  },
  { id: 'PROD-003', name: 'Desk',   price: 349 },
];

const targetProduct = products.find((p) => p.id === 'PROD-002');
// { id: 'PROD-002', name: 'Mouse', price: 29 }

Всегда проверяй на undefined

const product = products.find((p) => p.id === 'PROD-999');

if (!product) {
  throw new Error('Test data not found: PROD-999');
}

// Теперь безопасно использовать product
await page.fill('[data-testid="search"]', product.name);

Когда использовать find: когда нужен ровно один элемент из массива и знаешь что ищешь. Если нужны все совпадения, используй filter.

Комбинирование методов

Настоящая сила появляется при цепочке методов:

const apiOrders = [
  { id: 1, user: 'alice', status: 'completed', amount: 150, items: 3 },
  { id: 2, user: 'bob',   status: 'pending',   amount: 50,  items: 1 },
  { id: 3, user: 'alice', status: 'completed', amount: 200, items: 5 },
  { id: 4, user: 'carol', status: 'cancelled', amount: 75,  items: 2 },
  { id: 5, user: 'alice', status: 'pending',   amount: 30,  items: 1 },
];

// Получаем общую сумму завершённых заказов Alice
const aliceCompletedTotal = apiOrders
  .filter((o) => o.user === 'alice' && o.status === 'completed')
  .map((o) => o.amount)
  .reduce((sum, amount) => sum + amount, 0);

// 150 + 200 = 350

Или для проверки:

// Проверяем что все видимые цены товаров больше нуля
const priceLocators = await page.locator('[data-testid="product-price"]').all();
const prices = await Promise.all(
  priceLocators.map(async (el) => {
    const text = await el.textContent();
    return parseFloat(text?.replace('$', '') ?? '0');
  })
);

const hasNegativePrice = prices.some((p) => p <= 0);
expect(hasNegativePrice).toBe(false);

Краткая справка

МетодЧто возвращаетИспользовать когда
forEachНичего (undefined)Нужны побочные эффекты (логирование, действия)
mapНовый массив, той же длиныНужно трансформировать каждый элемент
filterНовый массив, короче или равной длиныНужно подмножество элементов
findОдин элемент или undefinedНужно первое совпадение

Частые ошибки

Использование map вместо forEach

// Неправильно: map предназначен для возврата значений, а не побочных эффектов
users.map((user) => {
  console.log(user.name); // работает, но расточительно
});

// Правильно
users.forEach((user) => {
  console.log(user.name);
});

Забыть что find может вернуть undefined

// Упадёт если пользователь не найден
const user = users.find((u) => u.id === 99);
user.name; // TypeError: Cannot read properties of undefined

Мутация исходного массива через map

Если делаешь users.map((u) => { u.role = 'admin'; return u; }), ты мутируешь исходные объекты, а не создаёшь новые. Создавай новые объекты:

// Правильно: создаёт новые объекты
users.map((u) => ({ ...u, role: 'admin' }));

Примечание про async в Playwright

Когда колбэк асинхронный (а в Playwright это постоянно), оборачивай в Promise.all:

// Не работает: forEach игнорирует промисы
cells.forEach(async (cell) => {
  const text = await cell.textContent();  // fire-and-forget
});

// Работает: ждёт все промисы
const texts = await Promise.all(
  cells.map(async (cell) => await cell.textContent())
);

Это самая частая ловушка с async в массивах в Playwright. Если сомневаешься, используй Promise.all с map.

→ См. также: JavaScript для QA-инженеров: необходимый минимум для автоматизации | Async/Await простым языком (для тестировщиков, которых сбивают с толку промисы) | JavaScript объекты и деструктуризация для QA-инженеров