Большинство задач с тест-данными решается четырьмя методами 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.

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