|

JavaScript Arrays, Objects and ES6 Features for QA Engineers

Table of Contents

Contents

Why Arrays and Objects Are the Real Foundation of Test Automation

If you know variables and data types, you have the vocabulary. But arrays and objects are the grammar. Without them, you cannot write a single meaningful test in Playwright, Cypress, or even Selenium. I see this every week in code reviews: testers trying to store 47 form fields in separate variables instead of one object, or writing 12 nearly identical test cases because they do not know how to parameterize with an array.

Day 1 of this series covered variables and operators. Today we move to the structures that hold real data. By the end of this article, you will handle API responses, iterate over test data, and write cleaner assertions using modern ES6 syntax. This is not theory. Every example below is code I have written or reviewed in production test suites.

Playwright has 88,783 GitHub stars and over 213 million monthly npm downloads as of May 2026. The framework is built on TypeScript, which itself is a superset of JavaScript. If your JavaScript fundamentals are weak, every Playwright feature feels like magic. If they are solid, you read the source code and understand exactly why page.locator() behaves the way it does.

JavaScript Arrays: What QA Engineers Actually Need

Creating and accessing arrays

An array stores multiple values in one variable. In test automation, arrays hold lists of URLs, user roles, browser viewport sizes, or API endpoints.

const browsers = ['chromium', 'firefox', 'webkit'];
const viewports = [
  { width: 1280, height: 720 },
  { width: 390, height: 844 },
  { width: 1920, height: 1080 }
];

// Access by index
console.log(browsers[0]); // 'chromium'
console.log(viewports[1].width); // 390

Arrays are zero-indexed. The first element is at position 0, not 1. I have debugged tests where someone used users[1] expecting the first admin user, but the test data had the admin at index 0. Off-by-one errors in array indexing are a real source of flaky tests.

The methods that actually matter

JavaScript has over 30 array methods. For QA automation, you need six:

  • map() — transform every item and return a new array.
  • filter() — keep only items that match a condition.
  • find() — return the first matching item.
  • includes() — check if a value exists (returns true/false).
  • some() — check if at least one item matches.
  • every() — check if all items match.

Here is how I use them in a typical API test:

const response = await request.get('/api/users');
const users = await response.json();

// Extract just the emails for a duplicate check
const emails = users.map((user: any) => user.email);

// Find active users only
const activeUsers = users.filter((user: any) => user.isActive === true);

// Check if admin exists in the list
const hasAdmin = users.some((user: any) => user.role === 'admin');

// Verify every user has an ID
const allHaveIds = users.every((user: any) => user.id !== undefined);

// Find a specific test user
const testUser = users.find((user: any) => user.email === 'qa@test.com');

forEach() exists, but I avoid it in tests. It does not return a value, it cannot be broken early with break, and it encourages side effects. map(), filter(), and find() are declarative. They say what you want, not how to loop.

Array destructuring

ES6 destructuring lets you pull values out of an array in one line. This is cleaner than indexing when you know the structure:

const [firstBrowser, secondBrowser] = ['chromium', 'firefox', 'webkit'];
console.log(firstBrowser); // 'chromium'

// Skip the first element
const [, , thirdBrowser] = ['chromium', 'firefox', 'webkit'];
console.log(thirdBrowser); // 'webkit'

I use this when a function returns multiple values as an array, or when unpacking test configuration tuples.

Spread and rest with arrays

The spread operator ... copies array items into a new array. This is how I merge test data or add items without mutating the original:

const baseUsers = ['admin', 'editor'];
const allUsers = [...baseUsers, 'viewer', 'guest'];
// allUsers is ['admin', 'editor', 'viewer', 'guest']
// baseUsers is untouched

The rest syntax collects remaining items:

const [primary, ...others] = ['chromium', 'firefox', 'webkit'];
console.log(primary); // 'chromium'
console.log(others);  // ['firefox', 'webkit']

Spread is also how Playwright merges project configurations. Understanding this operator makes reading playwright.config.ts far easier.

JavaScript Objects: The Data Structure Behind Every API Response

Object basics for testers

Every JSON API response is an object. Every Playwright page.evaluate() return value is either a primitive or an object. If you do not understand objects, you cannot assert on API payloads.

const user = {
  id: 101,
  name: 'Pramod Dutta',
  role: 'sdet',
  isActive: true,
  address: {
    city: 'Bengaluru',
    country: 'India'
  }
};

// Access properties
console.log(user.name);           // 'Pramod Dutta'
console.log(user['role']);        // 'sdet'
console.log(user.address.city);   // 'Bengaluru'

Bracket notation user['role'] is useful when property names have spaces or special characters, or when the key is stored in a variable. Dot notation is cleaner when the key is a valid identifier.

Object.keys, Object.values, and Object.entries

These static methods convert an object into an array you can iterate:

  • Object.keys(obj) — returns an array of property names.
  • Object.values(obj) — returns an array of values.
  • Object.entries(obj) — returns an array of [key, value] pairs.

I use Object.entries heavily when validating that an API response contains expected fields:

const expectedFields = {
  id: 'number',
  name: 'string',
  email: 'string',
  isActive: 'boolean'
};

for (const [field, type] of Object.entries(expectedFields)) {
  expect(typeof user[field]).toBe(type);
}

This loop checks four fields in four lines. Without Object.entries, you would write four separate assertions. The maintenance difference becomes massive when your API has 20 fields.

Object destructuring

Destructuring objects is the single most useful ES6 feature for Playwright tests. It extracts only the properties you need:

const apiResponse = {
  status: 200,
  data: { id: 42, name: 'Test Product', price: 999 },
  headers: { 'content-type': 'application/json' }
};

// Extract nested properties
const { data: { id, price }, status } = apiResponse;
console.log(id);     // 42
console.log(price);  // 999
console.log(status); // 200

In Playwright, I destructure page, request, and browser from fixtures constantly:

test('checkout flow', async ({ page, request, browserName }) => {
  // page, request, and browserName are destructured from the test fixture object
  // No need to import or instantiate them manually
});

This is not a convenience. It is the pattern that makes Playwright’s fixture system work. If you do not understand object destructuring, fixture syntax looks like black magic.

Optional chaining and nullish coalescing

Optional chaining ?. stops execution if a property is null or undefined, instead of throwing an error:

const user = { profile: null };

// Old way: throws if profile is null
// const bio = user.profile.bio; // TypeError

// New way: returns undefined safely
const bio = user?.profile?.bio;
console.log(bio); // undefined

In API testing, responses often have missing optional fields. Optional chaining prevents your entire test from crashing because one nested property was absent.

Nullish coalescing ?? provides a fallback only for null or undefined, not for other falsy values like 0 or empty strings:

const timeout = config.timeout ?? 5000;
// If config.timeout is 0, it stays 0. If it is undefined, it becomes 5000.

This is critical for test configuration. A timeout of 0 is valid (no waiting), but a missing timeout should fall back to a default. The old || operator would override 0, which caused bugs in my early test suites.

ES6 Features That Change How You Write Test Code

Arrow functions and why they matter in tests

Arrow functions have a shorter syntax and do not rebind this. In test automation, the this behavior rarely matters, but the brevity does:

// Old function expression
users.filter(function(user) {
  return user.isActive;
});

// Arrow function
users.filter(user => user.isActive);

// Multiple parameters need parentheses
users.map((user, index) => ({ ...user, rank: index + 1 }));

When you pass a callback to page.waitForResponse() or Array.filter(), arrow functions reduce visual noise. Less noise means fewer bugs in review.

Template literals for dynamic selectors and URLs

Template literals use backticks and ${} interpolation. I use them for dynamic locators and API endpoints:

const userId = 42;
const apiUrl = `/api/users/${userId}`;
const selector = `[data-testid="user-${userId}"]`;

// Multi-line strings without concatenation
const payload = `{
  "name": "Test User",
  "role": "qa"
}`;

Before template literals, building a selector with a variable looked like '[data-testid="user-' + userId + '"]'. That string concatenation is error-prone. Template literals are readable and type-safe in TypeScript.

const and let: why var is dead

var has function scope and hoisting behavior that creates subtle bugs. let and const have block scope, which matches how most programmers actually think:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints 3, 3, 3 because var is hoisted to function scope

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints 0, 1, 2 because let is block-scoped

In test files, I default to const. If a variable must be reassigned, I use let. I have not written var in a test file in five years. Hiring managers notice this in code reviews. It signals you write modern JavaScript, not legacy copy-paste from 2012 Stack Overflow answers.

Promises and async/await

Playwright is entirely asynchronous. Every browser operation returns a Promise. async/await makes this readable:

// Promise chaining (harder to read)
page.goto('/login')
  .then(() => page.fill('#username', 'qa'))
  .then(() => page.fill('#password', 'secret'))
  .then(() => page.click('#submit'))
  .then(() => page.waitForURL('/dashboard'));

// async/await (linear, like synchronous code)
await page.goto('/login');
await page.fill('#username', 'qa');
await page.fill('#password', 'secret');
await page.click('#submit');
await page.waitForURL('/dashboard');

In API contract testing with Playwright, I often fire multiple requests in parallel using Promise.all():

const [usersRes, ordersRes] = await Promise.all([
  request.get('/api/users'),
  request.get('/api/orders')
]);

const users = await usersRes.json();
const orders = await ordersRes.json();

Promise.all() takes an array of promises and returns an array of results. If you do not understand arrays, you cannot use this pattern effectively.

Real-World Playwright Examples Using Arrays, Objects, and ES6

Parameterized tests with arrays

Playwright’s test.each() accepts an array of test cases. This is how I run the same flow across multiple user roles:

const roles = [
  { role: 'admin',   expectedTabs: ['Dashboard', 'Users', 'Settings'] },
  { role: 'editor',  expectedTabs: ['Dashboard', 'Content'] },
  { role: 'viewer',  expectedTabs: ['Dashboard'] },
];

for (const { role, expectedTabs } of roles) {
  test(`navigation for ${role}`, async ({ page }) => {
    await loginAs(page, role);
    const visibleTabs = await page.locator('.nav-item').allTextContents();
    expect(visibleTabs).toEqual(expectedTabs);
  });
}

This pattern replaces 30 lines of duplicated test code with 10 lines of data-driven logic. When product adds a new role, I add one object to the array. No new test function needed.

Processing API response objects

Here is a realistic assertion on a paginated API response:

const response = await request.get('/api/products?page=1');
const body = await response.json();

// Destructure pagination and data
const { page, totalPages, data: products } = body;

// Verify every product has required fields
const requiredKeys = ['id', 'name', 'price', 'sku'];
for (const product of products) {
  const missing = requiredKeys.filter(key => !(key in product));
  expect(missing).toEqual([]);
}

// Verify no duplicate SKUs using Set
const skus = products.map((p: any) => p.sku);
const uniqueSkus = [...new Set(skus)];
expect(uniqueSkus.length).toBe(skus.length);

Set is an ES6 collection that stores unique values. Spreading it back into an array [...new Set(skus)] is the fastest way to deduplicate in JavaScript.

Using map and filter for test data builders

I often generate dynamic test data from a base template:

const baseUser = { name: 'Test', role: 'viewer', isActive: true };

const testUsers = [
  { ...baseUser, name: 'Admin User', role: 'admin' },
  { ...baseUser, name: 'Inactive User', isActive: false },
  { ...baseUser, name: 'Editor User', role: 'editor' },
];

// Create all users via API in parallel
await Promise.all(
  testUsers.map(user => request.post('/api/users', { data: user }))
);

This example uses object spread, array map(), and Promise.all() together. These are not separate topics. They compose into patterns you will use every day.

Common Mistakes QA Engineers Make with Arrays and Objects

I have reviewed over 500 test files in the last two years. These are the array and object errors I see most often:

  1. Mutating shared test data. Pushing into a global array inside one test poisons every subsequent test. Always clone with spread or map() before modifying.
  2. Using == instead of ===. [1] == '1' is true in JavaScript because of type coercion. Always use strict equality in assertions.
  3. Forgetting that find() returns undefined. If your test data is wrong, users.find(u => u.role === 'superadmin') returns undefined, and then undefined.email throws a TypeError.
  4. Deep equality confusion. { a: 1 } === { a: 1 } is false because objects are compared by reference, not value. Use expect(obj).toEqual(expected) in Playwright, not toBe().
  5. Not checking array length before indexing. elements[0].click() crashes if the locator returned zero elements. Always assert .toHaveCount(n) first.

Each of these mistakes has caused a production bug in a project I worked on. They are not hypothetical. The find() returning undefined issue alone cost my team four hours of debugging last month because the test data seed script had changed the admin role name from 'admin' to 'super_admin' without updating the test.

Set and Map: The ES6 Collections Most Testers Miss

Arrays and objects are the workhorses, but ES6 introduced two specialized collections that solve specific test automation problems: Set and Map.

Set for uniqueness checks

A Set stores only unique values. This is the fastest way to check for duplicates in a list of IDs, emails, or SKUs:

const skus = products.map((p: any) => p.sku);
const uniqueSkus = new Set(skus);
expect(uniqueSkus.size).toBe(skus.length);

Before ES6, you would write a nested loop or use filter() with indexOf(). That is O(n²). Set deduplication is O(n). In a test suite processing 10,000 inventory records, that difference is measurable.

Map for ordered key-value pairs

A Map is like an object, but it preserves insertion order, accepts any type as a key (not just strings), and has a .size property. I use Maps when I need to cache test data by a numeric ID or a complex object key:

const userCache = new Map();
userCache.set(101, { name: 'Pramod', role: 'sdet' });
const user = userCache.get(101);
console.log(userCache.size); // 1

In large test suites, caching API responses in a Map prevents redundant network calls. Your tests run faster, and your staging server thanks you.

India Context: What Hiring Managers Expect in 2026

In 2026, JavaScript and TypeScript proficiency is no longer optional for Indian QA roles at product companies. I screen candidates regularly. The difference between a ₹6 LPA manual tester and a ₹18 LPA automation engineer is often whether they can destructure an API response and write a filter() callback without copying from Google.

Service companies like TCS and Infosys still train on basic Java, but product startups in Bengaluru, Hyderabad, and Pune expect ES6 fluency on day one. The 2026 QA career roadmap places JavaScript arrays and objects in month two, right after variables. Skip this foundation, and you will stall when the curriculum reaches Playwright fixtures and API intercepts.

When I interview SDET candidates, I ask them to write a function that takes an array of API responses and returns only the ones with HTTP 200 status. Candidates who use filter() and arrow functions pass. Candidates who write a for loop with an if block technically solve it, but they signal that their JavaScript knowledge stopped in 2015.

Key Takeaways

  • Arrays and objects are not advanced topics. They are the minimum viable skill for writing Playwright tests that handle real data.
  • Learn map(), filter(), find(), some(), and every(). They replace most loops in test automation.
  • Object destructuring and optional chaining make API assertions concise and safe against missing fields.
  • const and let replace var completely. Default to const unless reassignment is required.
  • Template literals, arrow functions, and async/await are not syntactic sugar. They reduce bugs by making intent explicit.

FAQ

Do I need to learn JavaScript arrays and objects before TypeScript?

Yes. TypeScript is a superset of JavaScript. Every TypeScript array is a JavaScript array at runtime. The types add safety, but the logic is pure JavaScript. Learn the runtime behavior first, then add types.

Why does Playwright use arrays for fixtures and projects?

Playwright’s configuration is object-based. The projects array defines browser targets, the use object holds defaults, and test fixtures return objects. If you cannot read nested object structures, you cannot customize Playwright behavior beyond copy-paste.

Is forEach() ever okay in tests?

It works, but I avoid it. forEach() ignores return values, does not support await cleanly in loops, and cannot be broken early. for...of or map() are almost always better.

How do I deep clone an object in a test?

For simple objects, const clone = { ...original } works. For nested objects, use JSON.parse(JSON.stringify(obj)) or a utility like lodash.cloneDeep. Never mutate shared test data.

What is the difference between toBe() and toEqual() in Playwright?

toBe() checks reference equality. toEqual() checks deep value equality. For objects and arrays, always use toEqual(). Use toBe() only for primitives like strings and numbers.

When should I use a Map instead of a plain object?

Use a Map when you need to preserve insertion order of keys, when keys are not strings (like numeric IDs or objects), or when you need to check the size dynamically with .size. For simple configuration objects and API responses, plain objects are fine and serialize to JSON more naturally.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.