JavaScript Tutorial for QA Test Automation with Cypress
What is JavaScript and Why It Matters for QA
JavaScript has evolved from a simple browser scripting language into a full-featured programming language that powers the entire web. Originally created in 1995 by Brendan Eich, JavaScript has become ubiquitous — it runs on every major browser, and with Node.js, it also runs on servers and local machines.
For QA automation, JavaScript is increasingly critical because modern web applications are built with JavaScript frameworks (React, Vue, Angular). Moreover, modern testing tools like Cypress and Playwright are built in JavaScript, making JavaScript knowledge essential for modern QA teams. If you want to work with cutting-edge testing technologies, JavaScript fluency is no longer optional — it's essential.
JavaScript in Modern QA Automation
JavaScript's role in QA automation has expanded dramatically over the past few years. While Java + Selenium dominated for years, the rise of JavaScript-based tools has given QA teams new, powerful options.
JS vs Java for QA
| Aspect | JavaScript | Java |
|---|---|---|
| Type system | Dynamic (runtime errors) | Static (compile-time errors) |
| Best framework | Cypress, Playwright | Selenium + TestNG |
| Setup speed | Faster (npm install) | Slower (Maven, JDK) |
| Async handling | Native async/await | Needs explicit handling |
What is the difference between var, let, and const?
var is function-scoped and hoisted. let is block-scoped and mutable. const is block-scoped and cannot be reassigned (but object contents can change).
What is a Promise?
A Promise represents a future value — the result of an async operation. It can be pending, resolved (fulfilled), or rejected.
What is the difference between == and ===?
== does type coercion (1 == "1" is true). === is strict equality — both value and type must match (1 === "1" is false). Always use === in tests.
// Declaration keywords const baseUrl = 'https://thetechworldlabs.com' // immutable let testCount = 0 // mutable var legacy = 'avoid this' // function-scoped — avoid // Primitive Types typeof "hello" // "string" typeof 42 // "number" typeof true // "boolean" typeof null // "object" ← famous JS quirk typeof undefined // "undefined" typeof Symbol('id') // "symbol" // Object Types typeof {} // "object" typeof [] // "object" ← arrays are objects! typeof (() => {}) // "function" // Template literals const msg = `Running test ${testCount} on ${baseUrl}` const multiline = `Line 1 Line 2 Line 3`
// Arithmetic operators 10 + 5 // 15 (addition) 10 - 5 // 5 (subtraction) 10 * 3 // 30 (multiplication) 10 / 2 // 5 (division) 10 % 3 // 1 (modulus - remainder) 2 ** 3 // 8 (exponentiation) // Assignment operators let x = 5 x += 3 // x = 8 (add and assign) x -= 2 // x = 6 (subtract and assign) x *= 2 // x = 12 (multiply and assign) x /= 4 // x = 3 (divide and assign) // Increment / Decrement x++ // x = 4 (increment) ++x // x = 5 (pre-increment) x-- // x = 4 (decrement)
// Comparison operators (return boolean) 5 === 5 // true (strict equality) 5 == '5' // true (loose equality - avoid!) 5 !== 6 // true (strict inequality) 5 > 3 // true (greater than) 5 >= 5 // true (greater than or equal) 5 < 10 // true (less than) 5 <= 5 // true (less than or equal) // Logical operators true && true // true (AND - both must be true) true || false // true (OR - at least one true) !true // false (NOT - negation) // Short-circuit evaluation const name = user && user.name // name = user.name if user exists const fallback = input || 'default' // 'default' if input is falsy // Nullish coalescing operator const value = null ?? 'default' // 'default' (only null/undefined) const value = 0 ?? 'default' // 0 (0 is not nullish)
// String concatenation 'Hello' + ' ' + 'World' // "Hello World" 'Item: ' + 5 // "Item: 5" (number converts to string) // Conditional (ternary) operator const status = age >= 18 ? 'adult' : 'minor' // typeof operator typeof 42 // "number" typeof 'hello' // "string" typeof true // "boolean" typeof {} // "object" typeof undefined // "undefined" // instanceof operator [] instanceof Array // true {} instanceof Object // true
// String coercion 5 + '5' // "55" (number coerced to string) '5' + 5 // "55" '$' + 100 + 50 // "$10050" (left to right) // Numeric coercion 10 - '5' // 5 (string coerced to number) 10 * '2' // 20 '10' / '2' // 5 // Boolean coercion in conditions if (1) { } // true (truthy) if (0) { } // false (falsy) if ('') { } // false (empty string is falsy) if ('0') { } // true (non-empty string is truthy!) if (null) { } // false (falsy) if (undefined) { } // false (falsy) if (NaN) { } // false (falsy)
// Convert to String String(123) // "123" String(true) // "true" (123).toString() // "123" // Convert to Number Number('123') // 123 Number('123.45') // 123.45 Number('abc') // NaN (not a number) parseInt('123px', 10) // 123 (parse until non-digit) parseFloat('3.14e2') // 314 (parse float) +'123' // 123 (unary plus operator) // Convert to Boolean Boolean(1) // true Boolean(0) // false Boolean('hello') // true Boolean('') // false !!'hello' // true (double negation) !!0 // false
// Loose equality (==) - coerces types 5 == '5' // true (string coerced to number) 0 == false // true (false coerced to 0) null == undefined // true (special case) '' == 0 // true (empty string coerced) '0' == false // true (tricky!) // Strict equality (===) - NO coercion - ALWAYS USE THIS! 5 === '5' // false (different types) 0 === false // false (different types) null === undefined // false // Common QA gotchas const userInput = '123' userInput === 123 // false - string !== number Number(userInput) === 123 // true - now they match!
// String manipulation const email = ' USER@EXAMPLE.COM ' email.trim() // "USER@EXAMPLE.COM" email.toLowerCase() // " user@example.com " email.includes('@') // true email.startsWith('USER') // true email.split('@') // [' USER', 'EXAMPLE.COM '] email.replace('COM', 'NET') // " USER@EXAMPLE.NET " email.substring(2, 6) // "USER"
// Basic regex patterns for QA testing const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ emailRegex.test('test@example.com') // true emailRegex.test('invalid-email') // false // Common patterns const phoneRegex = /^\d{3}-\d{3}-\d{4}$/ // 123-456-7890 const urlRegex = /^https?:\/\/.+\..+$/ // URLs const alphaNumeric = /^[a-zA-Z0-9]+$/ // Only letters and numbers // Extracting data with groups const dateStr = '2024-12-25' const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})/) match[1] // "2024" match[2] // "12" match[3] // "25" // Find all matches const text = 'Test123 Error456 Failure789' text.match(/\d+/g) // ["123", "456", "789"]
const browsers = ['chrome', 'firefox', 'edge'] // Iteration methods browsers.forEach(b => console.log(b)) // iterate const upper = browsers.map(b => b.toUpperCase()) // transform const only = browsers.filter(b => b !== 'edge') // filter const found = browsers.find(b => b === 'chrome') // find first const has = browsers.includes('firefox') // check existence // Array mutations browsers.push('safari') // add to end browsers.pop() // remove last browsers.unshift('opera') // add to start browsers.slice(0, 2) // copy portion (non-mutating) browsers.splice(1, 1) // remove at index // Reduce - sum array [1, 2, 3, 4].reduce((sum, n) => sum + n, 0) // 10
// Object literals for test data const testUser = { id: 1, email: 'test@example.com', password: 'secure123', active: true, roles: ['user', 'admin'] } // Accessing properties testUser.email // "test@example.com" testUser['password'] // "secure123" testUser.roles[0] // "user" // Destructuring objects const { email, password } = testUser const { email: userEmail } = testUser // rename // Spread operator const newUser = { ...testUser, email: 'new@example.com', id: 2 } // Object methods Object.keys(testUser) // ["id", "email", "password", "active", "roles"] Object.values(testUser) // [1, "test@example.com", "secure123", true, [...]] Object.entries(testUser) // [["id", 1], ["email", "test@example.com"], ...]
// If/else statements if (status === 200) { console.log('Success') } else if (status === 404) { console.log('Not found') } else { console.log('Error') } // Ternary operator const result = status === 200 ? 'pass' : 'fail' // Switch statement switch (userRole) { case 'admin': hasAccess = true break case 'user': hasAccess = false break default: hasAccess = false } // Logical operators if (email && password) { // AND login() } if (skip || retry) { // OR handleTest() } if (!error) { // NOT success() }
// For loop for (let i = 0; i < 5; i++) { console.log(i) // 0, 1, 2, 3, 4 } // While loop let count = 0 while (count < 3) { console.log(count) count++ } // For-of loop (arrays) for (const browser of ['chrome', 'firefox']) { console.log(browser) } // For-in loop (object keys) for (const key in testUser) { console.log(key, testUser[key]) }
// Traditional function declaration function login(user, pass) { return `${user}:${pass}` } // Function expression const logout = function(user) { console.log(`${user} logged out`) } // Arrow function (preferred in tests) const validate = (email) => email.includes('@') // Arrow with multiple parameters const buildHeaders = (token, type) => ({ 'Authorization': `Bearer ${token}`, 'Content-Type': type }) // Arrow with body const fetchUser = async (id) => { const res = await fetch(`/api/users/${id}`) return res.json() } // Default parameters const visit = (path = '/') => cy.visit(baseUrl + path) // Rest parameters const combine = (...items) => items.join(',') combine('a', 'b', 'c') // "a,b,c"
// Basic async/await const getUser = async (id) => { try { const response = await fetch(`/api/users/${id}`) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } const data = await response.json() return data } catch (err) { console.error('API Error:', err.message) throw err } } // Sequential requests const workflow = async () => { const user = await getUser(1) const posts = await getPosts(user.id) return { user, posts } } // Parallel requests with Promise.all const parallel = async () => { const [user, settings] = await Promise.all([ getUser(1), getSettings() ]) return { user, settings } }
// Creating a promise const myPromise = new Promise((resolve, reject) => { const success = true if (success) { resolve('Operation succeeded') } else { reject(new Error('Operation failed')) } }) // Using .then() / .catch() getUser(1) .then(user => console.log(user)) .catch(err => console.error(err)) // Promise.race - first to complete Promise.race([fetchA(), fetchB()]) .then(winner => console.log('First result:', winner))
// GET request const res = await fetch('/api/users') const data = await res.json() // POST request with body const res = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'QA User', role: 'tester' }) }) const created = await res.json() // PUT request (update) await fetch(`/api/users/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Updated' }) }) // DELETE request await fetch(`/api/users/${id}`, { method: 'DELETE' }) // With authorization header const token = 'your-jwt-token' await fetch('/api/protected', { headers: { 'Authorization': `Bearer ${token}` } })
// utils.test.js const { isEmail, formatPrice } = require('./utils') describe('Utility Functions', () => { test('validates correct email', () => { expect(isEmail('test@example.com')).toBe(true) expect(isEmail('notanemail')).toBe(false) }) test('formats price correctly', () => { expect(formatPrice(1500)).toBe('NPR 1,500') }) test('API call returns user', async () => { const user = await fetchUser(1) expect(user).toHaveProperty('email') expect(user.id).toBe(1) }) test('thrown error is caught', () => { expect(() => throwError()).toThrow('Expected error') }) })
| Matcher | Assertion |
|---|---|
.toBe(val) | Strict equality (===) |
.toEqual(obj) | Deep equality for objects/arrays |
.toBeTruthy() | Value is truthy |
.toBeFalsy() | Value is falsy |
.toContain(val) | Array or string contains value |
.toHaveLength(n) | Array/string has length |
.toThrow() | Function throws an error |
.toHaveProperty(key) | Object has property |
// Select elements document.getElementById('submit') document.querySelector('.error-msg') // first match document.querySelectorAll('table tbody tr') // NodeList // Read properties element.textContent // text content element.innerText // visible text (respects CSS) element.innerHTML // HTML content element.value // input value element.getAttribute('href') element.getAttribute('data-id') // Modify DOM element.textContent = 'Updated' element.style.display = 'none' element.setAttribute('disabled', '') element.classList.add('active') element.classList.remove('disabled') element.classList.toggle('highlight') // Create and append elements const newDiv = document.createElement('div') newDiv.textContent = 'New Element' document.body.appendChild(newDiv)
// Add event listener const btn = document.getElementById('submit') btn.addEventListener('click', () => { console.log('Button clicked') }) // Remove event listener const handler = (e) => console.log('Clicked') btn.addEventListener('click', handler) btn.removeEventListener('click', handler) // Common events element.addEventListener('input', (e) => { console.log('Input value:', e.target.value) }) element.addEventListener('change', (e) => { console.log('Changed to:', e.target.value) }) element.addEventListener('submit', (e) => { e.preventDefault() console.log('Form submitted') }) // Mouse events element.addEventListener('mouseover', () => console.log('Hover')) element.addEventListener('mouseout', () => console.log('Leave')) element.addEventListener('dblclick', () => console.log('Double click'))
// Global scope const global = 'accessible everywhere' // Function scope (var only) function test() { var funcScoped = 'only in function' } // Block scope (let, const) { let blockScoped = 'only in this block' } // Shadowing - inner scope overrides outer const x = 1 { const x = 2 // shadows outer x console.log(x) // 2 } console.log(x) // 1
// Closure: function accesses outer scope function makeCounter() { let count = 0 // captured in closure return () => { count++ return count } } const counter = makeCounter() counter() // 1 counter() // 2 counter() // 3 // Data privacy with closures function createUser(name, pwd) { return { getName: () => name, checkPassword: (attempt) => attempt === pwd // pwd is private } } const user = createUser('Alice', 'secret') user.getName() // "Alice" user.checkPassword('secret') // true // Cannot access pwd directly!
// Basic try-catch-finally try { const result = JSON.parse('invalid json') } catch (err) { console.error('Parse error:', err.message) } finally { console.log('Cleanup code runs always') } // Error in async function async function safeFetch(url) { try { const res = await fetch(url) if (!res.ok) throw new Error(`HTTP ${res.status}`) return await res.json() } catch (err) { console.error('Fetch failed:', err) return null } } // Custom error class class ValidationError extends Error { constructor(message, field) { super(message) this.field = field this.name = 'ValidationError' } } throw new ValidationError('Invalid email', 'email')
// ES6 Class syntax class TestCase { constructor(name, timeout = 5000) { this.name = name this.timeout = timeout this.status = 'pending' } run() { this.status = 'running' console.log(`Running: ${this.name}`) } pass() { this.status = 'passed' } // Getter get isPassed() { return this.status === 'passed' } // Static method static create(name) { return new TestCase(name) } } // Inheritance class APITest extends TestCase { constructor(name, endpoint) { super(name) this.endpoint = endpoint } run() { super.run() console.log(`Testing endpoint: ${this.endpoint}`) } } const test = new APITest('Get Users', '/api/users') test.run()
// Map - transform each element const numbers = [1, 2, 3, 4] const doubled = numbers.map(n => n * 2) // [2, 4, 6, 8] const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] const names = users.map(u => u.name) // ["Alice", "Bob"] // Filter - keep matching elements const evens = numbers.filter(n => n % 2 === 0) // [2, 4] const admins = users.filter(u => u.role === 'admin') // Reduce - combine elements into single value const sum = numbers.reduce((acc, n) => acc + n, 0) // 10 const product = numbers.reduce((acc, n) => acc * n, 1) // Chaining operations [1, 2, 3, 4, 5] .filter(n => n > 2) // [3, 4, 5] .map(n => n * 10) // [30, 40, 50] .reduce((s, n) => s + n, 0) // 120
// Function that returns a function const multiply = (x) => (y) => x * y const double = multiply(2) double(5) // 10 // Compose - combine functions const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x) const add1 = (x) => x + 1 const double = (x) => x * 2 const pipe = compose(double, add1) pipe(5) // (5 + 1) * 2 = 12 // Decorator pattern const withRetry = (fn, retries = 3) => async (...args) => { for (let i = 0; i < retries; i++) { try { return await fn(...args) } catch (err) { if (i === retries - 1) throw err } } } const safeAPI = withRetry(fetchUser, 3)
// math.js - Export functions export const add = (a, b) => a + b export const multiply = (a, b) => a * b export default class Calculator { constructor(initial = 0) { this.value = initial } add(n) { this.value += n; return this } } // test.js - Import functions import Calculator, { add, multiply } from './math.js' console.log(add(2, 3)) // 5 const calc = new Calculator(10) calc.add(5) // Calculator { value: 15 }
// utils.js - CommonJS exports const isEmail = (str) => /^.+@.+\..+$/test(str) const formatDate = (d) => new Date(d).toLocaleDateString() module.exports = { isEmail, formatDate } // test.js - CommonJS imports const { isEmail, formatDate } = require('./utils') console.log(isEmail('test@example.com')) // true
Best Practices
Common Pitfalls
Callback Hell
Cause: Deeply nested callbacks. Solution: Use Promises or async/await for cleaner code.
this binding issues
Cause: Incorrect context in methods. Solution: Use arrow functions or .bind() to fix 'this'.
Unhandled promise rejections
Cause: Not catching promise errors. Solution: Add .catch() or try-catch in async/await.
Memory leaks
Cause: Circular references or event listeners not removed. Solution: Always clean up listeners and reset references.
FAQs
What's the difference between null and undefined?
undefined: Variable declared but not assigned. null: Intentionally set to 'no value'. Always check for both.
How do I work with dates in JavaScript?
Use Date object or modern libraries like Moment.js, date-fns. Cypress has built-in date/time utilities.
What's a closure?
Function with access to outer scope variables. Essential for data privacy and event handlers in Cypress tests.
How do I fetch data from APIs?
Use fetch() API or axios library. Parse JSON responses with .json() or response.data.
What's the Event Loop?
Mechanism handling synchronous code, callbacks, promises, and timers. Understanding it is crucial for debugging async issues.
How do I debug JavaScript?
Use browser DevTools, console.log(), debugger statement, or VS Code debugger for Node.js scripts.