Advanced Web Technologies for QA
- In modern web development, Quality Assurance (QA) goes beyond traditional testing techniques. Advanced web technologies like Advanced JavaScript, Asynchronous Operations, and Single Page Applications (SPA) introduce complexities that QA engineers must address.
Advanced JavaScript for QA
- JavaScript (JS) is at the core of web applications, and QA engineers need to understand its advanced features to ensure effective testing.
Closures
-
Closures retain access to variables even after the outer function has executed. QA’s need to understand memory leaks and variable persistence in test automation.
-
A closure is a function that remembers variables from its outer scope even after the outer function has finished executing.
-
A closure is created when a nested function remembers the variables of its parent function even after the parent function has returned.
-
Here, even though the outerFunction has finished execution, the returned innerFunction still remembers the counter because of closures.
function outerFunction() {
let counter = 0; // This variable is "remembered" by innerFunction()
return function innerFunction() {
counter++;
console.log(counter);
};
}
const increment = outerFunction(); // outerFunction runs and returns innerFunction
increment(); // counter is 1
increment(); // counter is 2
increment(); // counter is 3
Scoping
- Scoping defines where variables are accessible in your code.
Types of Scope in JavaScript
- Global Scope - A variable declared outside any function is globally scoped. Accessible anywhere in the script.
let globalVar = "I am global";
function example() {
console.log(globalVar); // Accessible here
}
console.log(globalVar); // Accessible here too
- Function Scope - Variables declared inside a function are not accessible outside.
function example() {
let functionScoped = "I exist only inside this function";
console.log(functionScoped); // Accessible here
}
console.log(functionScoped); // ReferenceError, not accessible here
- Block Scope (let and const) - Variables declared with let or const inside are limited to that block.
if (true) {
let blockScoped = "Inside if block";
console.log(blockScoped); // Accessible here
}
console.log(blockScoped); // ReferenceError, not accessible here
- Lexical Scope - Inner functions have access to variables in outer functions.
function outer() {
let outerVar = "I am from outer";
function inner() {
console.log(outerVar); // Accessible due to lexical scope
}
inner();
}
outer();
Prototypes
-
JavaScript uses prototypes instead of traditional class-based inheritance. Understanding prototypes is key to mastering JavaScript's Object-Oriented Programming (OOP). Every JavaScript object has an internal property called Prototype, which points to another object. This forms the prototype chain, allowing objects to inherit properties and methods. Understanding how objects behave will help the QA test that are better structured.
-
Here, the user object does not have a greet() method, but it can access greet() from a person object via the prototype chain.
const person = {
greet: () => {
console.log("Hello!");
},
};
const user = Object.create(person); // `user` inherits from `person`
user.greet(); // Output: "Hello!"
Prototype Chain
-
When you access a property or method on an object:
-
JavaScript will look for it in the object itself.
-
If not found, it checks the object's [[Prototype]].
-
This continues up the prototype chain until it finds the property or reaches null.
-
-
Here, dog.speak() works because:
-
The dog object doesn’t have a speak() method.
-
JavaScript checks dog.__proto__ (which is Animal.prototype) and finds the speak() method there.
-
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = () => {
console.log(`${this.name} makes a noise`);
};
const dog = new Animal("Dog");
dog.speak(); // Output: "Dog makes a noise"
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
Object-Oriented Programming (OOP)
- JavaScript supports OOP through constructor functions, prototypes, and now ES6 classes.
Constructor Functions (Pre-ES6)
-
Before ES6, OOP in JavaScript was done using constructor functions.
-
The Car function acts as a constructor and the method start() is added to Car.prototype, making it shared across all instances.
function Car(brand, model) {
this.brand = brand;
this.model = model;
}
Car.prototype.start = () => {
console.log(`${this.brand} ${this.model} is starting...`);
};
const myCar = new Car("Toyota", "Corolla");
myCar.start(); // Output: "Toyota Corolla is starting..."
ES6 Classes (Syntactic Sugar)
-
ES6 introduced class syntax, which is syntactic sugar over prototypes.
-
Under the hood, JavaScript still uses prototypes, but the syntax is cleaner.
class Car {
constructor(brand, model) {
this.brand = brand;
this.model = model;
}
start() {
console.log(`${this.brand} ${this.model} is starting...`);
}
}
const myCar = new Car("Tesla", "Model 3");
myCar.start(); // Output: "Tesla Model 3 is starting..."
Inheritance in JavaScript
- Inheritance allows one class (or function) to derive properties and methods from another.
Prototype-Based Inheritance
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = () => {
console.log(`${this.name} makes a noise`);
};
// Dog inherits from Animal
function Dog(name, breed) {
Animal.call(this, name); // Call the parent constructor
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype); // Inherit methods
Dog.prototype.constructor = Dog;
Dog.prototype.bark = () => {
console.log(`${this.name} barks!`);
};
const myDog = new Dog("Buddy", "Labrador");
myDog.speak(); // Output: "Buddy makes a noise"
myDog.bark(); // Output: "Buddy barks!"
ES6 Class-Based Inheritance
- Here, Dog extends Animal, inheriting its properties and methods while adding bark().
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
bark() {
console.log(`${this.name} barks!`);
}
}
const myDog = new Dog("Max", "Golden Retriever");
myDog.speak(); // Output: "Max makes a noise"
myDog.bark(); // Output: "Max barks!"
Higher-Order Functions and Functional Programming
- JavaScript supports functional programming (FP), where functions are treated as first-class citizens. This allows the use of higher-order functions (HOFs), which are essential in FP. For QA’s this is useful in test automation, making code reusable and modular.
Higher-Order Functions (HOFs)
-
A higher-order function is a function that does one or both of the following:
-
Takes a function as an argument.
-
Returns a function as its result.
-
Example: A Function Taking Another Function.
-
function operate(a, b, operation) {
return operation(a, b);
}
function add(x, y) {
return x + y;
}
// Here, operate is a higher-order function because it takes add as an argument.
console.log(operate(5, 3, add)); // Output: 8
- Example: A Function Returning Another Function.
function multiplier(factor) {
return (number) => {
return number * factor;
};
}
const double = multiplier(2);
// Here, multiplier(2) returns a new function, making it a higher-order function.
console.log(double(5)); // Output: 10
Functional Programming (FP)
- Functional programming is a declarative paradigm that treats computation as the evaluation of functions.
Core Principles of Functional Programming
-
First-Class Functions - Functions can be assigned to variables, passed as arguments, and returned.
-
Pure Functions - A function that always returns the same output for the same input, with no side effects.
-
Immutability - Data should not be modified; instead, return new copies.
-
Function Composition - Combining small functions to build complex behavior.
Common Higher-Order Functions in JavaScript
- JavaScript has built-in higher-order functions that are widely used in functional programming.
map()
- Transforming an Array, Creates a new array by applying a function to each element.
const numbers = [1, 2, 3, 4];
const squared = numbers.map((num) => num * num);
console.log(squared); // Output: [1, 4, 9, 16]
filter()
- Filtering an Array, Creates a new array with elements that pass a condition.
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter((num) => num % 2 === 0);
console.log(evenNumbers); // Output: [2, 4, 6]
reduce()
- Reducing an Array to a Single Value, Applies a function to accumulate values into a single result.
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // Output: 10
forEach()
- Iterating Over an Array, Executes a function for each element but does not return a new array.
const numbers = [1, 2, 3];
numbers.forEach((num) => console.log(num * 2)); // Output: 2, 4, 6
forEach() is not functional programming-friendly because it does not return a value.
sort()
- Sorting an Array, Sorts elements in place (modifies the original array).
const numbers = [4, 1, 7, 3];
numbers.sort((a, b) => a - b); // Ascending order
console.log(numbers); // Output: [1, 3, 4, 7]
Function Composition (Combining Functions)
-
In functional programming, function composition allows combining multiple functions into one.
-
Instead of calling functions separately, composition makes it declarative.
const add5 = (x) => x + 5;
const double = (x) => x * 2;
const addAndDouble = (x) => double(add5(x));
console.log(addAndDouble(3)); // (3 + 5) * 2 = 16
Pure Functions vs. Impure Functions
Pure Function (No Side Effects)
-
Returns the same output for the same input.
-
Has no side effects (Does not modify external state).
function pureAdd(a, b) {
return a + b;
}
console.log(pureAdd(2, 3)); // 5
console.log(pureAdd(2, 3)); // Always 5
Impure Function (Has Side Effects)
- An impure function modifies an external variable.
let count = 0;
function increment() {
count++; // Side effect (modifying external state)
return count;
}
console.log(increment()); // 1
console.log(increment()); // 2 (Different result)
Avoid impure functions in functional programming.
Currying (Breaking Functions into Smaller Parts)
-
Currying transforms a function that takes multiple arguments into a sequence of functions, each taking one argument.
-
This allows partial application (Fixing one argument at a time).
function multiply(a) {
return (b) => {
return a * b;
};
}
const double = multiply(2);
console.log(double(4)); // Output: 8
ES6+ Features in JavaScript (ECMAScript 2015 and Beyond)
- ES6 (ECMAScript 2015) introduced major improvements to JavaScript, making it more powerful, readable, and maintainable. Subsequent updates (ES7, ES8, etc.) added even more useful features.
let and const (Block-Scoped Variables)
-
Before ES6, var was the only way to declare variables, but it had function scope issues.
-
ES6 introduced:
-
let - Block-scoped, mutable
-
const - Block-scoped, immutable
-
if (true) {
let x = 10;
const y = 20;
console.log(x, y); // Accessible inside block
}
console.log(x, y); // ReferenceError (block-scoped)
Arrow Functions (=>)
- A shorter syntax for defining functions, with automatic this object binding.
// Traditional function
function add(a, b) {
return a + b;
}
// Arrow function (implicit return)
const addArrow = (a, b) => a + b;
console.log(addArrow(5, 3)); // Output: 8
Template Literals (Backticks ` `)
- Supports multi-line strings and interpolation.
const name = "Alice";
console.log(`Hello, ${name}!`); // Output: Hello, Alice!
Destructuring Assignment
- Extract values from objects or arrays easily.
// Array Destructuring
const numbers = [1, 2, 3];
const [first, second] = numbers;
console.log(first, second); // Output: 1, 2
// Object Destructuring
const user = { name: "Bob", age: 25 };
const { name, age } = user;
console.log(name, age); // Output: Bob 25
Default Parameters
- Set default values for function parameters.
function greet(name = "Guest") {
console.log(`Hello, ${name}`);
}
greet(); // Output: Hello, Guest
Spread (...)
- Expanding Arrays and Objects
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // Expanding `arr1`
console.log(arr2); // Output: [1, 2, 3, 4, 5]
const user = { name: "Alice", age: 25 };
const newUser = { ...user, role: "Admin" }; // Expanding `user`
console.log(newUser); // Output: { name: 'Alice', age: 25, role: 'Admin' }
Rest (...)
- Collecting Arguments
function sum(...nums) {
return nums.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3, 4)); // Output: 10
Object Property Shorthand
- If variable names match object properties, you can skip key-value pairs.
const name = "Charlie";
const age = 30;
const user = { name, age };
console.log(user); // Output: { name: 'Charlie', age: 30 }
Object and Array Methods (Object.entries(), Object.values(), includes())
// Object Methods
const user = { name: "Alice", age: 25 };
console.log(Object.keys(user)); // Output: ['name', 'age']
console.log(Object.values(user)); // Output: ['Alice', 25]
console.log(Object.entries(user)); // Output: [['name', 'Alice'], ['age', 25]]
// Array includes()
const fruits = ["apple", "banana", "orange"];
console.log(fruits.includes("banana")); // Output: true
Promises and Async/Await
Promises (ES6) - Used for handling asynchronous operations.
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => resolve("Data received"), 2000);
});
fetchData.then((data) => console.log(data)); // Output (after 2 sec): Data received
Async / Await (ES8) - A cleaner way to handle async operations.
async function fetchData() {
return "Data received";
}
console.log(await fetchData()); // Output: Data received
Modules (import / export)
Exporting a Module
export const greet = (name) => `Hello, ${name}!`;
Importing a Module
import { greet } from "./module.js";
console.log(greet("Alice")); // Output: Hello, Alice!
Optional Chaining (?.)
- Prevents errors when accessing nested properties.
const user = { profile: { name: "John" } };
console.log(user.profile?.name); // Output: John
console.log(user.address?.city); // Output: undefined (No error)
Nullish Coalescing (??)
- Returns the right-hand side when the left-hand value is null or undefined.
let user = null;
console.log(user ?? "Guest"); // Output: Guest
let age = 0;
console.log(age ?? 18); // Output: 0 (Because 0 is not null or undefined)
BigInt (Handling Large Numbers)
- Handles numbers bigger than Number.MAX_SAFE_INTEGER.
const bigNumber = 9007199254740991n;
console.log(bigNumber + 1n); // Output: 9007199254740992n
Dynamic Imports (ES2020)
- Load JavaScript modules dynamically.
import("./module.js").then(module => {
console.log(module.greet("Alice"));
});
Logical Assignment Operators (ES2021) - Shorter ways to assign values.
let name = null;
name ||= "Guest"; // Assign only if `name` is falsy
console.log(name); // Output: Guest
String .replaceAll() (ES2021)
- Replaces all occurrences of a substring.
const text = "JavaScript is great, and JavaScript is powerful!";
console.log(text.replaceAll("JavaScript", "JS")); // Output: "JS is great, and JS is powerful!"
Single Page Application (SPA)
- A Single Page Application (SPA) is a web application that loads a single HTML page and dynamically updates the content without refreshing the entire page as the user interacts with the app.
Key Characteristics of a SPA
-
Client-Side Routing - Navigation between views is handled by JavaScript such as React Router or Vue Router instead of the server.
-
Dynamic Content Loading - Data is fetched from the backend usually via APIs and rendered dynamically using JavaScript.
-
No Full Page Reloads - Only part of the page updates; the browser doesn’t reload the entire document.
-
Improved User Experience - SPAs feel faster and more responsive after the initial load.
- React, Angular, Vue.js, and Svelte are examples of SPA frameworks.
SPA Workflow
-
The browser loads index.html and a large JavaScript bundle.
-
JavaScript framework takes over rendering and routing.
-
Data is requested from APIs (usually REST or GraphQL).
-
The DOM updates dynamically based on user interaction.
SPA Testing
Unit Testing
-
Test small, isolated pieces of logic such as components or functions.
-
Use tools such as Jest or Jasmine to test a button component's onClick handler or validate form input functions or reducers.
Integration Testing
-
Test how components work together and connect to services.
-
Focus on real user flows without full browser automation.
-
Good for testing form validation + submission logic.
-
Use Cypress to fill out a form, check validation, and submit it.
End-to-End (E2E) Testing
-
Test full user flows in a real browser.
-
Slowest, but most realistic.
-
Covers routing, navigation, backend communication, auth, etc.
-
Use Cypress or Selenium to simulate: User logs in, navigates to dashboard, edits profile.
API & Network Testing
-
Test how your SPA handles API responses.
-
Mock APIs in frontend tests, or test APIs separately using Postman.
-
Check SPA’s behavior for Success(200, 201), Errors (404, 500) and Loading states.
Routing Tests
-
Test SPA-specific client-side navigation.
-
Verify if the URL change without reload? Are users redirected properly? or Is deep-linking ie: /profile/123 handled?
-
Use Cypress for E2E routing tests
Accessibility Testing
-
Ensure your SPA is usable for everyone.
-
Use axe to verify if the app is accessible.