Skip to content

Top 70 QA/SDET Coding Interview Questions

A comprehensive collection of coding interview questions for QA Engineers, SDETs, and Automation Engineers. Sourced from real interviews at Amazon, Google, Microsoft, and other top tech companies.


Table of Contents

  1. Part 1: JavaScript Fundamentals (Q1-12)
  2. Part 2: JavaScript Concepts (Q13-22)
  3. Part 3: Array & Object Manipulation (Q23-32)
  4. Part 4: Data Structures & Algorithms (Q33-40)
  5. Part 5: Testing & Automation Concepts (Q41-50)
  6. Part 6: Playwright JavaScript/TypeScript (Q51-70)

Part 1: JavaScript Fundamentals

1. Reverse a String

Problem: Reverse a given string without using built-in reverse methods.

Example: - Input: "hello" → Output: "olleh"

Solutions

function reverseString(str) {
  return str.split('').reverse().join('');
}
function reverseStringLoop(str) {
  let reversed = '';
  for (let i = str.length - 1; i >= 0; i--) {
    reversed += str[i];
  }
  return reversed;
}
function reverseStringReduce(str) {
  return str.split('').reduce((rev, char) => char + rev, '');
}

Interview Follow-up

What's the time complexity? → O(n) for all methods since we iterate through the string once.

Try it yourself

// Implement your own reverse string function!
function reverseString(str) {
  // Your code here

}

// Test your solution:
console.log(reverseString("hello"));    // Expected: "olleh"
console.log(reverseString("JavaScript")); // Expected: "tpircSavaJ"

2. Reverse a Number

Problem: Reverse an integer number, handling negative numbers.

Example: - Input: 1234 → Output: 4321 - Input: -123 → Output: -321

Solutions

function reverseNumber(num) {
  const sign = Math.sign(num);
  const reversed = parseInt(
    Math.abs(num).toString().split('').reverse().join('')
  );
  return sign * reversed;
}
function reverseNumberMath(num) {
  const sign = Math.sign(num);
  num = Math.abs(num);
  let reversed = 0;
  while (num > 0) {
    reversed = reversed * 10 + (num % 10);
    num = Math.floor(num / 10);
  }
  return sign * reversed;
}

Complexity

Time: O(log n) - number of digits | Space: O(1)

Try it yourself

// Implement reverse number (handle negatives!)
function reverseNumber(num) {
  // Your code here

}

// Test your solution:
console.log(reverseNumber(1234));   // Expected: 4321
console.log(reverseNumber(-567));   // Expected: -765
console.log(reverseNumber(100));    // Expected: 1

3. Check if String is Palindrome

Problem: Check whether a string reads the same forwards and backwards.

Example: - "madam"true - "hello"false

Solutions

function isPalindrome(str) {
  const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '');
  return cleaned === cleaned.split('').reverse().join('');
}
function isPalindromeTwoPointers(str) {
  const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '');
  let left = 0;
  let right = cleaned.length - 1;

  while (left < right) {
    if (cleaned[left] !== cleaned[right]) {
      return false;
    }
    left++;
    right--;
  }
  return true;
}

Pro Tip

The two-pointer approach is preferred in interviews as it shows algorithmic thinking and uses O(1) extra space (excluding the cleaned string).

Try it yourself

// Check if a string is a palindrome
function isPalindrome(str) {
  // Your code here

}

// Test your solution:
console.log(isPalindrome("madam"));        // Expected: true
console.log(isPalindrome("hello"));        // Expected: false
console.log(isPalindrome("A man a plan a canal Panama")); // Expected: true

4. Find Duplicate Elements in Array

Problem: Find all duplicate values in an array.

Example: - Input: [1, 2, 3, 2, 4, 1, 5] → Output: [1, 2]

Solutions

function findDuplicates(arr) {
  const freq = {};
  const duplicates = [];

  for (const num of arr) {
    freq[num] = (freq[num] || 0) + 1;
    if (freq[num] === 2) {
      duplicates.push(num);
    }
  }
  return duplicates;
}
function findDuplicatesSet(arr) {
  const seen = new Set();
  const duplicates = new Set();

  for (const num of arr) {
    if (seen.has(num)) {
      duplicates.add(num);
    }
    seen.add(num);
  }
  return [...duplicates];
}
function findDuplicatesFilter(arr) {
  return arr.filter((item, index) => arr.indexOf(item) !== index);
}

Edge Cases

  • Empty array → return empty array
  • Array with all unique elements → return empty array
  • Method 3 (filter) has O(n²) complexity - avoid for large arrays

Try it yourself

// Find all duplicate elements in an array
function findDuplicates(arr) {
  // Your code here

}

// Test your solution:
console.log(findDuplicates([1, 2, 3, 2, 4, 1, 5])); // Expected: [2, 1]
console.log(findDuplicates([1, 2, 3, 4, 5]));       // Expected: []

5. Find Missing Number in Array (1 to N)

Problem: Given an array containing n-1 numbers from 1 to N with one missing, find it.

Example: - Input: [1, 2, 4, 5, 6] (N=6) → Output: 3

Solutions

function findMissingNumber(arr, n) {
  const expectedSum = (n * (n + 1)) / 2;
  const actualSum = arr.reduce((sum, num) => sum + num, 0);
  return expectedSum - actualSum;
}
function findMissingNumberXOR(arr, n) {
  let xor1 = 0;
  let xor2 = 0;

  for (let i = 1; i <= n; i++) {
    xor1 ^= i;
  }
  for (const num of arr) {
    xor2 ^= num;
  }
  return xor1 ^ xor2;
}
function findMissingNumberSet(arr, n) {
  const set = new Set(arr);
  for (let i = 1; i <= n; i++) {
    if (!set.has(i)) return i;
  }
}

Pro Tip

The math formula is cleanest but can overflow with very large numbers. XOR approach avoids overflow and is a classic interview trick worth knowing.

Complexity

All methods: Time O(n) | Math: Space O(1), XOR: Space O(1), Set: Space O(n)

Try it yourself

// Find the missing number in array from 1 to N
function findMissingNumber(arr, n) {
  // Your code here (hint: use math formula)

}

// Test your solution:
console.log(findMissingNumber([1, 2, 4, 5, 6], 6)); // Expected: 3
console.log(findMissingNumber([1, 2, 3, 5], 5));    // Expected: 4
console.log(findMissingNumber([2, 3, 4, 5], 5));    // Expected: 1

6. Find Max/Min in Array (without Math.max/min)

Problem: Find the largest and smallest number without using built-in methods.

Example: - Input: [3, 1, 4, 1, 5, 9, 2, 6] → Output: { max: 9, min: 1 }

Solutions

function findMaxMin(arr) {
  if (arr.length === 0) return null;

  let max = arr[0];
  let min = arr[0];

  for (let i = 1; i < arr.length; i++) {
    if (arr[i] > max) max = arr[i];
    if (arr[i] < min) min = arr[i];
  }

  return { max, min };
}
function findMaxMinReduce(arr) {
  return arr.reduce(
    (acc, curr) => ({
      max: curr > acc.max ? curr : acc.max,
      min: curr < acc.min ? curr : acc.min
    }),
    { max: arr[0], min: arr[0] }
  );
}

Complexity

Time: O(n) - single pass | Space: O(1)

Try it yourself

// Find max and min without using Math.max/min
function findMaxMin(arr) {
  // Your code here

}

// Test your solution:
console.log(findMaxMin([3, 1, 4, 1, 5, 9, 2, 6])); // Expected: {max: 9, min: 1}
console.log(findMaxMin([-5, 0, 10, -3]));          // Expected: {max: 10, min: -5}

7. Two Sum Problem

Problem: Find two numbers in an array that add up to a target sum. Return their indices.

Example: - Input: nums = [2, 7, 11, 15], target = 9 → Output: [0, 1]

Solutions

// O(n) time, O(n) space
function twoSum(nums, target) {
  const map = new Map();

  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];
    if (map.has(complement)) {
      return [map.get(complement), i];
    }
    map.set(nums[i], i);
  }
  return [];
}
// O(n²) time, O(1) space
function twoSumBruteForce(nums, target) {
  for (let i = 0; i < nums.length; i++) {
    for (let j = i + 1; j < nums.length; j++) {
      if (nums[i] + nums[j] === target) {
        return [i, j];
      }
    }
  }
  return [];
}

Interview Follow-up

What if the array is sorted? → Use two pointers approach: start with pointers at both ends, move inward based on sum comparison. This achieves O(n) time with O(1) space.

Try it yourself

// Find two numbers that add up to target, return indices
function twoSum(nums, target) {
  // Your code here (hint: use a Map)

}

// Test your solution:
console.log(twoSum([2, 7, 11, 15], 9));  // Expected: [0, 1]
console.log(twoSum([3, 2, 4], 6));        // Expected: [1, 2]
console.log(twoSum([3, 3], 6));           // Expected: [0, 1]

8. Remove Duplicates from Array

Problem: Remove duplicate elements from an array.

Example: - Input: [1, 2, 2, 3, 4, 4, 5] → Output: [1, 2, 3, 4, 5]

Solutions

function removeDuplicates(arr) {
  return [...new Set(arr)];
}
function removeDuplicatesFilter(arr) {
  return arr.filter((item, index) => arr.indexOf(item) === index);
}
function removeDuplicatesReduce(arr) {
  return arr.reduce((unique, item) => {
    return unique.includes(item) ? unique : [...unique, item];
  }, []);
}
// Interview favorite - O(1) extra space
function removeDuplicatesSorted(arr) {
  if (arr.length === 0) return 0;
  let writeIndex = 1;

  for (let i = 1; i < arr.length; i++) {
    if (arr[i] !== arr[i - 1]) {
      arr[writeIndex] = arr[i];
      writeIndex++;
    }
  }
  return writeIndex; // Returns new length
}

Pro Tip

Method 4 (in-place for sorted arrays) is commonly asked in interviews because it demonstrates understanding of the two-pointer technique and space optimization.

Try it yourself

// Remove duplicates from an array
function removeDuplicates(arr) {
  // Your code here (try using Set!)

}

// Test your solution:
console.log(removeDuplicates([1, 2, 2, 3, 4, 4, 5])); // Expected: [1, 2, 3, 4, 5]
console.log(removeDuplicates([1, 1, 1, 1]));          // Expected: [1]
console.log(removeDuplicates(['a', 'b', 'a', 'c']));  // Expected: ['a', 'b', 'c']

9. Find First Non-Repeating Character

Problem: Find the first character in a string that doesn't repeat.

Example: - Input: "aabbcdd" → Output: "c" - Input: "aabb" → Output: null

Solutions

function firstNonRepeating(str) {
  const charCount = new Map();

  for (const char of str) {
    charCount.set(char, (charCount.get(char) || 0) + 1);
  }

  for (const char of str) {
    if (charCount.get(char) === 1) {
      return char;
    }
  }
  return null;
}
function firstNonRepeatingIndex(str) {
  for (let i = 0; i < str.length; i++) {
    if (str.indexOf(str[i]) === str.lastIndexOf(str[i])) {
      return str[i];
    }
  }
  return null;
}

Complexity

Method 1: Time O(n), Space O(k) where k = unique chars | Method 2: Time O(n²), Space O(1)

Try it yourself

// Find the first non-repeating character
function firstNonRepeating(str) {
  // Your code here

}

// Test your solution:
console.log(firstNonRepeating("aabbcdd"));    // Expected: "c"
console.log(firstNonRepeating("aabb"));       // Expected: null
console.log(firstNonRepeating("javascript")); // Expected: "j"

10. Check if Two Strings are Anagrams

Problem: Check if two strings contain the same characters in different order.

Example: - "listen" and "silent"true - "hello" and "world"false

Solutions

function areAnagrams(str1, str2) {
  if (str1.length !== str2.length) return false;

  const sort = (s) => s.toLowerCase().split('').sort().join('');
  return sort(str1) === sort(str2);
}
function areAnagramsFrequency(str1, str2) {
  if (str1.length !== str2.length) return false;

  const freq = {};

  for (const char of str1.toLowerCase()) {
    freq[char] = (freq[char] || 0) + 1;
  }

  for (const char of str2.toLowerCase()) {
    if (!freq[char]) return false;
    freq[char]--;
  }

  return true;
}

Complexity

Method 1: Time O(n log n) due to sorting | Method 2: Time O(n), Space O(k) - preferred in interviews

Try it yourself

// Check if two strings are anagrams
function areAnagrams(str1, str2) {
  // Your code here

}

// Test your solution:
console.log(areAnagrams("listen", "silent")); // Expected: true
console.log(areAnagrams("hello", "world"));   // Expected: false
console.log(areAnagrams("Debit Card", "Bad Credit")); // Expected: true

11. Longest Common Prefix

Problem: Find the longest common prefix among an array of strings.

Example: - Input: ["flower", "flow", "flight"] → Output: "fl" - Input: ["dog", "car", "race"] → Output: ""

Solutions

function longestCommonPrefix(strs) {
  if (strs.length === 0) return '';

  let prefix = strs[0];

  for (let i = 1; i < strs.length; i++) {
    while (strs[i].indexOf(prefix) !== 0) {
      prefix = prefix.slice(0, -1);
      if (prefix === '') return '';
    }
  }
  return prefix;
}
function longestCommonPrefixVertical(strs) {
  if (strs.length === 0) return '';

  for (let i = 0; i < strs[0].length; i++) {
    const char = strs[0][i];
    for (let j = 1; j < strs.length; j++) {
      if (i >= strs[j].length || strs[j][i] !== char) {
        return strs[0].slice(0, i);
      }
    }
  }
  return strs[0];
}

Complexity

Time: O(S) where S = sum of all characters | Space: O(1)

Try it yourself

// Find the longest common prefix
function longestCommonPrefix(strs) {
  // Your code here

}

// Test your solution:
console.log(longestCommonPrefix(["flower", "flow", "flight"])); // Expected: "fl"
console.log(longestCommonPrefix(["dog", "car", "race"]));       // Expected: ""
console.log(longestCommonPrefix(["test", "testing", "tested"])); // Expected: "test"

12. Valid Parentheses (Balanced Brackets)

Problem: Check if a string of brackets is balanced.

Example: - "()[]{}"true - "([)]"false - "{[]}"true

Solutions

function isValidParentheses(s) {
  const stack = [];
  const pairs = {
    ')': '(',
    '}': '{',
    ']': '['
  };

  for (const char of s) {
    if (char === '(' || char === '{' || char === '[') {
      stack.push(char);
    } else if (char === ')' || char === '}' || char === ']') {
      if (stack.length === 0 || stack.pop() !== pairs[char]) {
        return false;
      }
    }
  }

  return stack.length === 0;
}
function isValidParenthesesMap(s) {
  const stack = [];
  const map = new Map([
    [')', '('],
    ['}', '{'],
    [']', '[']
  ]);

  for (const char of s) {
    if (!map.has(char)) {
      stack.push(char);
    } else if (stack.pop() !== map.get(char)) {
      return false;
    }
  }

  return stack.length === 0;
}

Pro Tip

This is a classic stack problem. The key insight is that valid brackets must close in reverse order of opening - exactly what a stack provides.

Complexity

Time: O(n) | Space: O(n) worst case for stack

Try it yourself

// Check if parentheses are balanced
function isValidParentheses(s) {
  // Your code here (hint: use a stack)

}

// Test your solution:
console.log(isValidParentheses("()[]{}"));  // Expected: true
console.log(isValidParentheses("([)]"));    // Expected: false
console.log(isValidParentheses("{[]}"));    // Expected: true

Part 2: JavaScript Concepts

13. var vs let vs const

Question: Explain the differences between var, let, and const.

Answer:

Feature var let const
Scope Function-scoped Block-scoped Block-scoped
Hoisting Yes (initialized as undefined) Yes (TDZ) Yes (TDZ)
Re-declaration Allowed Not allowed Not allowed
Re-assignment Allowed Allowed Not allowed

Code Examples

// Scope difference
function scopeDemo() {
  if (true) {
    var x = 1;    // Function-scoped
    let y = 2;    // Block-scoped
    const z = 3;  // Block-scoped
  }
  console.log(x); // 1
  console.log(y); // ReferenceError
  console.log(z); // ReferenceError
}

// Hoisting difference
console.log(a); // undefined (hoisted)
console.log(b); // ReferenceError (TDZ)
var a = 1;
let b = 2;

// const with objects (reference is constant, not value)
const obj = { name: 'John' };
obj.name = 'Jane';  // Allowed
obj = {};           // TypeError

14. == vs === (Type Coercion)

Question: What's the difference between == and ===?

Answer: - == (loose equality): Compares values after type coercion - === (strict equality): Compares values AND types without coercion

Examples

// Loose equality (==) - Type coercion happens
0 == '0'           // true (string '0' coerced to number)
0 == false         // true (false coerced to 0)
null == undefined  // true (special case)
'' == false        // true
[] == false        // true
[] == 0            // true

// Strict equality (===) - No coercion
0 === '0'          // false
0 === false        // false
null === undefined // false
'' === false       // false

// Best Practice: Always use === unless you explicitly need coercion

Pro Tip

Always explain why === is preferred for predictable behavior. This shows you understand the subtle bugs loose equality can introduce.


15. Hoisting & Temporal Dead Zone

Question: Explain hoisting and the Temporal Dead Zone (TDZ).

Answer: - Hoisting: JavaScript moves declarations to the top of their scope during compilation - TDZ: The zone between entering scope and variable initialization where let/const cannot be accessed

Code Examples

// var hoisting
console.log(x); // undefined (declaration hoisted, not initialization)
var x = 5;

// let/const TDZ
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;     // TDZ ends here

// Function hoisting (fully hoisted)
greet(); // "Hello!" - works
function greet() {
  console.log("Hello!");
}

// Function expression (not hoisted)
sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
  console.log("Hi!");
};

16. Closures with Examples

Question: Explain closures with practical examples.

Answer: A closure is a function that remembers its outer variables and can access them even after the outer function has returned.

Code Examples

// Basic closure
function outer() {
  let count = 0;
  return function inner() {
    count++;
    return count;
  };
}

const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

// Practical example: Private variables
function createBankAccount(initialBalance) {
  let balance = initialBalance; // Private variable

  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error('Insufficient funds');
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    }
  };
}

const account = createBankAccount(100);
account.deposit(50);  // 150
account.withdraw(30); // 120
// account.balance is not accessible directly (private)

// Closure in loops (common interview question)
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3, 3, 3
}

// Fix with let (creates new scope each iteration)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 0, 1, 2
}

// Fix with IIFE (Immediately Invoked Function Expression)
for (var i = 0; i < 3; i++) {
  ((j) => {
    setTimeout(() => console.log(j), 100);
  })(i); // 0, 1, 2
}

17. setTimeout Output Question

Question: What will be the output?

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}

Answer: 3, 3, 3 (all print 3 after 1 second)

Explanation: - var is function-scoped, so there's only one i variable - By the time setTimeout callbacks execute, the loop has completed and i is 3 - All callbacks reference the same i

Solutions

// Solution 1: Use let (block-scoped)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}
// Output: 0, 1, 2

// Solution 2: IIFE (creates new scope)
for (var i = 0; i < 3; i++) {
  ((j) => {
    setTimeout(() => console.log(j), 1000);
  })(i);
}

// Solution 3: setTimeout third parameter
for (var i = 0; i < 3; i++) {
  setTimeout((j) => console.log(j), 1000, i);
}

18. Promise vs async/await

Question: Explain Promises and async/await with conversions.

Answer:

Code Examples

// Promise-based approach
function fetchUserPromise(id) {
  return fetch(`/api/users/${id}`)
    .then(response => response.json())
    .then(user => {
      console.log(user);
      return user;
    })
    .catch(error => {
      console.error('Error:', error);
      throw error;
    });
}

// async/await approach (cleaner syntax)
async function fetchUserAsync(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    console.log(user);
    return user;
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

// Promise methods
Promise.all([p1, p2, p3]);     // Wait for all, fail if any fails
Promise.allSettled([p1, p2]);  // Wait for all, get all results
Promise.race([p1, p2]);        // First to settle wins
Promise.any([p1, p2]);         // First to fulfill wins

// Sequential vs Parallel execution
// Sequential (slower)
async function sequential() {
  const user = await getUser();
  const posts = await getPosts(); // Waits for user first
  return { user, posts };
}

// Parallel (faster)
async function parallel() {
  const [user, posts] = await Promise.all([
    getUser(),
    getPosts()
  ]);
  return { user, posts };
}

19. JavaScript Event Loop

Question: Explain the JavaScript Event Loop.

Answer:

┌───────────────────────────┐
│        Call Stack         │ ← Executes synchronous code
└───────────────────────────┘
┌───────────────────────────┐
│      Web APIs / Node      │ ← setTimeout, fetch, DOM events
└───────────────────────────┘
┌───────────────────────────┐
│    Microtask Queue        │ ← Promises, queueMicrotask
│    (Higher Priority)      │
└───────────────────────────┘
┌───────────────────────────┐
│    Macrotask Queue        │ ← setTimeout, setInterval, I/O
│    (Callback Queue)       │
└───────────────────────────┘

Classic Interview Question

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

// Output: 1, 4, 3, 2
// Explanation:
// 1. '1' - synchronous, runs immediately
// 2. setTimeout callback goes to macrotask queue
// 3. Promise.then goes to microtask queue
// 4. '4' - synchronous, runs immediately
// 5. Call stack empty, check microtask queue → '3'
// 6. Microtask queue empty, check macrotask queue → '2'

20. Deep Copy vs Shallow Copy

Question: Explain the difference with implementation examples.

Answer:

Code Examples

// Shallow Copy - copies only first level, nested objects are referenced
const original = { a: 1, nested: { b: 2 } };

// Shallow copy methods
const shallow1 = { ...original };
const shallow2 = Object.assign({}, original);

shallow1.nested.b = 999;
console.log(original.nested.b); // 999 (affected!)

// Deep Copy - copies all levels
const original2 = { a: 1, nested: { b: 2 } };

// Method 1: JSON (simple but has limitations)
const deep1 = JSON.parse(JSON.stringify(original2));
// Limitations: loses functions, undefined, Date becomes string, circular refs fail

// Method 2: structuredClone (modern, recommended)
const deep2 = structuredClone(original2);

// Method 3: Custom recursive function
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  if (Array.isArray(obj)) {
    return obj.map(item => deepClone(item));
  }

  const cloned = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloned[key] = deepClone(obj[key]);
    }
  }
  return cloned;
}

deep2.nested.b = 999;
console.log(original2.nested.b); // 2 (not affected)

21. this Keyword Behavior

Question: Explain how this works in different contexts.

Answer:

Code Examples

// 1. Global context
console.log(this); // window (browser) or global (Node)

// 2. Object method - this = object
const obj = {
  name: 'John',
  greet() {
    console.log(this.name); // 'John'
  }
};

// 3. Regular function - this = undefined (strict) or global
function regularFunc() {
  console.log(this); // undefined in strict mode
}

// 4. Arrow function - this = lexically inherited (from enclosing scope)
const obj2 = {
  name: 'Jane',
  greet: () => {
    console.log(this.name); // undefined (inherits from outer scope)
  },
  greetCorrect() {
    const arrow = () => console.log(this.name);
    arrow(); // 'Jane' (inherits from greetCorrect)
  }
};

// 5. Event handler - this = element that received event
button.addEventListener('click', function() {
  console.log(this); // button element
});

// 6. Explicit binding - call, apply, bind
function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}

const person = { name: 'Alice' };

greet.call(person, 'Hello');    // 'Hello, Alice'
greet.apply(person, ['Hi']);    // 'Hi, Alice'
const boundGreet = greet.bind(person);
boundGreet('Hey');              // 'Hey, Alice'

22. Prototype & Prototypal Inheritance

Question: Explain prototypes and how inheritance works in JavaScript.

Answer:

Code Examples

// Every object has a prototype chain
const arr = [1, 2, 3];
// arr → Array.prototype → Object.prototype → null

// Constructor function pattern
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  console.log(`${this.name} makes a sound`);
};

function Dog(name, breed) {
  Animal.call(this, name); // Call parent constructor
  this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log(`${this.name} barks!`);
};

const dog = new Dog('Rex', 'German Shepherd');
dog.speak(); // 'Rex makes a sound' (inherited)
dog.bark();  // 'Rex barks!'

// ES6 Class syntax (syntactic sugar over prototypes)
class AnimalClass {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class DogClass extends AnimalClass {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  bark() {
    console.log(`${this.name} barks!`);
  }
}

// Checking prototype chain
console.log(dog instanceof Dog);    // true
console.log(dog instanceof Animal); // true
console.log(Dog.prototype.isPrototypeOf(dog)); // true

Part 3: Array & Object Manipulation

23. Flatten Nested Array

Problem: Convert a nested array into a single-level array.

Example: - Input: [1, [2, [3, [4]], 5]] → Output: [1, 2, 3, 4, 5]

Solutions

function flattenArray(arr) {
  return arr.flat(Infinity);
}
function flattenRecursive(arr) {
  const result = [];

  function flatten(items) {
    for (const item of items) {
      if (Array.isArray(item)) {
        flatten(item);
      } else {
        result.push(item);
      }
    }
  }

  flatten(arr);
  return result;
}
function flattenReduce(arr) {
  return arr.reduce((flat, item) => {
    return flat.concat(Array.isArray(item) ? flattenReduce(item) : item);
  }, []);
}
function flattenStack(arr) {
  const stack = [...arr];
  const result = [];

  while (stack.length) {
    const item = stack.pop();
    if (Array.isArray(item)) {
      stack.push(...item);
    } else {
      result.unshift(item);
    }
  }
  return result;
}

Interview Follow-up

Why use iterative over recursive? → Avoids stack overflow for deeply nested arrays. Interviewers often ask about trade-offs between recursive and iterative approaches.

Try it yourself

// Flatten a nested array of any depth
function flattenArray(arr) {
  // Your code here

}

// Test your solution:
console.log(flattenArray([1, [2, [3, [4]], 5]])); // Expected: [1, 2, 3, 4, 5]
console.log(flattenArray([[1, 2], [3, [4, 5]]])); // Expected: [1, 2, 3, 4, 5]

24. Array map vs forEach vs filter vs reduce

Question: Explain the differences and use cases.

Answer:

Code Examples

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

// forEach - executes function for each element, returns undefined
// Use: side effects (logging, DOM manipulation)
numbers.forEach((num, index) => {
  console.log(`Index ${index}: ${num}`);
});
// Returns: undefined

// map - transforms each element, returns new array
// Use: transforming data
const doubled = numbers.map(num => num * 2);
// Returns: [2, 4, 6, 8, 10]

// filter - keeps elements that pass test, returns new array
// Use: filtering data based on condition
const evens = numbers.filter(num => num % 2 === 0);
// Returns: [2, 4]

// reduce - accumulates values into single result
// Use: summing, grouping, transforming to different structure
const sum = numbers.reduce((acc, num) => acc + num, 0);
// Returns: 15

// Chaining example
const result = numbers
  .filter(num => num > 2)      // [3, 4, 5]
  .map(num => num * 2)         // [6, 8, 10]
  .reduce((sum, n) => sum + n, 0); // 24

// Key differences:
// | Method  | Returns     | Mutates | Use Case           |
// |---------|-------------|---------|-------------------|
// | forEach | undefined   | No      | Side effects      |
// | map     | New array   | No      | Transform data    |
// | filter  | New array   | No      | Select subset     |
// | reduce  | Any value   | No      | Accumulate/aggregate |

25. Merge Two Sorted Arrays

Problem: Merge two sorted arrays into one sorted array.

Example: - Input: [1, 3, 5] and [2, 4, 6] → Output: [1, 2, 3, 4, 5, 6]

Solutions

// O(n + m) time, O(n + m) space
function mergeSortedArrays(arr1, arr2) {
  const result = [];
  let i = 0, j = 0;

  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] <= arr2[j]) {
      result.push(arr1[i]);
      i++;
    } else {
      result.push(arr2[j]);
      j++;
    }
  }

  // Add remaining elements
  while (i < arr1.length) {
    result.push(arr1[i]);
    i++;
  }
  while (j < arr2.length) {
    result.push(arr2[j]);
    j++;
  }

  return result;
}
// O(n log n) time - simpler but slower
function mergeSortedSimple(arr1, arr2) {
  return [...arr1, ...arr2].sort((a, b) => a - b);
}

Pro Tip

Always mention that Method 1 preserves the sorted property and is more efficient. Interviewers want to see you understand when to leverage pre-sorted data.

Try it yourself

// Merge two sorted arrays into one sorted array
function mergeSortedArrays(arr1, arr2) {
  // Your code here (hint: use two pointers)

}

// Test your solution:
console.log(mergeSortedArrays([1, 3, 5], [2, 4, 6])); // Expected: [1, 2, 3, 4, 5, 6]
console.log(mergeSortedArrays([1, 2, 3], [4, 5, 6])); // Expected: [1, 2, 3, 4, 5, 6]

26. Find Second Largest Element

Problem: Find the second largest number in an array.

Example: - Input: [12, 35, 1, 10, 34, 1] → Output: 34

Solutions

// O(n) time, O(1) space
function secondLargest(arr) {
  if (arr.length < 2) return null;

  let first = -Infinity;
  let second = -Infinity;

  for (const num of arr) {
    if (num > first) {
      second = first;
      first = num;
    } else if (num > second && num !== first) {
      second = num;
    }
  }

  return second === -Infinity ? null : second;
}
// O(n log n) time
function secondLargestSort(arr) {
  const unique = [...new Set(arr)].sort((a, b) => b - a);
  return unique.length >= 2 ? unique[1] : null;
}
function secondLargestReduce(arr) {
  const result = arr.reduce(
    (acc, num) => {
      if (num > acc.first) {
        return { first: num, second: acc.first };
      } else if (num > acc.second && num !== acc.first) {
        return { ...acc, second: num };
      }
      return acc;
    },
    { first: -Infinity, second: -Infinity }
  );
  return result.second === -Infinity ? null : result.second;
}

Edge Cases

  • Array with less than 2 elements → return null
  • Array with all same elements → return null (no second largest)
  • Handle negative numbers by initializing with -Infinity

Try it yourself

// Find the second largest element in an array
function secondLargest(arr) {
  // Your code here

}

// Test your solution:
console.log(secondLargest([12, 35, 1, 10, 34, 1])); // Expected: 34
console.log(secondLargest([10, 10, 10]));           // Expected: null
console.log(secondLargest([5, 5, 4, 2]));           // Expected: 4

27. Rotate Array by K Positions

Problem: Rotate an array to the right by K steps.

Example: - Input: [1, 2, 3, 4, 5], k = 2 → Output: [4, 5, 1, 2, 3]

Solutions

function rotateArray(arr, k) {
  k = k % arr.length; // Handle k > array length
  return [...arr.slice(-k), ...arr.slice(0, -k)];
}
// O(n) time, O(1) space - interview favorite!
function rotateInPlace(arr, k) {
  k = k % arr.length;

  function reverse(start, end) {
    while (start < end) {
      [arr[start], arr[end]] = [arr[end], arr[start]];
      start++;
      end--;
    }
  }

  reverse(0, arr.length - 1);  // Reverse entire array
  reverse(0, k - 1);            // Reverse first k elements
  reverse(k, arr.length - 1);   // Reverse remaining elements

  return arr;
}
function rotateUnshift(arr, k) {
  k = k % arr.length;
  for (let i = 0; i < k; i++) {
    arr.unshift(arr.pop());
  }
  return arr;
}

Pro Tip

The triple-reverse method (Method 2) is a classic interview question. It demonstrates in-place array manipulation with O(1) extra space.

Edge Cases

  • Handle k > array.length by using modulo: k = k % arr.length
  • Handle k = 0 → return original array
  • Handle empty array → return empty array

Try it yourself

// Rotate array to the right by k positions
function rotateArray(arr, k) {
  // Your code here

}

// Test your solution:
console.log(rotateArray([1, 2, 3, 4, 5], 2)); // Expected: [4, 5, 1, 2, 3]
console.log(rotateArray([1, 2, 3], 4));       // Expected: [3, 1, 2]
console.log(rotateArray([1, 2], 1));          // Expected: [2, 1]

28. Object Deep Clone Implementation

Problem: Implement a deep clone function that handles all edge cases.

Solution

function deepClone(obj, hash = new WeakMap()) {
  // Handle primitives and null
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // Handle circular references
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  // Handle Date
  if (obj instanceof Date) {
    return new Date(obj.getTime());
  }

  // Handle RegExp
  if (obj instanceof RegExp) {
    return new RegExp(obj.source, obj.flags);
  }

  // Handle Array
  if (Array.isArray(obj)) {
    const arrCopy = [];
    hash.set(obj, arrCopy);
    for (let i = 0; i < obj.length; i++) {
      arrCopy[i] = deepClone(obj[i], hash);
    }
    return arrCopy;
  }

  // Handle Map
  if (obj instanceof Map) {
    const mapCopy = new Map();
    hash.set(obj, mapCopy);
    obj.forEach((value, key) => {
      mapCopy.set(deepClone(key, hash), deepClone(value, hash));
    });
    return mapCopy;
  }

  // Handle Set
  if (obj instanceof Set) {
    const setCopy = new Set();
    hash.set(obj, setCopy);
    obj.forEach(value => {
      setCopy.add(deepClone(value, hash));
    });
    return setCopy;
  }

  // Handle plain objects
  const objCopy = Object.create(Object.getPrototypeOf(obj));
  hash.set(obj, objCopy);

  for (const key of Reflect.ownKeys(obj)) {
    objCopy[key] = deepClone(obj[key], hash);
  }

  return objCopy;
}

29. Debounce vs Throttle Implementation

Question: Implement and explain debounce and throttle.

Answer:

Solution

// DEBOUNCE: Delays execution until after wait period of inactivity
// Use case: Search input, resize handler, form validation
function debounce(func, wait) {
  let timeoutId;

  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, wait);
  };
}

// Usage
const debouncedSearch = debounce((query) => {
  console.log('Searching for:', query);
}, 300);

// THROTTLE: Ensures function runs at most once per wait period
// Use case: Scroll handler, mousemove, API rate limiting
function throttle(func, wait) {
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= wait) {
      lastTime = now;
      func.apply(this, args);
    }
  };
}

// Throttle with trailing call
function throttleWithTrailing(func, wait) {
  let lastTime = 0;
  let timeoutId;

  return function (...args) {
    const now = Date.now();
    const remaining = wait - (now - lastTime);

    if (remaining <= 0) {
      clearTimeout(timeoutId);
      lastTime = now;
      func.apply(this, args);
    } else if (!timeoutId) {
      timeoutId = setTimeout(() => {
        lastTime = Date.now();
        timeoutId = null;
        func.apply(this, args);
      }, remaining);
    }
  };
}

Key Difference

  • Debounce: User types "hello" → only ONE call after 300ms of no typing
  • Throttle: User scrolls continuously → calls every 300ms during scroll

30. Memoization Implementation

Problem: Implement a memoization function for caching expensive computations.

Solution

// Basic memoization
function memoize(fn) {
  const cache = new Map();

  return function (...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Usage
const expensiveOperation = (n) => {
  console.log('Computing...');
  return n * n;
};

const memoizedOp = memoize(expensiveOperation);
memoizedOp(5); // Computing... → 25
memoizedOp(5); // → 25 (cached, no log)

// Advanced memoization with custom key and TTL
function memoizeAdvanced(fn, options = {}) {
  const { maxSize = 100, ttl = null, keyFn = JSON.stringify } = options;
  const cache = new Map();

  return function (...args) {
    const key = keyFn(args);

    if (cache.has(key)) {
      const { value, timestamp } = cache.get(key);
      if (!ttl || Date.now() - timestamp < ttl) {
        return value;
      }
      cache.delete(key);
    }

    const result = fn.apply(this, args);

    // LRU-like: delete oldest if over maxSize
    if (cache.size >= maxSize) {
      const firstKey = cache.keys().next().value;
      cache.delete(firstKey);
    }

    cache.set(key, { value: result, timestamp: Date.now() });
    return result;
  };
}

// Fibonacci with memoization (classic example)
const fibonacci = memoize(function (n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

fibonacci(50); // Fast! Without memoization this would be extremely slow

31. Group Array of Objects by Property

Problem: Group an array of objects by a specific property.

Example:

const people = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 25 }
];
// Group by age → { 25: [{...}, {...}], 30: [{...}] }

Solution

// Method 1: Using reduce
function groupBy(arr, key) {
  return arr.reduce((groups, item) => {
    const groupKey = item[key];
    groups[groupKey] = groups[groupKey] || [];
    groups[groupKey].push(item);
    return groups;
  }, {});
}

// Method 2: Using Object.groupBy (ES2024+)
// const grouped = Object.groupBy(people, person => person.age);

// Method 3: With callback for computed keys
function groupByCallback(arr, keyFn) {
  return arr.reduce((groups, item) => {
    const key = keyFn(item);
    groups[key] = groups[key] || [];
    groups[key].push(item);
    return groups;
  }, {});
}

// Usage
const people = [
  { name: 'Alice', age: 25, city: 'NYC' },
  { name: 'Bob', age: 30, city: 'LA' },
  { name: 'Charlie', age: 25, city: 'NYC' }
];

groupBy(people, 'age');
// { 25: [Alice, Charlie], 30: [Bob] }

groupByCallback(people, p => p.city);
// { NYC: [Alice, Charlie], LA: [Bob] }

// Count by group
function countBy(arr, key) {
  return arr.reduce((counts, item) => {
    const groupKey = typeof key === 'function' ? key(item) : item[key];
    counts[groupKey] = (counts[groupKey] || 0) + 1;
    return counts;
  }, {});
}

Try it yourself

// Group array of objects by a property
function groupBy(arr, key) {
  // Your code here

}

// Test your solution:
const people = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 25 }
];
console.log(groupBy(people, 'age'));
// Expected: { 25: [{name: 'Alice', age: 25}, {name: 'Charlie', age: 25}], 30: [{name: 'Bob', age: 30}] }

32. Find Intersection of Two Arrays

Problem: Find common elements between two arrays.

Example: - Input: [1, 2, 2, 1] and [2, 2] → Output: [2, 2] (with duplicates) - Input: [1, 2, 3] and [2, 3, 4] → Output: [2, 3] (unique)

Solutions

function intersectionUnique(arr1, arr2) {
  const set2 = new Set(arr2);
  return [...new Set(arr1)].filter(item => set2.has(item));
}
function intersectionWithDuplicates(arr1, arr2) {
  const map = new Map();
  const result = [];

  // Count occurrences in arr2
  for (const num of arr2) {
    map.set(num, (map.get(num) || 0) + 1);
  }

  // Find intersection
  for (const num of arr1) {
    if (map.get(num) > 0) {
      result.push(num);
      map.set(num, map.get(num) - 1);
    }
  }

  return result;
}
// Simple but O(n*m)
function intersectionSimple(arr1, arr2) {
  return arr1.filter(item => arr2.includes(item));
}
function intersectionSorted(arr1, arr2) {
  const result = [];
  let i = 0, j = 0;

  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] === arr2[j]) {
      result.push(arr1[i]);
      i++;
      j++;
    } else if (arr1[i] < arr2[j]) {
      i++;
    } else {
      j++;
    }
  }

  return result;
}

Try it yourself

// Find common elements between two arrays (unique)
function intersection(arr1, arr2) {
  // Your code here

}

// Test your solution:
console.log(intersection([1, 2, 3, 4], [2, 4, 6])); // Expected: [2, 4]
console.log(intersection([1, 2, 3], [4, 5, 6]));    // Expected: []
console.log(intersection([1, 1, 2], [1, 1]));       // Expected: [1]

Part 4: Data Structures & Algorithms

33. Check if Linked List is Palindrome

Problem: Determine if a singly linked list is a palindrome.

Solutions

class ListNode {
  constructor(val, next = null) {
    this.val = val;
    this.next = next;
  }
}

function isPalindromeArray(head) {
  const values = [];
  let current = head;

  while (current) {
    values.push(current.val);
    current = current.next;
  }

  let left = 0;
  let right = values.length - 1;

  while (left < right) {
    if (values[left] !== values[right]) return false;
    left++;
    right--;
  }
  return true;
}
// O(1) space
function isPalindromeOptimal(head) {
  if (!head || !head.next) return true;

  // Find middle
  let slow = head;
  let fast = head;
  while (fast.next && fast.next.next) {
    slow = slow.next;
    fast = fast.next.next;
  }

  // Reverse second half
  let secondHalf = reverseList(slow.next);
  let firstHalf = head;

  // Compare
  while (secondHalf) {
    if (firstHalf.val !== secondHalf.val) return false;
    firstHalf = firstHalf.next;
    secondHalf = secondHalf.next;
  }

  return true;
}

function reverseList(head) {
  let prev = null;
  let current = head;

  while (current) {
    const next = current.next;
    current.next = prev;
    prev = current;
    current = next;
  }

  return prev;
}

34. Binary Search Implementation

Problem: Implement binary search to find a target in a sorted array.

Example: - Input: [1, 2, 3, 4, 5, 6, 7, 8, 9], target = 5 → Output: 4 (index)

Solutions

function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (arr[mid] === target) {
      return mid;
    } else if (arr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return -1; // Not found
}
function binarySearchRecursive(arr, target, left = 0, right = arr.length - 1) {
  if (left > right) return -1;

  const mid = Math.floor((left + right) / 2);

  if (arr[mid] === target) return mid;
  if (arr[mid] < target) {
    return binarySearchRecursive(arr, target, mid + 1, right);
  }
  return binarySearchRecursive(arr, target, left, mid - 1);
}

Complexity

Iterative: Time O(log n), Space O(1) | Recursive: Time O(log n), Space O(log n) due to call stack

Pro Tip

Always prefer iterative binary search in interviews - it avoids stack overflow on large inputs and demonstrates understanding of space optimization.

Try it yourself

// Implement binary search (return index or -1)
function binarySearch(arr, target) {
  // Your code here

}

// Test your solution:
console.log(binarySearch([1, 2, 3, 4, 5, 6, 7, 8, 9], 5)); // Expected: 4
console.log(binarySearch([1, 2, 3, 4, 5], 1));             // Expected: 0
console.log(binarySearch([1, 2, 3, 4, 5], 6));             // Expected: -1

35. Find Peak Element

Problem: Find any peak element in an array (greater than neighbors).

Example: - Input: [1, 2, 3, 1] → Output: 2 (index of 3)

Solutions

// O(log n)
function findPeakElement(nums) {
  let left = 0;
  let right = nums.length - 1;

  while (left < right) {
    const mid = Math.floor((left + right) / 2);

    if (nums[mid] > nums[mid + 1]) {
      // Peak is on the left side (including mid)
      right = mid;
    } else {
      // Peak is on the right side
      left = mid + 1;
    }
  }

  return left;
}
// O(n)
function findPeakLinear(nums) {
  for (let i = 0; i < nums.length; i++) {
    const leftOk = i === 0 || nums[i] > nums[i - 1];
    const rightOk = i === nums.length - 1 || nums[i] > nums[i + 1];
    if (leftOk && rightOk) return i;
  }
  return -1;
}

Try it yourself

// Find the index of any peak element (greater than neighbors)
function findPeakElement(nums) {
  // Your code here (hint: use binary search)

}

// Test your solution:
console.log(findPeakElement([1, 2, 3, 1]));       // Expected: 2 (index of 3)
console.log(findPeakElement([1, 2, 1, 3, 5, 6, 4])); // Expected: 5 (index of 6)

36. Implement Stack using Array

Problem: Implement a stack with push, pop, peek, and isEmpty operations.

Solution

class Stack {
  constructor() {
    this.items = [];
  }

  // Add element to top - O(1)
  push(element) {
    this.items.push(element);
  }

  // Remove and return top element - O(1)
  pop() {
    if (this.isEmpty()) {
      throw new Error('Stack is empty');
    }
    return this.items.pop();
  }

  // Return top element without removing - O(1)
  peek() {
    if (this.isEmpty()) {
      throw new Error('Stack is empty');
    }
    return this.items[this.items.length - 1];
  }

  // Check if stack is empty - O(1)
  isEmpty() {
    return this.items.length === 0;
  }

  // Get stack size - O(1)
  size() {
    return this.items.length;
  }

  // Clear the stack - O(1)
  clear() {
    this.items = [];
  }
}

// Usage
const stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.peek());  // 3
console.log(stack.pop());   // 3
console.log(stack.size());  // 2

37. Implement Queue using Two Stacks

Problem: Implement a queue (FIFO) using two stacks (LIFO).

Solution

class QueueUsingStacks {
  constructor() {
    this.stack1 = []; // For enqueue
    this.stack2 = []; // For dequeue
  }

  // Add element to queue - O(1)
  enqueue(element) {
    this.stack1.push(element);
  }

  // Remove and return front element - Amortized O(1)
  dequeue() {
    if (this.isEmpty()) {
      throw new Error('Queue is empty');
    }

    // Transfer from stack1 to stack2 if stack2 is empty
    if (this.stack2.length === 0) {
      while (this.stack1.length > 0) {
        this.stack2.push(this.stack1.pop());
      }
    }

    return this.stack2.pop();
  }

  // Return front element without removing - Amortized O(1)
  peek() {
    if (this.isEmpty()) {
      throw new Error('Queue is empty');
    }

    if (this.stack2.length === 0) {
      while (this.stack1.length > 0) {
        this.stack2.push(this.stack1.pop());
      }
    }

    return this.stack2[this.stack2.length - 1];
  }

  isEmpty() {
    return this.stack1.length === 0 && this.stack2.length === 0;
  }

  size() {
    return this.stack1.length + this.stack2.length;
  }
}

// Example:
// enqueue(1), enqueue(2), enqueue(3)
// stack1: [1, 2, 3], stack2: []
// dequeue() → transfer → stack1: [], stack2: [3, 2, 1] → returns 1

38. Level Order Traversal (Binary Tree)

Problem: Traverse a binary tree level by level (BFS).

Example:

    1
   / \
  2   3
 / \
4   5

Output: [[1], [2, 3], [4, 5]]

Solutions

class TreeNode {
  constructor(val, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

function levelOrder(root) {
  if (!root) return [];

  const result = [];
  const queue = [root];

  while (queue.length > 0) {
    const levelSize = queue.length;
    const currentLevel = [];

    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
      currentLevel.push(node.val);

      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }

    result.push(currentLevel);
  }

  return result;
}
function levelOrderDFS(root) {
  const result = [];

  function traverse(node, level) {
    if (!node) return;

    if (result.length === level) {
      result.push([]);
    }

    result[level].push(node.val);
    traverse(node.left, level + 1);
    traverse(node.right, level + 1);
  }

  traverse(root, 0);
  return result;
}

39. Find First Occurrence in Sorted Array

Problem: Find the first occurrence of a target in a sorted array with duplicates.

Example: - Input: [1, 2, 2, 2, 3, 4], target = 2 → Output: 1 (first index of 2)

Solution

function findFirstOccurrence(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  let result = -1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (arr[mid] === target) {
      result = mid;
      right = mid - 1; // Continue searching left for first occurrence
    } else if (arr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return result;
}

// Find last occurrence
function findLastOccurrence(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  let result = -1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (arr[mid] === target) {
      result = mid;
      left = mid + 1; // Continue searching right for last occurrence
    } else if (arr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return result;
}

// Count occurrences
function countOccurrences(arr, target) {
  const first = findFirstOccurrence(arr, target);
  if (first === -1) return 0;
  const last = findLastOccurrence(arr, target);
  return last - first + 1;
}

Try it yourself

// Find the first occurrence of target in sorted array
function findFirstOccurrence(arr, target) {
  // Your code here (hint: modified binary search)

}

// Test your solution:
console.log(findFirstOccurrence([1, 2, 2, 2, 3, 4], 2)); // Expected: 1
console.log(findFirstOccurrence([1, 1, 1, 1, 1], 1));    // Expected: 0
console.log(findFirstOccurrence([1, 2, 3, 4, 5], 6));    // Expected: -1

40. Kadane's Algorithm (Max Subarray Sum)

Problem: Find the contiguous subarray with the maximum sum.

Example: - Input: [-2, 1, -3, 4, -1, 2, 1, -5, 4] → Output: 6 (subarray [4, -1, 2, 1])

Solutions

// O(n)
function maxSubarraySum(nums) {
  if (nums.length === 0) return 0;

  let maxSum = nums[0];
  let currentSum = nums[0];

  for (let i = 1; i < nums.length; i++) {
    // Either extend current subarray or start new one
    currentSum = Math.max(nums[i], currentSum + nums[i]);
    maxSum = Math.max(maxSum, currentSum);
  }

  return maxSum;
}
function maxSubarrayWithIndices(nums) {
  let maxSum = nums[0];
  let currentSum = nums[0];
  let start = 0;
  let end = 0;
  let tempStart = 0;

  for (let i = 1; i < nums.length; i++) {
    if (nums[i] > currentSum + nums[i]) {
      currentSum = nums[i];
      tempStart = i;
    } else {
      currentSum = currentSum + nums[i];
    }

    if (currentSum > maxSum) {
      maxSum = currentSum;
      start = tempStart;
      end = i;
    }
  }

  return {
    maxSum,
    subarray: nums.slice(start, end + 1),
    startIndex: start,
    endIndex: end
  };
}

// Example
const arr = [-2, 1, -3, 4, -1, 2, 1, -5, 4];
console.log(maxSubarraySum(arr)); // 6
console.log(maxSubarrayWithIndices(arr));
// { maxSum: 6, subarray: [4, -1, 2, 1], startIndex: 3, endIndex: 6 }

Try it yourself

// Find maximum sum of contiguous subarray (Kadane's Algorithm)
function maxSubarraySum(nums) {
  // Your code here

}

// Test your solution:
console.log(maxSubarraySum([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // Expected: 6
console.log(maxSubarraySum([1, 2, 3, 4, 5]));                  // Expected: 15
console.log(maxSubarraySum([-1, -2, -3]));                     // Expected: -1

Part 5: Testing & Automation Concepts

41. Testing Pyramid Explanation

Question: Explain the Testing Pyramid and its layers.

Answer:

                    /\
                   /  \
                  / E2E \         ← Few, slow, expensive
                 /________\
                /          \
               / Integration \    ← Medium amount
              /______________\
             /                \
            /    Unit Tests    \  ← Many, fast, cheap
           /____________________\

Layers:

Layer Quantity Speed Cost Purpose
Unit Many (70%) Fast (ms) Low Test individual functions/components
Integration Medium (20%) Medium (s) Medium Test component interactions
E2E/UI Few (10%) Slow (min) High Test complete user flows

Key Principles

  • More tests at the bottom (unit), fewer at the top (E2E)
  • Unit tests provide fastest feedback
  • E2E tests are most realistic but slowest and most brittle
  • Balance coverage across all layers

42. Unit vs Integration vs E2E Testing

Question: Explain the differences with examples.

Answer:

Unit Testing

// Testing a single function in isolation
function add(a, b) {
  return a + b;
}

// Unit test
test('add function adds two numbers', () => {
  expect(add(2, 3)).toBe(5);
  expect(add(-1, 1)).toBe(0);
});

Integration Testing

// Testing multiple components working together
test('user service creates user and sends email', async () => {
  const userService = new UserService(database, emailService);

  const user = await userService.createUser({
    email: 'test@example.com',
    name: 'Test User'
  });

  expect(user.id).toBeDefined();
  expect(emailService.sendWelcome).toHaveBeenCalledWith('test@example.com');
});

E2E Testing

// Testing complete user flow through UI
test('user can login and view dashboard', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[data-testid="email"]', 'user@example.com');
  await page.fill('[data-testid="password"]', 'password123');
  await page.click('[data-testid="login-button"]');

  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('h1')).toContainText('Welcome');
});

43. Test Case Prioritization Strategy

Question: How do you prioritize which tests to write and run?

Answer:

Prioritization Criteria (Risk-Based):

  1. Business Impact
  2. Revenue-critical features (checkout, payments)
  3. User-facing functionality
  4. Core business logic

  5. Frequency of Use

  6. Features used daily by most users
  7. Happy path scenarios

  8. Defect Probability

  9. Recently changed code
  10. Complex business logic
  11. Areas with historical bugs

  12. Test Execution Strategy:

Prioritization Example

// Fast feedback first
const testSuite = {
  smoke: ['login', 'search', 'checkout'], // Run always, < 5 min
  regression: ['all critical paths'],       // Run on PR, < 30 min
  full: ['all tests'],                      // Run nightly
};

// CI/CD Pipeline prioritization
// PR → Smoke + Unit + Changed file tests
// Merge → Full regression
// Deploy → Smoke tests in production

44. CI/CD Pipeline Concepts

Question: Explain CI/CD and how testing fits in.

Answer:

CI (Continuous Integration): - Automatically build and test code on every commit - Detect integration issues early - Run unit and integration tests

CD (Continuous Delivery/Deployment): - Automatically deploy tested code to environments - Delivery = manual approval for production - Deployment = automatic to production

Pipeline Stages (GitHub Actions)

name: CI/CD Pipeline

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Unit Tests
        run: npm test -- --coverage

      - name: Integration Tests
        run: npm run test:integration

      - name: E2E Tests
        run: npx playwright test

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Production
        run: ./deploy.sh

45. Page Object Model (POM) Design Pattern

Question: Explain POM and its benefits.

Answer:

What is POM? A design pattern that creates an object repository for UI elements, separating test logic from page interactions.

Benefits: - Reduced code duplication - Easier maintenance (change locator in one place) - Improved readability - Reusable components

Implementation

// pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}

  // Locators
  private emailInput = '[data-testid="email"]';
  private passwordInput = '[data-testid="password"]';
  private loginButton = '[data-testid="login-btn"]';
  private errorMessage = '[data-testid="error"]';

  // Actions
  async navigate() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.page.fill(this.emailInput, email);
    await this.page.fill(this.passwordInput, password);
    await this.page.click(this.loginButton);
  }

  async getErrorMessage() {
    return this.page.textContent(this.errorMessage);
  }
}

// tests/login.spec.ts
test('successful login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.navigate();
  await loginPage.login('user@test.com', 'password123');

  await expect(page).toHaveURL('/dashboard');
});

46. Handling Flaky Tests

Question: What are flaky tests and how do you handle them?

Answer:

What are Flaky Tests? Tests that pass and fail inconsistently without code changes.

Common Causes: 1. Timing issues / Race conditions 2. Test order dependencies 3. Shared state between tests 4. Network instability 5. External service dependencies 6. Environment differences

Solutions

// 1. Use proper waits instead of hardcoded sleeps
// BAD
await page.waitForTimeout(5000);

// GOOD
await page.waitForSelector('[data-testid="loaded"]');
await expect(page.locator('.spinner')).toBeHidden();

// 2. Isolate tests - don't share state
beforeEach(async () => {
  await database.reset();
  await page.context().clearCookies();
});

// 3. Use retry logic for flaky external dependencies
test('api call', { retries: 2 }, async () => {
  // Test code
});

// 4. Mock external services
await page.route('**/api/external/**', route => {
  route.fulfill({ json: mockData });
});

// 5. Quarantine flaky tests
test.skip('known flaky test', async () => {
  // TODO: Fix timing issue
});

Process: 1. Identify flaky tests (track pass/fail rates) 2. Quarantine immediately (don't block CI) 3. Root cause analysis 4. Fix or delete


47. API Testing Fundamentals

Question: Explain API testing approaches and best practices.

Answer:

What to Test: - Response status codes - Response body structure/schema - Response data correctness - Headers (content-type, auth) - Error handling - Performance (response time)

Example with Playwright

import { test, expect } from '@playwright/test';

test.describe('API Testing', () => {
  test('GET users returns list', async ({ request }) => {
    const response = await request.get('/api/users');

    expect(response.status()).toBe(200);
    expect(response.headers()['content-type']).toContain('application/json');

    const users = await response.json();
    expect(users).toBeInstanceOf(Array);
    expect(users[0]).toHaveProperty('id');
    expect(users[0]).toHaveProperty('email');
  });

  test('POST creates user', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: {
        name: 'Test User',
        email: 'test@example.com'
      }
    });

    expect(response.status()).toBe(201);
    const user = await response.json();
    expect(user.id).toBeDefined();
  });

  test('unauthorized request returns 401', async ({ request }) => {
    const response = await request.get('/api/protected', {
      headers: {} // No auth header
    });

    expect(response.status()).toBe(401);
  });
});

48. Performance Testing Metrics

Question: What are key performance testing metrics?

Answer:

Key Metrics:

Metric Description Target Example
Response Time Time to complete request < 200ms (API), < 3s (page load)
Throughput Requests per second 1000 RPS
Error Rate % of failed requests < 1%
Latency (P95/P99) 95th/99th percentile response P95 < 500ms
Concurrent Users Simultaneous active users 10,000

Web Vitals (Frontend)

// Core Web Vitals
const metrics = {
  LCP: 2.5,   // Largest Contentful Paint (< 2.5s = good)
  FID: 100,   // First Input Delay (< 100ms = good)
  CLS: 0.1,   // Cumulative Layout Shift (< 0.1 = good)
  TTFB: 800,  // Time to First Byte (< 800ms)
  FCP: 1800   // First Contentful Paint (< 1.8s)
};

// Playwright performance measurement
test('page load performance', async ({ page }) => {
  await page.goto('/');

  const performanceMetrics = await page.evaluate(() => {
    const timing = performance.getEntriesByType('navigation')[0];
    return {
      domContentLoaded: timing.domContentLoadedEventEnd,
      loadComplete: timing.loadEventEnd,
      ttfb: timing.responseStart - timing.requestStart
    };
  });

  expect(performanceMetrics.loadComplete).toBeLessThan(3000);
});

49. Test Data Management

Question: How do you manage test data effectively?

Answer:

Strategies:

1. Factory Functions

// Create consistent test data
function createTestUser(overrides = {}) {
  return {
    id: faker.string.uuid(),
    email: faker.internet.email(),
    name: faker.person.fullName(),
    createdAt: new Date().toISOString(),
    ...overrides
  };
}

// Usage
const user = createTestUser({ role: 'admin' });

2. Database Seeding

// Before tests - seed database
beforeAll(async () => {
  await db.seed('users', testUsers);
  await db.seed('products', testProducts);
});

// After tests - cleanup
afterAll(async () => {
  await db.truncate(['users', 'products']);
});

3. API-Driven Setup

test.beforeEach(async ({ request }) => {
  // Create test data via API
  const response = await request.post('/api/test/setup', {
    data: { scenario: 'checkout-flow' }
  });
  testContext = await response.json();
});

4. Fixtures File

// fixtures/users.json
{
  "validUser": {
    "email": "valid@test.com",
    "password": "ValidPass123!"
  },
  "adminUser": {
    "email": "admin@test.com",
    "password": "AdminPass123!",
    "role": "admin"
  }
}

50. Code Coverage vs Test Coverage

Question: Explain the difference and their importance.

Answer:

Code Coverage: - Measures what % of code is executed during tests - Metrics: line, branch, function, statement coverage - Tool-generated (Istanbul, NYC, Jest)

Code Coverage Example

// Example: 100% line coverage but not fully tested
function divide(a, b) {
  return a / b; // Line covered, but what about b = 0?
}

test('divide', () => {
  expect(divide(10, 2)).toBe(5); // 100% coverage
  // Missing: divide(10, 0) edge case!
});

Test Coverage: - Measures what % of requirements/features are tested - Includes scenarios, edge cases, user flows - Requires test planning and traceability

Comparison:

Aspect Code Coverage Test Coverage
Measures Lines of code executed Requirements tested
Tool Automated (Istanbul) Manual tracking (Jira)
Goal Find untested code Verify feature completeness
Limitation High % ≠ quality tests Subjective measurement

Best Practice

// Use both metrics together
// playwright.config.ts
export default defineConfig({
  use: {
    // Track coverage
    coverage: {
      include: ['src/**/*.ts'],
      exclude: ['**/*.test.ts']
    }
  }
});

// Target: 80%+ code coverage PLUS
// 100% critical path test coverage
// Edge cases for high-risk areas

Part 6: Playwright JavaScript/TypeScript

51. What is Playwright & its Architecture?

Question: Explain Playwright and how it works internally.

Answer:

What is Playwright? Playwright is a modern end-to-end testing framework by Microsoft that enables reliable cross-browser testing for web applications.

Key Features: - Multi-browser support (Chromium, Firefox, WebKit) - Auto-wait mechanism - Network interception - Mobile emulation - API testing support - Parallel test execution - Built-in test runner

Architecture:

┌─────────────────────────────────────────────────────┐
│                    Test Script                       │
│              (JavaScript/TypeScript)                 │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│              Playwright Library                      │
│         (Node.js binding / API layer)               │
└─────────────────────────────────────────────────────┘
                         ▼ WebSocket (CDP/DevTools Protocol)
┌─────────────────────────────────────────────────────┐
│              Browser Server                          │
│    ┌─────────┐  ┌─────────┐  ┌─────────┐           │
│    │Chromium │  │ Firefox │  │ WebKit  │           │
│    └─────────┘  └─────────┘  └─────────┘           │
└─────────────────────────────────────────────────────┘

Communication Protocol: - Uses Chrome DevTools Protocol (CDP) for Chromium - Custom protocols for Firefox and WebKit - Single WebSocket connection per browser instance


52. Playwright vs Selenium vs Cypress

Question: Compare these testing frameworks.

Answer:

Feature Playwright Selenium Cypress
Language JS/TS, Python, C#, Java Multiple JavaScript only
Browsers Chromium, Firefox, WebKit All major browsers Chromium, Firefox, WebKit
Architecture Single process Client-Server In-browser
Auto-wait Built-in Manual waits Built-in
Parallel Native Grid setup Limited (paid)
iFrames Native support Complex Limited
Mobile Emulation Appium needed Limited
Network Full control Limited Full control
Speed Fast Slower Fast
Debugging Trace viewer, Inspector Browser DevTools Time-travel

When to Choose

  • Playwright: Modern apps, cross-browser, API testing, fast execution
  • Selenium: Legacy support, multiple languages, existing infrastructure
  • Cypress: Quick setup, developer experience, single browser okay

53. Browser Contexts and Pages

Question: Explain the relationship between Browser, Context, and Page.

Answer:

Browser
├── Context 1 (like incognito)
│   ├── Page 1
│   ├── Page 2
│   └── Page 3
└── Context 2 (isolated from Context 1)
    ├── Page 4
    └── Page 5

Browser: The browser instance (Chromium, Firefox, WebKit) Context: Isolated session (like incognito window) - no shared cookies/storage Page: Single tab within a context

Code Example

import { chromium } from 'playwright';

async function example() {
  // Launch browser
  const browser = await chromium.launch();

  // Create isolated context
  const context = await browser.newContext({
    viewport: { width: 1280, height: 720 },
    locale: 'en-US',
    permissions: ['geolocation']
  });

  // Create page in context
  const page = await context.newPage();
  await page.goto('https://example.com');

  // Multiple pages in same context share cookies
  const page2 = await context.newPage();

  // Clean up
  await context.close();
  await browser.close();
}

Use Cases for Multiple Contexts

  • Testing multi-user scenarios (e.g., chat between two users)
  • Parallel test isolation
  • Different authentication states
  • Testing cross-tab communication

54. Auto-waiting Mechanism

Question: How does Playwright's auto-waiting work?

Answer:

Playwright automatically waits for elements to be actionable before performing actions, eliminating the need for manual waits.

Actionability Checks

// For click(), Playwright waits until element is:
// ✓ Attached to DOM
// ✓ Visible
// ✓ Stable (not animating)
// ✓ Receives events (not obscured)
// ✓ Enabled

await page.click('button#submit'); // Auto-waits for all conditions

// For fill(), waits until element is:
// ✓ Attached, Visible, Enabled
// ✓ Editable

await page.fill('input#email', 'test@example.com');

Configuring Timeouts

// Global timeout
const browser = await chromium.launch();
const context = await browser.newContext();
context.setDefaultTimeout(30000);

// Per-action timeout
await page.click('button', { timeout: 5000 });

// Assertion timeout
await expect(page.locator('.result')).toBeVisible({ timeout: 10000 });

When Auto-wait Isn't Enough

// Wait for specific conditions
await page.waitForLoadState('networkidle');
await page.waitForURL('**/dashboard');
await page.waitForSelector('.loaded');
await page.waitForFunction(() => document.title.includes('Ready'));

55. Locator Strategies

Question: What locator strategies does Playwright support?

Answer:

Recommended Locators (Most Reliable)

// 1. getByRole - Best practice, accessibility-focused
await page.getByRole('button', { name: 'Submit' });
await page.getByRole('textbox', { name: 'Email' });
await page.getByRole('link', { name: /learn more/i });

// 2. getByTestId - When you control the markup
await page.getByTestId('submit-button');
// Requires: <button data-testid="submit-button">

// 3. getByText - For text content
await page.getByText('Welcome back');
await page.getByText(/hello/i); // Case-insensitive regex

// 4. getByLabel - For form inputs
await page.getByLabel('Username');
await page.getByPlaceholder('Enter email');

CSS and XPath

// CSS selectors
await page.locator('button.primary');
await page.locator('#submit-btn');
await page.locator('[data-cy="login"]');
await page.locator('form >> button');

// XPath (use sparingly)
await page.locator('//button[contains(text(), "Submit")]');
await page.locator('xpath=//div[@class="container"]//button');

Chaining and Filtering

// Chain locators
const dialog = page.locator('.dialog');
const submitBtn = dialog.locator('button', { hasText: 'Submit' });

// Filter locators
const rows = page.locator('tr');
const activeRow = rows.filter({ hasText: 'Active' });

// nth element
await page.locator('.item').nth(2).click();
await page.locator('.item').first().click();
await page.locator('.item').last().click();

56. Implementing POM in Playwright/TypeScript

Question: Show a complete POM implementation.

Answer:

Complete POM Implementation

// pages/BasePage.ts
import { Page, Locator } from '@playwright/test';

export class BasePage {
  constructor(protected page: Page) {}

  async navigate(path: string) {
    await this.page.goto(path);
  }

  async waitForPageLoad() {
    await this.page.waitForLoadState('networkidle');
  }
}

// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
  // Locators
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    super(page);
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.loginButton = page.getByRole('button', { name: 'Sign In' });
    this.errorMessage = page.getByTestId('error-message');
  }

  async goto() {
    await this.navigate('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async expectError(message: string) {
    await expect(this.errorMessage).toContainText(message);
  }
}

// pages/DashboardPage.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';

export class DashboardPage extends BasePage {
  readonly welcomeMessage: Locator;
  readonly logoutButton: Locator;

  constructor(page: Page) {
    super(page);
    this.welcomeMessage = page.getByRole('heading', { level: 1 });
    this.logoutButton = page.getByRole('button', { name: 'Logout' });
  }

  async expectWelcome(name: string) {
    await expect(this.welcomeMessage).toContainText(`Welcome, ${name}`);
  }
}

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

test.describe('Login Flow', () => {
  test('successful login redirects to dashboard', async ({ page }) => {
    const loginPage = new LoginPage(page);
    const dashboardPage = new DashboardPage(page);

    await loginPage.goto();
    await loginPage.login('user@test.com', 'password123');

    await expect(page).toHaveURL(/dashboard/);
    await dashboardPage.expectWelcome('User');
  });

  test('invalid credentials show error', async ({ page }) => {
    const loginPage = new LoginPage(page);

    await loginPage.goto();
    await loginPage.login('invalid@test.com', 'wrong');

    await loginPage.expectError('Invalid credentials');
  });
});

57. Playwright Fixtures Explained

Question: What are fixtures and how do they work?

Answer:

Fixtures are Playwright's dependency injection system - they set up test environments and provide resources to tests.

Built-in Fixtures

import { test, expect } from '@playwright/test';

test('using built-in fixtures', async ({
  page,        // New page for each test
  context,     // Browser context
  browser,     // Browser instance
  request,     // API request context
  browserName  // 'chromium' | 'firefox' | 'webkit'
}) => {
  await page.goto('/');
});

How Fixtures Work:

1. Test requests fixtures it needs via function arguments
2. Playwright initializes fixtures before test
3. Test runs with fixture values
4. Playwright tears down fixtures after test

Fixture Scopes

// test-scoped: new instance per test (default)
test.use({ viewport: { width: 800, height: 600 } });

// worker-scoped: shared across tests in same worker
// Useful for expensive setup (database, auth)

58. Custom Fixtures Creation

Question: How do you create custom fixtures?

Answer:

Custom Fixtures Implementation

// fixtures/fixtures.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

// Define fixture types
type MyFixtures = {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
  authenticatedPage: Page;
};

// Extend base test with custom fixtures
export const test = base.extend<MyFixtures>({
  // Page Object fixtures
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
  },

  dashboardPage: async ({ page }, use) => {
    await use(new DashboardPage(page));
  },

  // Pre-authenticated page fixture
  authenticatedPage: async ({ page }, use) => {
    // Setup: Login before test
    await page.goto('/login');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password123');
    await page.click('button[type="submit"]');
    await page.waitForURL('**/dashboard');

    // Provide to test
    await use(page);

    // Teardown: Logout after test
    await page.click('#logout');
  },
});

export { expect } from '@playwright/test';

// tests/dashboard.spec.ts
import { test, expect } from '../fixtures/fixtures';

test('dashboard with fixtures', async ({ loginPage, dashboardPage }) => {
  await loginPage.goto();
  await loginPage.login('user@test.com', 'pass123');
  await dashboardPage.expectWelcome('User');
});

test('pre-authenticated test', async ({ authenticatedPage }) => {
  // Already logged in!
  await expect(authenticatedPage).toHaveURL(/dashboard/);
});

59. Fixtures vs beforeEach/afterEach Hooks

Question: When should you use fixtures vs hooks?

Answer:

Hooks Approach

import { test, expect, Page } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

let loginPage: LoginPage;

test.beforeEach(async ({ page }) => {
  loginPage = new LoginPage(page);
  await loginPage.goto();
});

test.afterEach(async ({ page }) => {
  // Cleanup
});

test('test 1', async ({ page }) => {
  await loginPage.login('user@test.com', 'pass');
});

Fixtures Approach

import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

const test = base.extend<{ loginPage: LoginPage }>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await use(loginPage);
  },
});

test('test 1', async ({ loginPage }) => {
  await loginPage.login('user@test.com', 'pass');
});

Comparison:

Aspect Fixtures beforeEach/afterEach
Reusability Cross-file Same file only
Type Safety Full TypeScript Requires careful typing
Lazy Loading Only loads if used Always runs
Parallelization Better isolation Shared variables risk
Composition Can depend on other fixtures Manual dependency management

Best Practice: Use fixtures for reusable setup, hooks for file-specific setup.


60. Worker-scoped vs Test-scoped Fixtures

Question: Explain fixture scopes and when to use each.

Answer:

Fixture Scopes Example

import { test as base } from '@playwright/test';

type Fixtures = {
  // Test-scoped: new instance per test
  testDatabase: Database;
  // Worker-scoped: shared across tests in worker
  sharedDatabase: Database;
};

const test = base.extend<Fixtures, { sharedDatabase: Database }>({
  // Test-scoped (default) - isolated per test
  testDatabase: async ({}, use) => {
    const db = await Database.create();
    await db.seed();
    await use(db);
    await db.cleanup(); // Runs after each test
  },

  // Worker-scoped - shared, expensive setup once
  sharedDatabase: [async ({}, use) => {
    console.log('Setting up shared database...');
    const db = await Database.createShared();
    await db.migrate();
    await use(db);
    await db.close(); // Runs once when worker done
  }, { scope: 'worker' }],
});

// Usage
test('test 1', async ({ testDatabase }) => {
  // Gets fresh database
});

test('test 2', async ({ sharedDatabase }) => {
  // Shares database with other tests in same worker
});

When to Use Each:

Use Test-scoped Use Worker-scoped
Test data that must be isolated Expensive one-time setup
Tests that modify shared state Read-only shared resources
Default choice for most fixtures Browser authentication state
Page objects Database connections

61. Network Interception & Mocking

Question: How do you intercept and mock network requests?

Answer:

Network Interception Examples

import { test, expect } from '@playwright/test';

test.describe('Network Interception', () => {
  // Mock API response
  test('mock API response', async ({ page }) => {
    await page.route('**/api/users', route => {
      route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: 1, name: 'Mock User' }
        ])
      });
    });

    await page.goto('/users');
    await expect(page.locator('.user-name')).toContainText('Mock User');
  });

  // Modify response
  test('modify response', async ({ page }) => {
    await page.route('**/api/products', async route => {
      const response = await route.fetch();
      const json = await response.json();

      // Modify the response
      json.products[0].price = 0;

      await route.fulfill({
        response,
        json
      });
    });

    await page.goto('/products');
  });

  // Block requests
  test('block images', async ({ page }) => {
    await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
    await page.goto('/gallery');
  });

  // Wait for specific request
  test('wait for API call', async ({ page }) => {
    const responsePromise = page.waitForResponse('**/api/data');
    await page.click('#load-data');
    const response = await responsePromise;

    expect(response.status()).toBe(200);
    const data = await response.json();
    expect(data).toHaveProperty('items');
  });

  // Mock failed request
  test('test error handling', async ({ page }) => {
    await page.route('**/api/save', route => {
      route.fulfill({
        status: 500,
        body: JSON.stringify({ error: 'Server error' })
      });
    });

    await page.goto('/form');
    await page.click('#submit');
    await expect(page.locator('.error')).toBeVisible();
  });
});

62. API Testing with APIRequestContext

Question: How do you perform API testing in Playwright?

Answer:

API Testing Examples

import { test, expect } from '@playwright/test';

test.describe('API Testing', () => {
  test('GET request', async ({ request }) => {
    const response = await request.get('/api/users');

    expect(response.ok()).toBeTruthy();
    expect(response.status()).toBe(200);

    const users = await response.json();
    expect(users).toBeInstanceOf(Array);
    expect(users.length).toBeGreaterThan(0);
  });

  test('POST request with body', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: {
        name: 'New User',
        email: 'new@example.com'
      },
      headers: {
        'Content-Type': 'application/json'
      }
    });

    expect(response.status()).toBe(201);
    const user = await response.json();
    expect(user.id).toBeDefined();
  });

  test('authenticated API request', async ({ request }) => {
    // Login first
    const loginResponse = await request.post('/api/login', {
      data: { email: 'user@test.com', password: 'pass123' }
    });
    const { token } = await loginResponse.json();

    // Use token for subsequent requests
    const response = await request.get('/api/protected', {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });

    expect(response.ok()).toBeTruthy();
  });

  test('file upload', async ({ request }) => {
    const response = await request.post('/api/upload', {
      multipart: {
        file: {
          name: 'test.txt',
          mimeType: 'text/plain',
          buffer: Buffer.from('Hello World')
        },
        description: 'Test file'
      }
    });

    expect(response.status()).toBe(200);
  });
});

// Create reusable API client fixture
import { test as base, APIRequestContext } from '@playwright/test';

const test = base.extend<{ apiClient: APIRequestContext }>({
  apiClient: async ({ playwright }, use) => {
    const context = await playwright.request.newContext({
      baseURL: 'https://api.example.com',
      extraHTTPHeaders: {
        'Authorization': 'Bearer token123'
      }
    });
    await use(context);
    await context.dispose();
  }
});

63. Handling iframes and Popups

Question: How do you interact with iframes and popup windows?

Answer:

iframes and Popups Examples

import { test, expect } from '@playwright/test';

test.describe('iframes', () => {
  test('interact with iframe content', async ({ page }) => {
    await page.goto('/page-with-iframe');

    // Method 1: Using frameLocator
    const frame = page.frameLocator('#my-iframe');
    await frame.locator('button#submit').click();
    await expect(frame.locator('.result')).toBeVisible();

    // Method 2: Using frame() for more control
    const frameElement = page.frame({ name: 'iframe-name' });
    if (frameElement) {
      await frameElement.fill('input#email', 'test@example.com');
    }

    // Nested iframes
    const nestedFrame = page
      .frameLocator('#outer-iframe')
      .frameLocator('#inner-iframe');
    await nestedFrame.locator('button').click();
  });
});

test.describe('Popups and New Windows', () => {
  test('handle popup window', async ({ page }) => {
    await page.goto('/main-page');

    // Wait for popup
    const popupPromise = page.waitForEvent('popup');
    await page.click('a[target="_blank"]');
    const popup = await popupPromise;

    // Wait for popup to load
    await popup.waitForLoadState();

    // Interact with popup
    await expect(popup).toHaveTitle('Popup Page');
    await popup.fill('#form-input', 'data');
    await popup.click('#submit');

    // Close popup
    await popup.close();
  });

  test('handle dialogs (alert, confirm, prompt)', async ({ page }) => {
    // Setup dialog handler before triggering
    page.on('dialog', async dialog => {
      console.log('Dialog message:', dialog.message());

      if (dialog.type() === 'confirm') {
        await dialog.accept();
      } else if (dialog.type() === 'prompt') {
        await dialog.accept('My input');
      } else {
        await dialog.dismiss();
      }
    });

    await page.goto('/page-with-dialogs');
    await page.click('#trigger-alert');
  });

  test('handle file download', async ({ page }) => {
    const downloadPromise = page.waitForEvent('download');
    await page.click('#download-button');
    const download = await downloadPromise;

    // Save to specific path
    await download.saveAs('/tmp/downloaded-file.pdf');

    // Or get download stream
    const path = await download.path();
    console.log('Downloaded to:', path);
  });
});

64. Screenshot, Video & Trace Recording

Question: How do you capture screenshots, videos, and traces?

Answer:

Screenshot, Video & Trace Examples

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // Screenshot options
    screenshot: 'only-on-failure', // 'on', 'off', 'only-on-failure'

    // Video recording
    video: 'retain-on-failure', // 'on', 'off', 'on-first-retry', 'retain-on-failure'

    // Trace recording for debugging
    trace: 'retain-on-failure', // 'on', 'off', 'on-first-retry', 'retain-on-failure'
  },
});

// In tests - manual capture
import { test, expect } from '@playwright/test';

test('capture screenshots', async ({ page }) => {
  await page.goto('/dashboard');

  // Full page screenshot
  await page.screenshot({
    path: 'screenshots/dashboard.png',
    fullPage: true
  });

  // Element screenshot
  await page.locator('.chart').screenshot({
    path: 'screenshots/chart.png'
  });

  // Screenshot with mask
  await page.screenshot({
    path: 'screenshots/masked.png',
    mask: [page.locator('.sensitive-data')]
  });
});

test('manual trace recording', async ({ page, context }) => {
  // Start tracing
  await context.tracing.start({
    screenshots: true,
    snapshots: true,
    sources: true
  });

  // Run test
  await page.goto('/');
  await page.click('#login');

  // Stop and save trace
  await context.tracing.stop({
    path: 'traces/login-flow.zip'
  });

  // View trace: npx playwright show-trace traces/login-flow.zip
});

Viewing Traces:

# Open trace viewer
npx playwright show-trace trace.zip

# View latest test report
npx playwright show-report


65. Parallel Test Execution & Sharding

Question: How does Playwright handle parallel execution and sharding?

Answer:

Parallel Execution Config

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Parallel execution settings
  workers: process.env.CI ? 2 : undefined, // # of parallel workers
  fullyParallel: true, // Run all tests in parallel

  // Retry failed tests
  retries: process.env.CI ? 2 : 0,

  // Reporter
  reporter: [
    ['html'],
    ['junit', { outputFile: 'results.xml' }]
  ],
});

Controlling Parallelism

import { test } from '@playwright/test';

// Run tests in this file serially
test.describe.configure({ mode: 'serial' });

test.describe('Sequential tests', () => {
  test('step 1', async ({ page }) => { /* ... */ });
  test('step 2', async ({ page }) => { /* depends on step 1 */ });
});

// Or parallel (default)
test.describe.configure({ mode: 'parallel' });

Sharding for CI:

# Split tests across multiple CI machines
# Machine 1:
npx playwright test --shard=1/4

# Machine 2:
npx playwright test --shard=2/4

# Machine 3:
npx playwright test --shard=3/4

# Machine 4:
npx playwright test --shard=4/4

# Merge reports
npx playwright merge-reports ./all-blob-reports --reporter=html

GitHub Actions Sharding

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx playwright test --shard=${{ matrix.shard }}/4
      - uses: actions/upload-artifact@v3
        with:
          name: blob-report-${{ matrix.shard }}
          path: blob-report/

66. TypeScript Configuration in Playwright

Question: How do you set up TypeScript with Playwright?

Answer:

TypeScript Configuration

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": ".",
    "paths": {
      "@pages/*": ["./pages/*"],
      "@fixtures/*": ["./fixtures/*"],
      "@utils/*": ["./utils/*"]
    }
  },
  "include": ["**/*.ts"],
  "exclude": ["node_modules"]
}

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
import path from 'path';

export default defineConfig({
  testDir: './tests',
  timeout: 30000,
  expect: {
    timeout: 5000
  },
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

Using Path Aliases:

// tests/login.spec.ts
import { test } from '@fixtures/fixtures';
import { LoginPage } from '@pages/LoginPage';
import { generateUser } from '@utils/testData';


67. Type-safe Selectors and Fixtures

Question: How do you ensure type safety in Playwright tests?

Answer:

Type-safe Implementation

// types/global.d.ts
declare global {
  interface Window {
    myApp: {
      user: { id: string; name: string };
      config: { apiUrl: string };
    };
  }
}
export {};

// fixtures/types.ts
import { Page, Locator, APIRequestContext } from '@playwright/test';

export interface User {
  id: string;
  email: string;
  name: string;
  role: 'admin' | 'user';
}

export interface TestFixtures {
  loginPage: LoginPage;
  apiClient: APIRequestContext;
  testUser: User;
}

// fixtures/fixtures.ts
import { test as base } from '@playwright/test';
import type { TestFixtures } from './types';

export const test = base.extend<TestFixtures>({
  testUser: async ({}, use) => {
    const user: User = {
      id: '123',
      email: 'test@example.com',
      name: 'Test User',
      role: 'user'
    };
    await use(user);
  },

  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});

// Type-safe page evaluation
test('type-safe evaluate', async ({ page }) => {
  await page.goto('/');

  const userData = await page.evaluate(() => {
    return window.myApp.user; // TypeScript knows the type
  });

  expect(userData.name).toBe('Expected Name');
});

// Type-safe locator helpers
class TypeSafeLocators {
  constructor(private page: Page) {}

  button(name: string): Locator {
    return this.page.getByRole('button', { name });
  }

  input(label: string): Locator {
    return this.page.getByLabel(label);
  }

  testId<T extends string>(id: T): Locator {
    return this.page.getByTestId(id);
  }
}

68. Custom Reporter Implementation

Question: How do you create a custom test reporter?

Answer:

Custom Reporter Implementation

// reporters/CustomReporter.ts
import {
  Reporter,
  TestCase,
  TestResult,
  FullResult,
  Suite
} from '@playwright/test/reporter';

class CustomReporter implements Reporter {
  private results: { name: string; status: string; duration: number }[] = [];

  onBegin(config: any, suite: Suite) {
    console.log(`Starting test run with ${suite.allTests().length} tests`);
  }

  onTestBegin(test: TestCase) {
    console.log(`Starting: ${test.title}`);
  }

  onTestEnd(test: TestCase, result: TestResult) {
    this.results.push({
      name: test.title,
      status: result.status,
      duration: result.duration
    });

    const statusIcon = result.status === 'passed' ? '✓' : '✗';
    console.log(`${statusIcon} ${test.title} (${result.duration}ms)`);

    if (result.status === 'failed') {
      console.log(`  Error: ${result.error?.message}`);
    }
  }

  onEnd(result: FullResult) {
    console.log('\n--- Test Summary ---');
    console.log(`Status: ${result.status}`);

    const passed = this.results.filter(r => r.status === 'passed').length;
    const failed = this.results.filter(r => r.status === 'failed').length;
    const total = this.results.length;

    console.log(`Passed: ${passed}/${total}`);
    console.log(`Failed: ${failed}/${total}`);

    // Could send to external service
    // await this.sendToSlack(this.results);
    // await this.saveToDatabase(this.results);
  }
}

export default CustomReporter;

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [
    ['./reporters/CustomReporter.ts'],
    ['html', { open: 'never' }],
    ['json', { outputFile: 'test-results.json' }]
  ],
});

69. Playwright Config Best Practices

Question: What are the best practices for playwright.config.ts?

Answer:

Complete Config Example

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // Test directory
  testDir: './tests',
  testMatch: '**/*.spec.ts',

  // Timeouts
  timeout: 30 * 1000,
  expect: {
    timeout: 5000,
    toHaveScreenshot: { maxDiffPixels: 100 }
  },

  // Parallel execution
  fullyParallel: true,
  workers: process.env.CI ? 2 : undefined,

  // Retries
  retries: process.env.CI ? 2 : 0,

  // Fail fast in CI
  maxFailures: process.env.CI ? 10 : undefined,

  // Reporter
  reporter: process.env.CI
    ? [['github'], ['html', { open: 'never' }]]
    : [['html'], ['list']],

  // Global setup/teardown
  globalSetup: require.resolve('./global-setup'),
  globalTeardown: require.resolve('./global-teardown'),

  // Shared settings
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',

    // Artifacts
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'retain-on-failure',

    // Browser options
    headless: true,
    viewport: { width: 1280, height: 720 },
    ignoreHTTPSErrors: true,

    // Context options
    locale: 'en-US',
    timezoneId: 'America/New_York',
  },

  // Projects for different browsers/configs
  projects: [
    // Setup project (runs first)
    {
      name: 'setup',
      testMatch: /global\.setup\.ts/,
    },

    // Desktop browsers
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
      dependencies: ['setup'],
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
      dependencies: ['setup'],
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
      dependencies: ['setup'],
    },

    // Mobile browsers
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 5'] },
      dependencies: ['setup'],
    },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 13'] },
      dependencies: ['setup'],
    },
  ],

  // Local dev server
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120 * 1000,
  },
});

70. CI/CD Integration (GitHub Actions, Jenkins)

Question: How do you integrate Playwright tests into CI/CD pipelines?

Answer:

GitHub Actions

# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: Run Playwright tests
        run: npx playwright test
        env:
          BASE_URL: ${{ secrets.BASE_URL }}

      - name: Upload test results
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

      - name: Upload test artifacts
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: test-artifacts
          path: |
            test-results/
            screenshots/

Jenkins Pipeline

// Jenkinsfile
pipeline {
    agent {
        docker {
            image 'mcr.microsoft.com/playwright:v1.40.0-jammy'
        }
    }

    environment {
        CI = 'true'
        BASE_URL = credentials('base-url')
    }

    stages {
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Test') {
            steps {
                sh 'npx playwright test'
            }
            post {
                always {
                    publishHTML([
                        reportDir: 'playwright-report',
                        reportFiles: 'index.html',
                        reportName: 'Playwright Report'
                    ])
                }
            }
        }
    }

    post {
        failure {
            archiveArtifacts artifacts: 'test-results/**/*', allowEmptyArchive: true
        }
    }
}

Docker Setup

# Dockerfile.playwright
FROM mcr.microsoft.com/playwright:v1.40.0-jammy

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npx", "playwright", "test"]
# Run tests in Docker
docker build -f Dockerfile.playwright -t playwright-tests .
docker run --rm playwright-tests

Interview Tips for QA / SDET

Coding Interview Tips

  • Focus on edge cases (empty arrays, null inputs, negative numbers)
  • Write clean, readable code with meaningful variable names
  • Explain your thought process while coding
  • Start with brute force, then optimize
  • Always discuss time and space complexity

Automation Interview Tips

  • Understand the Testing Pyramid and when to use each level
  • Know Page Object Model pattern thoroughly
  • Be ready to write locators using various strategies
  • Explain how you handle flaky tests
  • Discuss CI/CD integration experience

Playwright-Specific Tips

  • Understand auto-waiting vs explicit waits
  • Know the difference between locators and elements
  • Explain fixtures and their scopes
  • Be familiar with network interception
  • Know how to debug using traces

Sources


Document created for QA/SDET interview preparation. Last updated: 2026