The Testing Academy · Class Notes Tuesday, 11 August (IST)
Live class · study guide

JavaScript 2D arrays and the star patterns interviews love

Rows always come before columns, process.stdout.write is the trick that prints without a newline, and the left triangle, reverse triangle and pyramid all fall out of the same nested-loop shape once you draw the grid.

By Pramod Dutta, The Testing Academy. Study notes from the live JavaScript class, rebuilt from the session recording and the class notes deck. Code is reproduced as it was written on screen, with the output the class saw.

01

Rows and columns, before any code

A one-dimensional array is the array you already know: a flat collection of values in square brackets. A two-dimensional array adds a second direction, and everything in this class depends on keeping the two directions straight.

  • Rows run horizontally, columns run vertically. In a spreadsheet, the numbered lines are rows and the lettered ones are columns.
  • Rows come first, always. A selection described as 3 by 3 is 3 rows by 3 columns. An 11 by 2 selection is 11 rows and 2 columns, never the other way round.
  • [i][j] reads as row i, column j. The first index picks the row, the second picks the cell inside it.
  • A matrix is the special case where rows equal columns. 3 by 3 is a matrix. One row of four values is a perfectly good 2D array, but it is not a matrix.

The analogy from class: a spreadsheet named "Playwright 3x batch test data". Select a block and say its size out loud, rows first. It is also a preview of real work: a sheet like that one is probably the shape your Playwright test data takes later in the course.

Index positions for a 3 by 3 grid, the picture to hold in your head:

j, the column i, the row col 0col 1col 2 row 0 row 1 row 2 [0][0][0][1][0][2] [1][0][1][1][1][2] [2][0][2][1][2][2] grid[1][2], row first, then the column: 60
Rows before columns, always: [i][j] picks the row, then the cell inside it.
02

Creating and reading a 2D array

An array of arrays. Each inner array is one row:

JavaScript
let grid = [
  [10, 20, 30],
  [40, 50, 60],
  [70, 80, 90]
];                       // 3x3

grid[0][0];              // 10   row 0, column 0
grid[2][1];              // 80   row 2, column 1
grid[1][2];              // 60   row 1, column 2

grid[0][0] = 99;         // cells can be reassigned

The two lengths are different questions:

JavaScript
grid.length;             // 3   number of rows
grid[0].length;          // 3   number of columns in row 0

grid[grid.length - 1][grid[0].length - 1];   // 90, the last element

grid.length - 1 is 2, grid[0].length - 1 is 2, so the last-element expression is just grid[2][2] written without hardcoding the size. It leans on every row being the same length, which holds for this grid; a ragged grid would measure the last row's own length instead.

Mixed types are allowed in the same grid, with the same looseness one-dimensional JavaScript arrays have.

03

Printing without a newline

The pattern programs stand on one printing fact, so the class settled it first.

  • console.log always adds a newline. Every call ends the line.
  • process.stdout.write prints raw. No newline: output continues on the same line.

The first attempt at using it inside a loop failed with a real error:

Text
TypeError: The "chunk" argument must be of type string or an instance of Buffer

process.stdout.write does not take numbers. It wants a string (or a Buffer, as the error says), so handing it a grid cell that is a number throws. The fix is to concatenate with a string, and this loop is the corrected version the class landed on:

JavaScript
for (let i = 0; i < grid.length; i++) {
  for (let j = 0; j < grid[i].length; j++) {
    process.stdout.write(grid[i][j] + " ");   // number + string = string
  }
  console.log();   // row break
}

The pairing to remember: process.stdout.write for the cells, one empty console.log() after the inner loop for the row break. That combination prints a grid shaped like a grid, and it is exactly how the left triangle below runs. The other two patterns show the alternative: build the row in a string first, then print it once.

04

Walking a grid: three ways, one winner

The workhorse is two nested for loops: the outer loop picks the row, the inner loop walks the cells of that row.

JavaScript
let testMatrix = [
  ["login", "pass", 200],
  ["checkout", "fail", 404],
  ["search", "pass", 180]
];

for (let i = 0; i < testMatrix.length; i++) {
  for (let j = 0; j < testMatrix[i].length; j++) {
    process.stdout.write(testMatrix[i][j] + " ");
  }
  console.log();
}

The inner condition is j < testMatrix[i].length, the length of the current row, not a hardcoded 3. Rows are allowed to have different lengths, and this form keeps working when they do.

The dry run for i = 0: the inner loop runs j through 0, 1, 2 while i stays fixed, so login pass 200 prints on one line, then the empty console.log() breaks the row. Then i becomes 1 and the inner loop starts over.

outer loop: i picks the next row i < grid.length inner loop: j walks that row's cells j < grid[i].length print the cell stdout.write stays on the same line next cell, same row row done: console.log() breaks the line, next i one console.log() per finished row is what keeps the grid grid-shaped
The engine of every program on this page: outer loop owns rows, inner loop owns cells.

Two alternatives exist and were shown once:

JavaScript
for (let row of testMatrix) {
  for (let cell of row) {
    process.stdout.write(cell + " ");
  }
  console.log();
}

testMatrix.forEach(row => {
  row.forEach(cell => process.stdout.write(cell + " "));
  console.log();
});

Both work, neither needs lengths or indexes, and both are rarely used in practice. The indexed nested for is the one you will write and the one interviews expect.

05

Where a tester actually meets a 2D array

Two more grids from the class, both shapes you will see in test automation:

JavaScript
// scan a suite for failures
let suiteResults = [
  ["login-pass", "register-pass", "logout-pass"],   // Auth suite
  ["search-pass", "filter-fail", "sort-pass"],      // Search suite
  ["checkout-fail", "payment-fail", "confirm-pass"] // Payment suite
];

for (let i = 0; i < suiteResults.length; i++) {
  for (let j = 0; j < suiteResults[i].length; j++) {
    if (suiteResults[i][j].includes("fail")) {
      console.log(suiteResults[i][j]);
    }
  }
}
// filter-fail
// checkout-fail
// payment-fail   (one per line, console.log breaks each)
JavaScript
// execution times per environment, a 3x4
let execTimes = [
  [120, 340, 89, 450],    // dev
  [200, 410, 100, 520],   // staging
  [180, 390, 95, 490]     // prod
];

Rows are suites or environments, and columns are test cases. The nested loop plus an if prints every failing case in a handful of lines.

06

Row sums with map and reduce

One functional-style trick made the cut, because it turns up in real code:

JavaScript
let scores = [
  [85, 90, 78],   // student 0
  [60, 45, 70],   // student 1
  [95, 88, 92]    // student 2
];

let rowSums = scores.map(row => row.reduce((a, b) => a + b, 0));
console.log(rowSums);   // [253, 175, 275]

How to read it: map hands the callback one whole row at a time, not one number. Inside, reduce collapses that row by carrying a running total: the callback receives the total so far and the next value, starting from 0. Three rows in, three sums out.

You do not pass three arguments for three elements. reduce walks the row itself: for the first row, 0 plus 85, then 85 plus 90, then 175 plus 78, giving 253.

The same result is a nested loop with an accumulator. The class point was narrower: recognise this shape when you meet it in someone else's framework, then write whichever version you find clearer.

07

The star patterns: why they matter

Three programs, and the reason they get the biggest slice of the class: the left triangle, reverse triangle and pyramid are standing SDET interview questions, asked of automation testers and manual testers alike. They test whether you can drive nested loops and see a grid, which is exactly the skill that walking a web table's rows and cells asks for later.

The method for all three is the same and it was repeated throughout the class: draw the pattern as a grid first. Number the rows, number the columns, mark where the stars sit, and read the rule off the drawing. The code falls out afterwards.

A learner asked why testers get programming questions at all. The answer from class: being a tester does not exempt you from code. Interviewers need people who can write it and test it, so the patterns stay in the loop.

08

Pattern 1: the left triangle

Target, for n = 3:

Text
*
**
***

Draw it on the grid and the rule appears: a star sits wherever the column number is less than or equal to the row number, counting both from 1. Row 1 has 1 star, row 2 has 2, row i has i.

JavaScript
let n = 3;

for (let i = 1; i <= n; i++) {
  for (let j = 1; j <= i; j++) {
    process.stdout.write("*");
  }
  console.log();
}

The inner loop's ceiling is i, not n. That single character is the entire pattern: row 1 lets j reach 1, row 2 lets it reach 2, and the triangle grows a star per row. The empty console.log() supplies the row break, and process.stdout.write keeps the stars of one row on one line.

The trace, one row per line:

i j runs prints
1 1 *
2 1 to 2 **
3 1 to 3 ***

Change n to 4 or 5 and the triangle simply grows. Nothing else changes.

Start both loops from 1 or both from 0; the pair has to move together. One-based pairs with i <= n, zero-based pairs with i < n and let j = 0 with j <= i still intact. Flip only the outer loop and the first row goes blank. This off-by-one is the most common way the pattern breaks in an interview.

09

Pattern 2: the reverse triangle

Target, for n = 5:

Text
*****
****
***
**
*

Same grid, opposite direction. The first row wants all 5 stars, so run the outer loop backwards:

JavaScript
let n = 5;

for (let i = n; i >= 1; i--) {
  let row = "";
  for (let j = 1; j <= i; j++) {
    row += "*";
  }
  console.log(row);
}

i starts at n and walks down, j still climbs from 1 to i. First pass: i is 5, j runs 1 to 5, five stars. Next pass i is 4, four stars. The condition j <= i is unchanged from the left triangle; only the outer loop's direction flipped.

This version also shows the second printing style: build the row in a string with row += "*", then console.log(row) once per row. An accumulator string and process.stdout.write are both correct, and the class used both deliberately.

10

Pattern 3: the pyramid

This is the one described in class, repeatedly and with affection, as the nightmare program.

Target, for n = 3:

Text
  *
 ***
*****

Two observations unlock it. Each row is spaces, then stars. And the star counts are the odd numbers: 1, 3, 5.

So the class solution gives every row two inner loops: one printing the leading spaces, one printing the stars.

row i spaces: n - i stars: 2 * i - 1
1 2 1
2 1 3
3 0 5
row 1row 2row 3 * *** ***** 2 spaces, 1 star 1 space, 3 stars 0 spaces, 5 stars n - i = 2, 2*i - 1 = 1 n - i = 1, 2*i - 1 = 3 n - i = 0, 2*i - 1 = 5 grey cells are the spaces loop, coral cells are the stars loop: two inner loops, one row each pass
The pyramid on the grid: spaces shrink by n - i, stars grow by 2 * i - 1.
JavaScript
let n = 3;

for (let i = 1; i <= n; i++) {
  let row = "";
  for (let j = 1; j <= n - i; j++) {
    row += " ";
  }
  for (let j = 1; j <= 2 * i - 1; j++) {
    row += "*";
  }
  console.log(row);
}

The dry run for row 1: i is 1, the spaces loop runs j from 1 to 2 and adds two spaces, the stars loop runs 1 to 1 and adds one star. Row prints as two spaces and a star. For row 2: one space, then 2 * 2 - 1 is 3 stars. For row 3: 3 - 3 is 0, the spaces loop never runs, and 5 stars fill the line.

Reusing j for both inner loops is fine. Each let j is a fresh variable scoped to its own loop.

The two formulas are the whole trick, and they come straight off the drawing. Spaces shrink by one per row, so n - i. Stars are the odd numbers, and the i-th odd number is 2 * i - 1. If the pyramid confuses you, build the table above before touching the code. The class watched it work in a step-by-step visualisation, and the table is that visualisation on paper.

Printing patterns this way, watching intermediate output line by line, is manual debugging, and it is the honest tool while VS Code's JavaScript debugger setup stays clunky with Code Runner. A proper debugging walkthrough was promised as its own tutorial.

11

Tasks and announcements

  • Today's task, mandatory: all three pattern programs, written by you, pushed to your GitHub repository.
  • Then practice 2 to 3 more with your own logic; a reverse pyramid is a good candidate. Two or three beyond the class set are enough, so do not disappear into pattern-golf.
  • Re-watch over copy-typing. If the pyramid did not land live, re-watch the recording with the trace tables beside you. Several in the batch are meeting matrices for the first time, and that is expected.
  • Doubt thread for this session is up; put questions there.
  • Tonight, 8:00 PM IST: Claude 101 part two, the final class, with certification.
  • Wednesday, 8:00 PM IST: the Skills masterclass.
  • Coming next in JavaScript: callbacks, promises, and async versus sync.
  • Last class's code is being pushed to the class repository with an updated README.

Interview forms of today's material: print the left triangle for a given n. Flip it to the reverse triangle by changing one loop. Explain why the class pyramid uses two inner loops, and name the two formulas. If you can answer the third one from the table alone, the topic is yours.