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¶
- Part 1: JavaScript Fundamentals (Q1-12)
- Part 2: JavaScript Concepts (Q13-22)
- Part 3: Array & Object Manipulation (Q23-32)
- Part 4: Data Structures & Algorithms (Q33-40)
- Part 5: Testing & Automation Concepts (Q41-50)
- 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
Interview Follow-up
What's the time complexity? → O(n) for all methods since we iterate through the string once.
Try it yourself
2. Reverse a Number¶
Problem: Reverse an integer number, handling negative numbers.
Example:
- Input: 1234 → Output: 4321
- Input: -123 → Output: -321
Solutions
Complexity
Time: O(log n) - number of digits | Space: O(1)
Try it yourself
3. Check if String is Palindrome¶
Problem: Check whether a string reads the same forwards and backwards.
Example:
- "madam" → true
- "hello" → false
Solutions
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
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
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
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
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
Complexity
Time: O(n) - single pass | Space: O(1)
Try it yourself
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
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
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
Complexity
Method 1: Time O(n), Space O(k) where k = unique chars | Method 2: Time O(n²), Space O(1)
Try it yourself
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
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
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;
}
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?
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
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
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;
}
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;
}
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
// 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;
}
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.lengthby 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 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;
}
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 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;
}
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:
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;
}
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
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):
- Business Impact
- Revenue-critical features (checkout, payments)
- User-facing functionality
-
Core business logic
-
Frequency of Use
- Features used daily by most users
-
Happy path scenarios
-
Defect Probability
- Recently changed code
- Complex business logic
-
Areas with historical bugs
-
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
2. Database Seeding
3. API-Driven Setup
4. Fixtures File
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
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
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
When Auto-wait Isn't Enough
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
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
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
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
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¶
- InterviewBit - Top SDET Interview Questions
- Naukri - Top 50 SDET Interview Questions
- LambdaTest - Playwright Interview Questions
- TestLeaf - Top 30 Playwright Interview Questions
- Software Testing Material - 70+ Playwright Questions
- GeeksforGeeks - SDET Interview Questions
- Tech Interview Handbook - Array Cheatsheet
- LeetCode Top Interview Questions
- Playwright Official Documentation
Document created for QA/SDET interview preparation. Last updated: 2026