Extracting text from a list of page elements, filtering an API response by status, and building test data sets all use the same four JavaScript methods: map, filter, find, and forEach. The most common mistake in Playwright specifically is calling forEach with an async callback: it fires promises without waiting, so assertions run before data arrives. This guide covers each method with the patterns that appear in real test suites, including how Promise.all with map replaces forEach for async element work.

Arrays: a one-minute recap

An array is an ordered list of values:

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 },
];

You access elements by index (starting at 0):

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

Everything else in this guide builds on top of this.

forEach: do something with each item

forEach runs a function once for every item in the array. Use it when you want to do something with each item but don't need a result back.

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

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

// Output:
// Testing user: alice
// Testing user: bob
// Testing user: charlie

In Playwright tests

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

// Check that all locale login pages load (not a great pattern,
// but shows forEach in action)
loginUrls.forEach(async (url) => {
  await page.goto(url);
  await expect(page.locator('h1')).toBeVisible();
});

When to use forEach: when you want side effects: logging, clicking buttons, filling in forms. You don't use it when you need to transform data or filter it. There's a better tool for that.

map: transform every item

map creates a new array by applying a function to every item. The original array doesn't change.

Think of it as: "for each item, give me back something different."

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]  ← unchanged

Practical example: extracting text from a list of elements

You often get back an array of locators from Playwright and need to extract the text from each one:

// Get all table cell texts
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', ...]

Building test data with 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' },
  ...
]
*/

Transforming API response data

// API returns raw data
const apiUsers = [
  { first_name: 'Alice', last_name: 'Smith', is_active: 1 },
  { first_name: 'Bob',   last_name: 'Jones', is_active: 0 },
];

// Transform it to match what your UI shows
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 }]

When to use map: when you want to transform every item in an array into something else. The result is always the same length as the input.

filter: keep only what you need

filter creates a new array containing only the items that pass a test. Items that fail the test are dropped.

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]  ← unchanged

Filtering test cases

This is where filter shines in test automation. When you have a big list of test cases, you can split them:

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 has 2 items, invalidCases has 3

Filtering API results

// All orders from 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 },
];

// Keep only completed orders for verification
const completedOrders = orders.filter((o) => o.status === 'completed');
// [{ id: 1, ... }, { id: 3, ... }]

Filtering page elements by text

// All rows in a table
const rows = await page.locator('table tbody tr').all();

// Only rows that contain 'Admin'
const adminRows = [];
for (const row of rows) {
  const text = await row.textContent();
  if (text?.includes('Admin')) {
    adminRows.push(row);
  }
}

When to use filter: when you want a subset of an array. The result can be shorter than the input (or even empty).

find: get the first match

find returns the first item that passes a test. If nothing matches, it returns undefined. Unlike filter, it stops searching as soon as it finds a match.

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

Finding test data by 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 }

Always check for undefined

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

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

// Now safe to use product
await page.fill('[data-testid="search"]', product.name);

When to use find: when you need exactly one item from an array and you know what you're looking for. If you need all matches, use filter.

Combining them

The real power comes from chaining these together:

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 },
];

// Get the total amount of Alice's completed orders
const aliceCompletedTotal = apiOrders
  .filter((o) => o.user === 'alice' && o.status === 'completed')
  .map((o) => o.amount)
  .reduce((sum, amount) => sum + amount, 0);

// 150 + 200 = 350

Or for verification:

// Verify that all visible product prices are above zero
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);

Quick reference

| Method | What it returns | Use when |

|--------|----------------|----------|

| forEach | Nothing (undefined) | You want side effects (logging, actions) |

| map | New array, same length | You want to transform each item |

| filter | New array, shorter or equal | You want a subset of items |

| find | One item or undefined | You want the first match |

Common mistakes

Using map when you want forEach:

// Wrong — map is for returning values, not side effects
users.map((user) => {
  console.log(user.name); // works but wasteful
});

// Right
users.forEach((user) => {
  console.log(user.name);
});

Forgetting that find can return undefined:

// This will throw if user is not found
const user = users.find((u) => u.id === 99);
user.name; // TypeError: Cannot read properties of undefined

Mutating the original array with map:

If you do users.map((u) => { u.role = 'admin'; return u; }), you've mutated the original objects, not just created new ones. Create new objects instead:

// Right — creates new objects
users.map((u) => ({ ...u, role: 'admin' }));

Async note for Playwright

When your callback is async (which happens constantly in Playwright), wrap with Promise.all:

// This doesn't work — forEach ignores promises
cells.forEach(async (cell) => {
  const text = await cell.textContent();  // ⚠️ fire-and-forget
});

// This works — waits for all promises
const texts = await Promise.all(
  cells.map(async (cell) => await cell.textContent())
);

This is the most common "async in arrays" gotcha in Playwright. When in doubt, use Promise.all with map.

You now have the core array methods used in real Playwright test suites. map, filter, find, and forEach cover 90% of what you'll need when working with test data, API responses, and arrays of page elements.

→ See also: JavaScript for QA Engineers: The Minimum You Need to Start Automating | Async/Await in Plain English (for Testers Who Get Tripped Up by Promises) | JavaScript Objects and Destructuring for QA Engineers