UNPKG

ng-qcauto

Version:

Effortless, stable test IDs for Angular apps. Automatically injects data-qcauto attributes for QA and test automation teams without cluttering templates.

517 lines (490 loc) 16.1 kB
function hashBase36(input) { let h1 = 0x811c9dc5, h2 = 0x811c9dc5; for (let i = 0; i < input.length; i++) { const c = input.charCodeAt(i); h1 ^= c; h1 = Math.imul(h1, 0x01000193); h2 ^= (c << 1); h2 = Math.imul(h2, 0x01000193); } const mixed = (h1 ^ (h2 >>> 7)) >>> 0; return mixed.toString(36); } // qc-dompath.util.ts function domPathWithinHost(el, host) { const parts = []; let node = el; while (node && node !== host && node.nodeType === Node.ELEMENT_NODE) { const tag = node.tagName.toLowerCase(); // nth-of-type among element siblings with the same tag (more stable than nth-child) let index = 1; let sib = node.previousElementSibling; while (sib) { if (sib.tagName.toLowerCase() === tag) index++; sib = sib.previousElementSibling; } parts.push(`${tag}:${index}`); node = node.parentElement; } return parts.reverse().join('>'); } // qc-auto-global.ts // Current version of the QC Auto configuration const QC_AUTO_VERSION = '2.0.8'; const VERSION_KEY = 'qcAuto-version'; function loadConfigFromStorage() { const getArray = (key) => { const raw = localStorage.getItem(key); try { return raw ? JSON.parse(raw) : []; } catch { return []; } }; const clickToCopy = localStorage.getItem('qcAuto-clickToCopy'); return { tags: getArray('qcAuto-tags'), classes: getArray('qcAuto-classes'), ids: getArray('qcAuto-ids'), clickToCopy: clickToCopy === 'true', }; } function ensureDefaults() { const storedVersion = localStorage.getItem(VERSION_KEY); // If version doesn't match or doesn't exist, clear all old qcAuto data if (storedVersion !== QC_AUTO_VERSION) { console.log(`QC Auto: Version mismatch (stored: ${storedVersion}, current: ${QC_AUTO_VERSION}). Clearing old configuration...`); // Clear all qcAuto-related items from localStorage const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('qcAuto-')) { keysToRemove.push(key); } } keysToRemove.forEach(key => localStorage.removeItem(key)); // Set defaults for the new version const defaultTags = [ 'a', 'button', 'input', 'textarea', 'select', 'form', 'label', 'option', 'img' ]; localStorage.setItem('qcAuto-tags', JSON.stringify(defaultTags)); localStorage.setItem('qcAuto-classes', JSON.stringify([])); localStorage.setItem('qcAuto-ids', JSON.stringify([])); localStorage.setItem('qcAuto-clickToCopy', 'false'); // Store the current version localStorage.setItem(VERSION_KEY, QC_AUTO_VERSION); console.log(`QC Auto: Configuration initialized with version ${QC_AUTO_VERSION}`); } } let CONFIG = { tags: [], classes: [], ids: [], clickToCopy: false }; let mutationObserver = null; function getCurrentRoutePath() { const path = window.location.pathname; // Clean up the path: remove leading/trailing slashes, replace slashes with underscores return path .replace(/^\/+|\/+$/g, '') // Remove leading/trailing slashes .replace(/\//g, '_') // Replace slashes with underscores .replace(/[^a-zA-Z0-9_-]/g, '') || 'home'; // Remove special chars, default to 'home' } function initQcAutoGlobal() { ensureDefaults(); CONFIG = loadConfigFromStorage(); console.log('QC-Auto initialized with config:', CONFIG); // Inject styles for modal and toast injectModalStyles(); // Setup keyboard shortcut listener (Ctrl+Q) setupKeyboardShortcut(); // Setup click-to-copy if enabled if (CONFIG.clickToCopy) { setupClickToCopy(); } // Initial scan document.querySelectorAll('*').forEach(el => { assignQcId(el); }); // Watch DOM changes mutationObserver = new MutationObserver(muts => { muts.forEach(m => { m.addedNodes.forEach(node => { if (node instanceof Element) { if (matchesTarget(node)) { assignQcId(node); } node.querySelectorAll?.('*').forEach(el => { assignQcId(el); }); } }); }); }); mutationObserver.observe(document.body, { childList: true, subtree: true }); } function matchesTarget(el) { const tag = el.tagName.toLowerCase(); if (CONFIG.tags?.includes(tag)) return true; if (CONFIG.classes?.some(cls => el.classList.contains(cls))) return true; if (CONFIG.ids?.includes(el.id)) return true; return false; } function assignQcId(el) { if (el.hasAttribute('data-qcauto')) return; if (!matchesTarget(el)) return; const routePath = getCurrentRoutePath(); const key = el.getAttribute('data-qc-key'); let basis; if (key) { basis = `${routePath}|${el.tagName}|${key}`; } else { const path = domPathWithinHost(el, document.body); basis = `${routePath}|${el.tagName}|${el.id}|${Array.from(el.classList).join('.') || ''}|${path}`; } const id = el.id ? `qc_${routePath}_${el.tagName.toLowerCase()}_${el.id}` : `qc_${routePath}_${el.tagName.toLowerCase()}_${hashBase36(basis)}`; el.setAttribute('data-qcauto', id); // Add cursor pointer if click-to-copy is enabled if (CONFIG.clickToCopy) { el.style.cursor = 'pointer'; } } // Click-to-Copy System function setupClickToCopy() { document.body.addEventListener('contextmenu', (e) => { const target = e.target; // Don't copy if clicking on modal elements if (target.closest('.qc-config-modal')) return; // Find the closest element with data-qcauto attribute const qcElement = target.closest('[data-qcauto]'); if (qcElement) { const qcId = qcElement.getAttribute('data-qcauto'); if (qcId) { e.preventDefault(); // Prevent context menu from showing e.stopPropagation(); copyToClipboard(qcId); } } }, true); // Use capture phase to catch events early } function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { console.log('Copied to clipboard:', text); showToast(`✓ ${text}`); }).catch(err => { console.error('Failed to copy:', err); // Fallback method const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); showToast(`✓ ${text}`); }); } function showToast(message) { const toast = document.createElement('div'); toast.className = 'qc-toast'; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000); } // Keyboard Shortcut System (Ctrl+Q) let keySequence = []; let keySequenceTimeout = null; function setupKeyboardShortcut() { document.addEventListener('keydown', (e) => { if (e.ctrlKey || e.metaKey) { keySequence.push(e.key.toLowerCase()); // Clear timeout if exists if (keySequenceTimeout) { clearTimeout(keySequenceTimeout); } // Reset sequence after 1 second keySequenceTimeout = setTimeout(() => { keySequence = []; }, 1000); // Check if sequence matches Ctrl+Q const sequenceStr = keySequence.join(''); if (sequenceStr.includes('q')) { e.preventDefault(); openConfigModal(); keySequence = []; } } }); } // Configuration Modal function openConfigModal() { // Remove existing modal if any const existing = document.querySelector('.qc-config-modal'); if (existing) { existing.remove(); return; } const modal = document.createElement('div'); modal.className = 'qc-config-modal'; const currentConfig = loadConfigFromStorage(); modal.innerHTML = ` <div class="qc-modal-content"> <div class="qc-modal-header"> <h2>QC Auto Configuration</h2> <button class="qc-modal-close">&times;</button> </div> <div class="qc-modal-body"> <div class="qc-form-group"> <label for="qc-tags">Tags (comma-separated):</label> <input type="text" id="qc-tags" placeholder="e.g., button, input, a" value="${currentConfig.tags.join(', ')}" /> </div> <div class="qc-form-group"> <label for="qc-classes">Classes (comma-separated):</label> <input type="text" id="qc-classes" placeholder="e.g., btn, form-control" value="${currentConfig.classes.join(', ')}" /> </div> <div class="qc-form-group"> <label for="qc-ids">IDs (comma-separated):</label> <input type="text" id="qc-ids" placeholder="e.g., submit-btn, user-form" value="${currentConfig.ids.join(', ')}" /> </div> <div class="qc-form-group qc-checkbox-group"> <label> <input type="checkbox" id="qc-click-to-copy" ${currentConfig.clickToCopy ? 'checked' : ''} /> Enable Click-to-Copy QC IDs </label> </div> </div> <div class="qc-modal-footer"> <button class="qc-btn qc-btn-primary" id="qc-reload-btn">Save & Reload</button> <button class="qc-btn qc-btn-secondary" id="qc-cancel-btn">Cancel</button> </div> </div> `; document.body.appendChild(modal); injectModalStyles(); // Event listeners modal.querySelector('.qc-modal-close')?.addEventListener('click', () => modal.remove()); modal.querySelector('#qc-cancel-btn')?.addEventListener('click', () => modal.remove()); modal.querySelector('#qc-reload-btn')?.addEventListener('click', () => { saveConfigAndReload(modal); }); // Close on outside click modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); // Focus first input setTimeout(() => { modal.querySelector('#qc-tags')?.focus(); }, 100); } function saveConfigAndReload(modal) { const tagsInput = modal.querySelector('#qc-tags').value; const classesInput = modal.querySelector('#qc-classes').value; const idsInput = modal.querySelector('#qc-ids').value; const clickToCopy = modal.querySelector('#qc-click-to-copy').checked; // Parse and save const tags = tagsInput.split(',').map(s => s.trim()).filter(s => s); const classes = classesInput.split(',').map(s => s.trim()).filter(s => s); const ids = idsInput.split(',').map(s => s.trim()).filter(s => s); localStorage.setItem('qcAuto-tags', JSON.stringify(tags)); localStorage.setItem('qcAuto-classes', JSON.stringify(classes)); localStorage.setItem('qcAuto-ids', JSON.stringify(ids)); localStorage.setItem('qcAuto-clickToCopy', String(clickToCopy)); console.log('Configuration saved:', { tags, classes, ids, clickToCopy }); // Reload the page window.location.reload(); } function injectModalStyles() { if (document.getElementById('qc-auto-styles')) return; const style = document.createElement('style'); style.id = 'qc-auto-styles'; style.textContent = ` /* Force LTR direction and prevent color overrides */ .qc-config-modal, .qc-config-modal * { direction: ltr !important; color: inherit !important; } .qc-config-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 999999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } .qc-modal-content { background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); width: 90%; max-width: 500px; max-height: 90vh; overflow-y: auto; } .qc-modal-header { padding: 20px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; } .qc-modal-header h2 { margin: 0; font-size: 20px; color: #333 !important; } .qc-modal-close { background: none; border: none; font-size: 28px; cursor: pointer; color: #999 !important; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; line-height: 1; } .qc-modal-close:hover { color: #333 !important; } .qc-modal-body { padding: 20px; } .qc-form-group { margin-bottom: 20px; } .qc-form-group label { display: block; margin-bottom: 8px; font-weight: 600; color: #333 !important; font-size: 14px; } .qc-form-group input[type="text"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; } .qc-form-group input[type="text"]:focus { outline: none; border-color: #4CAF50 !important; } .qc-checkbox-group label { display: flex; align-items: center; cursor: pointer; font-weight: normal; } .qc-checkbox-group input[type="checkbox"] { margin-right: 8px; width: 18px; height: 18px; cursor: pointer; } .qc-modal-footer { padding: 20px; border-top: 1px solid #e0e0e0; display: flex; gap: 10px; justify-content: flex-end; } .qc-btn { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; } .qc-btn-primary { background: #4CAF50 !important; color: white !important; } .qc-btn-primary:hover { background: #45a049 !important; } .qc-btn-secondary { background: #f0f0f0 !important; color: #333 !important; } .qc-btn-secondary:hover { background: #e0e0e0 !important; } /* Toast notification */ .qc-toast { position: fixed; bottom: 20px; right: 20px; background: #323232 !important; color: white !important; padding: 12px 20px; border-radius: 4px; z-index: 1000000; animation: qc-toast-in 0.3s ease-out; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 14px; } @keyframes qc-toast-in { from { transform: translateY(100px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } `; document.head.appendChild(style); } /* * Public API Surface of ng-qcauto */ /** * Generated bundle index. Do not edit. */ export { domPathWithinHost, hashBase36, initQcAutoGlobal }; //# sourceMappingURL=ng-qcauto.mjs.map