/** * ======================================================================== * A/B Testing Standard Snippet Template * ======================================================================== * This is the standard template for all A/B testing variations. * Use this structure to ensure consistency across all tests. */ (function() { try { /* Main variables */ var debug = 0; // Set to 1 to enable console logging for debugging var variation_name = ""; // Name of your test variation (e.g., "homepage-hero-test-v1") /* All Pure helper functions */ function waitForElement(selector, trigger, delayInterval = 50, delayTimeout = 15000) { var interval = setInterval(function() { if (document && document.querySelector(selector) && document.querySelectorAll(selector).length > 0) { clearInterval(interval); trigger(document.querySelector(selector)); } }, delayInterval); setTimeout(function() { clearInterval(interval); }, delayTimeout); } /* Variation functions - Write your test logic here */ /* Variation Init - This is where your test starts */ function init() { /* Start your code here */ console.log("Test initialized. Please check if functionality is triggering correctly."); } /* Initialize variation */ waitForElement("body", init); } catch (e) { if (debug) console.log(e, "error in Test " + variation_name); } })(); /** * ======================================================================== * LIBRARY FUNCTIONS FOR A/B TESTING * ======================================================================== * All functions below are pure functions (no side effects, same input = same output) * that can be used in your A/B tests. Copy only what you need. */ // ======================================================================== // 1. DOM MANIPULATION & OBSERVATION // ======================================================================== /** * #1.1 Wait For Element * * USE WHEN: You need to wait for an element to appear in the DOM before executing code. * COMMON USE CASE: Single Page Applications (SPAs), dynamically loaded content, lazy-loaded images * * EXAMPLE: * waitForElement('.product-title', (element) => { * element.textContent = 'New Title'; * }); * * @param {string} selector - CSS selector for the element to wait for * @param {Function} trigger - Callback function to execute when element is found * @param {number} [delayInterval=50] - How often to check for element (in milliseconds) * @param {number} [delayTimeout=15000] - How long to wait before giving up (in milliseconds) */ function waitForElement(selector, trigger, delayInterval = 50, delayTimeout = 15000) { var interval = setInterval(function() { if (document && document.querySelector(selector) && document.querySelectorAll(selector).length > 0) { clearInterval(interval); trigger(document.querySelector(selector)); } }, delayInterval); setTimeout(function() { clearInterval(interval); }, delayTimeout); } /** * #1.2 Observe Selector (Advanced DOM Observer) * * USE WHEN: You need to watch for multiple elements that might be added/removed dynamically * DIFFERENCE FROM waitForElement: This continues watching and can handle multiple elements * COMMON USE CASE: Product listing pages, infinite scroll, search results * DEPENDS ON: debounce() function (include it in your code) * * EXAMPLE: * const stopObserving = observeSelector('.product-card', (element) => { * console.log('New product card found:', element); * }); * // Later, if needed: stopObserving(); * * @param {string} selector - CSS selector to observe * @param {Function} callback - Function to call for each matching element found * @param {Object} [options={}] - Configuration options * @param {Document} [options.document=window.document] - The document to observe * @param {boolean} [options.once=false] - If true, stops observing after first match * @returns {Function} - A function to stop observing when called */ function observeSelector(selector, callback, options = {}) { const document = options.document || window.document; const processed = new Map(); if (options.timeout || options.onTimeout) { throw `observeSelector options \`timeout\` and \`onTimeout\` are not yet implemented.`; } let obs; let isDone = false; const done = () => { if (obs) obs.disconnect(); isDone = true; }; const processElement = el => { if (!processed.has(el)) { processed.set(el, true); callback(el); if (options.once) { done(); return true; } } return false; }; const lookForSelector = () => { const elParent = document.documentElement; if (elParent.matches(selector) || elParent.querySelector(selector)) { const elements = elParent.querySelectorAll(selector); elements.forEach(el => processElement(el)); } }; const debouncedLookForSelector = debounce(() => { lookForSelector(); }, 100); // Initial check for the selector on page load lookForSelector(); if (!isDone) { obs = new MutationObserver(() => { debouncedLookForSelector(); }); obs.observe(document, { attributes: false, childList: true, subtree: true, }); } return done; } /** * #1.3 Add Class to Element * * USE WHEN: You need to add a CSS class to an element * COMMON USE CASE: Applying custom styling, toggling visibility * * EXAMPLE: * addClass('.product-card', 'highlighted'); * * @param {string} selector - CSS selector for the element * @param {string} [cls=""] - Class name to add */ function addClass(selector, cls = "") { var el = document.querySelector(selector); if (el) { el.classList.add(cls); } } /** * #1.4 Remove Class from Element * * USE WHEN: You need to remove a CSS class from an element * COMMON USE CASE: Removing default styling, hiding elements * * EXAMPLE: * removeClass('.modal', 'hidden'); * * @param {string} selector - CSS selector for the element * @param {string} [cls=""] - Class name to remove */ function removeClass(selector, cls = "") { var el = document.querySelector(selector); if (el && el.classList.contains(cls)) { el.classList.remove(cls); } } /** * #1.5 Change Content of Element * * USE WHEN: You need to replace the HTML content of an element * COMMON USE CASE: Updating headlines, button text, descriptions * WARNING: Use with caution - replaces all inner HTML * * EXAMPLE: * changeContent('.hero-title', '

New Headline

'); * * @param {string} selector - CSS selector for the element * @param {string} [content=""] - HTML content to set */ function changeContent(selector, content = "") { var el = document.querySelector(selector); if (el) { el.innerHTML = content; } } // ======================================================================== // 2. EVENT HANDLING // ======================================================================== /** * #2.1 Live Event Listener (Event Delegation) * * USE WHEN: You need to attach events to elements that don't exist yet or are dynamically added * DIFFERENCE FROM addEventListener: Works on future elements matching the selector * COMMON USE CASE: Click handlers for dynamically loaded buttons, product cards * * EXAMPLE: * live('.add-to-cart-btn', 'click', function(e) { * console.log('Add to cart clicked', this); * }); * * @param {string} selector - CSS selector for target elements * @param {string} event - Event type (e.g., 'click', 'input', 'change') * @param {Function} callback - Function to call when event fires * @param {Document|Element} [context=document] - Parent element to attach listener to */ function live(selector, event, callback, context = document) { const addEvent = (el, type, handler) => { el.addEventListener(type, handler); }; const liveHandler = e => { const el = e.target && e.target.closest ? e.target.closest(selector) : null; if (el && el !== context) { callback.call(el, e); } }; addEvent(context, event, liveHandler); } /** * #2.2 Debounce Function * * USE WHEN: You need to limit how often a function is called * COMMON USE CASE: Search input handlers, window resize events, scroll handlers * WHY: Prevents performance issues by reducing function calls * * EXAMPLE: * const handleSearch = debounce((value) => { * console.log('Searching for:', value); * }, 300); * * input.addEventListener('input', (e) => handleSearch(e.target.value)); * * @param {Function} func - The function to debounce * @param {number} [delay=20] - Wait time in milliseconds before calling function * @returns {Function} - Debounced version of the function */ function debounce(func, delay = 20) { let timeout; return function(...args) { const context = this; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), delay); }; } /** * #2.3 SPA Location Change Listener * * USE WHEN: Testing on Single Page Applications (React, Vue, Angular) where URL changes don't reload page * COMMON USE CASE: Running test logic when user navigates between pages in an SPA * * EXAMPLE: * listener(() => { * console.log('Page changed to:', window.location.pathname); * // Re-run your test logic here * }); * * @param {Function} trigger - Callback function to execute on location change */ function listener(trigger) { window.addEventListener("locationchange", function() { trigger(); }); history.pushState = (f => function pushState() { var ret = f.apply(this, arguments); window.dispatchEvent(new Event("pushstate")); window.dispatchEvent(new Event("locationchange")); return ret; })(history.pushState); history.replaceState = (f => function replaceState() { var ret = f.apply(this, arguments); window.dispatchEvent(new Event("replacestate")); window.dispatchEvent(new Event("locationchange")); return ret; })(history.replaceState); window.addEventListener("popstate", () => { window.dispatchEvent(new Event("locationchange")); }); } // ======================================================================== // 3. COOKIE MANAGEMENT // ======================================================================== /** * #3.1 Set Cookie * * USE WHEN: You need to store data in browser cookies * COMMON USE CASE: Saving user preferences, tracking test exposure, storing variation info * * EXAMPLE: * setCookie('test_variant', 'variant_b', 30); // Expires in 30 days * * @param {string} name - Cookie name * @param {string} value - Cookie value * @param {number} days - Number of days until cookie expires */ function setCookie(name, value, days) { var expires = ""; if (days) { var date = new Date(); date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); expires = "; expires=" + date.toUTCString(); } document.cookie = name + "=" + (value || "") + expires + "; path=/"; } /** * #3.2 Get Cookie * * USE WHEN: You need to read a cookie value * COMMON USE CASE: Checking if user has seen a test before, reading saved preferences * * EXAMPLE: * const variant = getCookie('test_variant'); * if (variant === 'variant_b') { * // Show variant B * } * * @param {string} name - Cookie name to retrieve * @returns {string|null} - Cookie value or null if not found */ function getCookie(name) { var nameEQ = name + "="; var ca = document.cookie.split(";"); for (var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) == " ") c = c.substring(1, c.length); if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); } return null; } /** * #3.3 Remove Cookie * * USE WHEN: You need to delete a cookie * COMMON USE CASE: Clearing test data, removing user preferences * * EXAMPLE: * removeCookie('test_variant'); * * @param {string} cookieName - Name of cookie to remove */ function removeCookie(cookieName) { document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; } // ======================================================================== // 4. UTILITY FUNCTIONS // ======================================================================== /** * #4.1 Generate Unique ID (UUID) * * USE WHEN: You need a unique identifier for tracking, session IDs, or element IDs * COMMON USE CASE: Creating unique IDs for dynamically inserted elements, tracking events * * EXAMPLE: * const uniqueId = uuid(); * element.setAttribute('data-test-id', uniqueId); * * @returns {string} - A unique identifier string */ function uuid() { var timestamp = new Date().getTime(); var randomNumber = Math.floor(Math.random() * 100000); var uuid = `${timestamp}-${randomNumber}-`; var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".split(""); for (var i = 0; i < 8; i++) { uuid += characters[Math.floor(Math.random() * characters.length)]; } return uuid; } // ======================================================================== // 5. ASYNC/FETCH UTILITIES // ======================================================================== /** * #5.1 Cache Fetch (with Session Storage) * * USE WHEN: You need to fetch data from an API and cache it to avoid repeated requests * COMMON USE CASE: Fetching product data, pricing, availability * WHY: Improves performance and reduces server load * * EXAMPLE: * cache('https://api.example.com/products', 'products-cache', true) * .then(data => console.log('Products:', data)) * .catch(err => console.error('Error:', err)); * * @param {string} url - URL to fetch from * @param {string} [cacheKey=""] - Unique key for caching * @param {boolean} [isJson=true] - Whether to parse response as JSON * @param {Object} [options={}] - Fetch options * @param {number} [ttl=3600] - Cache time-to-live in seconds (default 1 hour) * @returns {Promise} - Promise resolving to the fetched data */ async function cache(url, cacheKey = "", isJson = true, options = {}, ttl = 3600) { const now = Date.now(); const cacheKeyCombined = `${url}-${cacheKey}`; const cached = JSON.parse(sessionStorage.getItem(cacheKeyCombined) || "{}"); try { // Check if valid cache exists if (cached.data && now - cached.timestamp < ttl * 1000) { return cached.data; } // Fetch from the network if no valid cache is found const fetchOptions = { ...options, cache: options?.cache || "default" }; const response = await fetch(url, fetchOptions); // Ensure the response is valid if (!response.ok) { throw new Error(`Failed to fetch data. HTTP Status: ${response.status}`); } // Parse the response based on isJson flag const data = await response[isJson ? "json" : "text"](); // Save the response to the cache sessionStorage.setItem(cacheKeyCombined, JSON.stringify({ data, timestamp: now })); return data; } catch (error) { throw error; } } // ======================================================================== // 6. SHOPIFY CART MONITORING (E-commerce Specific) // ======================================================================== /** * #6.1 Shopify Cart Change Observer * * USE WHEN: Testing on Shopify stores and need to detect cart changes * COMMON USE CASE: Triggering cart upsells, showing notifications when items are added * * EXAMPLE: * new PerformanceObserver(function(e) { * !e.getEntriesByType("resource").some(({ name }) => * /\/cart\/(change|update|add|clear)/i.test(name) * ) || showCartNotification(); * }).observe({ entryTypes: ["resource"] }); * * Replace YOUR_FUNCTION with your callback function */ new PerformanceObserver(function(e) { !e.getEntriesByType("resource").some(({ name }) => /\/cart\/(change|update|add|clear)/i.test(name)) || YOUR_FUNCTION(); }).observe({ entryTypes: ["resource"] }); // ======================================================================== // 7. QUICK REFERENCE EXAMPLES // ======================================================================== /** * COMMON A/B TESTING PATTERNS: * * 1. CHANGE BUTTON TEXT: * waitForElement('.checkout-button', (btn) => { * btn.textContent = 'Complete Purchase'; * }); * * 2. ADD URGENCY MESSAGE: * waitForElement('.product-price', (priceEl) => { * priceEl.insertAdjacentHTML('afterend', * '
Only 3 left in stock!
' * ); * }); * * 3. TRACK BUTTON CLICKS: * live('.add-to-cart', 'click', function() { * console.log('Add to cart clicked'); * // Track event in your analytics tool * }); * * 4. SHOW ELEMENT ONLY FOR RETURNING USERS: * if (getCookie('returning_user') === 'yes') { * waitForElement('.special-offer', (el) => { * el.style.display = 'block'; * }); * } * * 5. DEBOUNCED SEARCH: * const searchHandler = debounce((value) => { * // Perform search API call * console.log('Searching for:', value); * }, 500); * * document.querySelector('.search-input').addEventListener('input', (e) => { * searchHandler(e.target.value); * }); */ // ======================================================================== // BEST PRACTICES FOR JUNIOR DEVELOPERS // ======================================================================== /** * 1. ALWAYS WRAP YOUR CODE IN TRY-CATCH: * Prevents one error from breaking the entire test * * 2. USE waitForElement BEFORE DOM MANIPULATION: * Elements might not exist when your code runs * * 3. TEST IN MULTIPLE BROWSERS: * Chrome, Firefox, Safari, and mobile browsers * * 4. AVOID BLOCKING THE MAIN THREAD: * Use debounce for expensive operations * * 5. CLEAN UP AFTER YOURSELF: * Remove observers when no longer needed using the returned cleanup function * * 6. USE MEANINGFUL VARIABLE NAMES: * Instead of 'btn', use 'checkoutButton' * * 7. COMMENT YOUR CODE: * Explain WHY, not just WHAT * * 8. TEST THOROUGHLY: * Check edge cases, slow connections, and different device sizes */ // ================================================================== The End ================================================================== // =============================================================================================================================================