UNPKG

@birhaus/test-utils

Version:

BIRHAUS Testing & Validation Framework - Comprehensive testing utilities for cognitive load, accessibility, and BIRHAUS principle compliance

455 lines (448 loc) 15.2 kB
'use strict'; var react = require('@testing-library/react'); var userEvent = require('@testing-library/user-event'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var userEvent__default = /*#__PURE__*/_interopDefault(userEvent); // src/undo/UndoPatternValidator.ts var UndoPatternValidator = class { constructor(options = {}) { // Confirmation dialog anti-patterns this.confirmationPatterns = [ /are\s+you\s+sure/i, /¿está.?\s+seguro/i, /confirm/i, /confirmar/i, /delete.*\?/i, /eliminar.*\?/i, /really.*delete/i, /realmente.*eliminar/i, /this.*cannot.*be.*undone/i, /esto.*no.*se.*puede.*deshacer/i ]; // Destructive action patterns this.destructivePatterns = [ /delete/i, /eliminar/i, /remove/i, /quitar/i, /destroy/i, /destruir/i, /clear/i, /limpiar/i, /reset/i, /restablecer/i, /purge/i, /purgar/i ]; this.options = { detectConfirmationDialogs: true, validateUndoPresence: true, validateUndoTimeout: true, validateUndoMessaging: true, defaultUndoTimeout: 1e4, // 10 seconds maxUndoTimeout: 3e4, // 30 seconds max minUndoTimeout: 3e3, // 3 seconds min destructiveSelectors: [ '[data-action="delete"]', '[data-action="remove"]', ".delete-button", ".remove-button", 'button[class*="delete"]', 'button[class*="remove"]' ], destructiveTexts: [ "delete", "eliminar", "remove", "quitar", "destroy", "destruir" ], requireSpanishUndoMessages: true, confirmationSelectors: [ '[role="alertdialog"]', '[role="dialog"]', ".confirmation-dialog", ".confirm-modal", ".alert-dialog" ], undoSelectors: [ '[data-action="undo"]', ".undo-button", 'button[class*="undo"]', '[aria-label*="undo"]', '[aria-label*="deshacer"]' ], destructiveActionSelectors: [ 'button[data-destructive="true"]', ".destructive-action", '[data-birhaus-component="button"][variant="destructive"]' ], ...options }; } /** * Validate all undo pattern compliance */ async validateAll(container = document.body) { const violations = []; if (this.options.detectConfirmationDialogs) { violations.push(...this.detectConfirmationDialogs(container)); } if (this.options.validateUndoPresence) { violations.push(...this.validateUndoPresence(container)); } if (this.options.validateUndoMessaging) { violations.push(...this.validateUndoMessaging(container)); } return violations; } /** * Detect confirmation dialogs (anti-pattern) */ // eslint-disable-next-line max-lines-per-function -- Dialog detection requires comprehensive pattern matching detectConfirmationDialogs(container) { const violations = []; this.options.confirmationSelectors.forEach((selector) => { const dialogs = container.querySelectorAll(selector); dialogs.forEach((dialog) => { const text = dialog.textContent || ""; const hasConfirmationPattern = this.confirmationPatterns.some( (pattern) => pattern.test(text) ); if (hasConfirmationPattern) { violations.push({ type: "confirmation-dialog", element: dialog, severity: "critical", message: "Confirmation dialog detected - violates BIRHAUS Principle #5", messageEs: "Di\xE1logo de confirmaci\xF3n detectado - viola el Principio BIRHAUS #5", actionType: "destructive", birhausPrinciple: 5, suggestions: [ "Replace confirmation with undo functionality", "Use BirhausDialog with undo pattern", "Implement immediate action with undo option", "Show success message with undo button" ] }); } }); }); const elementsWithOnclick = container.querySelectorAll("[onclick]"); elementsWithOnclick.forEach((element) => { const onclick = element.getAttribute("onclick") || ""; if (onclick.includes("confirm(")) { violations.push({ type: "confirmation-dialog", element, severity: "critical", message: "JavaScript confirm() detected in onclick handler", messageEs: "confirm() de JavaScript detectado en manejador onclick", birhausPrinciple: 5, suggestions: [ "Remove confirm() call from onclick", "Implement undo pattern instead", "Use BirhausButton with undo integration", "Add data-destructive attribute for proper handling" ] }); } }); return violations; } /** * Validate undo functionality is present for destructive actions */ validateUndoPresence(container) { const violations = []; const destructiveActions = this.findDestructiveActions(container); destructiveActions.forEach((action) => { const hasUndoSupport = this.checkUndoSupport(action); if (!hasUndoSupport) { const actionText = action.textContent?.trim() || ""; const actionType = this.getActionType(actionText); violations.push({ type: "missing-undo", element: action, severity: actionType === "delete" ? "serious" : "moderate", message: `Destructive action "${actionText}" lacks undo functionality`, messageEs: `Acci\xF3n destructiva "${actionText}" carece de funcionalidad de deshacer`, actionType, birhausPrinciple: 5, suggestions: [ "Add undo functionality to destructive action", "Use BirhausButton with destructive variant and undo", "Implement undo timeout with visual countdown", "Show success message with undo option" ] }); } }); return violations; } /** * Validate undo messaging and timeout settings */ // eslint-disable-next-line max-lines-per-function -- Undo messaging validation requires comprehensive checking validateUndoMessaging(container) { const violations = []; const undoElements = container.querySelectorAll( this.options.undoSelectors.join(", ") ); undoElements.forEach((undoElement) => { const hasTimeout = this.hasTimeoutInfo(undoElement); if (!hasTimeout) { violations.push({ type: "poor-undo-ux", element: undoElement, severity: "moderate", message: "Undo element lacks timeout information", messageEs: "Elemento de deshacer carece de informaci\xF3n de tiempo l\xEDmite", birhausPrinciple: 5, suggestions: [ "Add countdown timer to undo message", "Show remaining time for undo action", "Use BirhausDialog undo pattern with visual timer", "Display auto-close information" ] }); } if (this.options.requireSpanishUndoMessages) { const hasSpanishMessage = this.hasSpanishUndoMessage(undoElement); if (!hasSpanishMessage) { violations.push({ type: "missing-spanish", element: undoElement, severity: "moderate", message: "Undo element lacks Spanish messaging", messageEs: "Elemento de deshacer carece de mensajes en espa\xF1ol", birhausPrinciple: 7, suggestions: [ "Add Spanish undo labels", 'Use "Deshacer" instead of "Undo"', "Include Spanish timeout messages", "Add labelEs prop to undo components" ] }); } } }); return violations; } /** * Test undo functionality for a specific action */ // eslint-disable-next-line max-lines-per-function -- Undo functionality testing requires comprehensive state management async testUndoFunctionality(actionElement, container = document.body) { const user = userEvent__default.default.setup(); const initialState = this.captureState(container); await user.click(actionElement); let undoButton = null; let undoTimeout = 0; let undoMessage = ""; let undoMessageSpanish = false; try { await react.waitFor(() => { undoButton = container.querySelector( this.options.undoSelectors.join(", ") ); if (!undoButton) { throw new Error("Undo button not found"); } }, { timeout: 5e3 }); if (undoButton) { undoTimeout = this.extractUndoTimeout(undoButton); undoMessage = undoButton?.textContent?.trim() || ""; undoMessageSpanish = this.isSpanishUndoMessage(undoMessage); await user.click(undoButton); await react.waitFor(() => { const currentState = this.captureState(container); return this.statesEqual(currentState, initialState); }, { timeout: 3e3 }); const finalState = this.captureState(container); return { undoAvailable: true, undoTimeout, undoMessage, undoMessageSpanish, undoTriggered: true, originalState: initialState, undoState: finalState }; } } catch (error) { } return { undoAvailable: false, undoTimeout: 0, undoMessage: "", undoMessageSpanish: false, undoTriggered: false, originalState: initialState, undoState: this.captureState(container) }; } // Helper methods findDestructiveActions(container) { const actions = []; this.options.destructiveSelectors.forEach((selector) => { actions.push(...Array.from(container.querySelectorAll(selector))); }); const buttons = container.querySelectorAll('button, [role="button"], a'); buttons.forEach((button) => { const text = button.textContent?.toLowerCase() || ""; const isDestructive = this.destructivePatterns.some( (pattern) => pattern.test(text) ); if (isDestructive) { actions.push(button); } }); return Array.from(new Set(actions)); } checkUndoSupport(action) { if (action.hasAttribute("data-undo-support")) { return true; } if (action.getAttribute("data-birhaus-component") === "button") { const undoConfig = action.getAttribute("data-undo-config"); return !!undoConfig; } const parent = action.closest("[data-undo-container]"); return !!parent; } getActionType(actionText) { const lowerText = actionText.toLowerCase(); if (/delete|eliminar|destroy|destruir/.test(lowerText)) { return "delete"; } if (/remove|quitar|clear|limpiar|reset|restablecer/.test(lowerText)) { return "destructive"; } return "irreversible"; } hasTimeoutInfo(element) { const text = element.textContent || ""; const ariaLabel = element.getAttribute("aria-label") || ""; const title = element.getAttribute("title") || ""; const allText = `${text} ${ariaLabel} ${title}`.toLowerCase(); const timeoutPatterns = [ /\d+\s*(seconds?|segundos?)/i, /auto.*close/i, /cierre.*automático/i, /remaining|restante/i, /countdown|cuenta.*regresiva/i ]; return timeoutPatterns.some((pattern) => pattern.test(allText)); } hasSpanishUndoMessage(element) { const text = element.textContent || ""; const ariaLabel = element.getAttribute("aria-label") || ""; const spanishUndoPatterns = [ /deshacer/i, /revertir/i, /cancelar.*acción/i, /volver.*atrás/i ]; return spanishUndoPatterns.some( (pattern) => pattern.test(text) || pattern.test(ariaLabel) ); } extractUndoTimeout(element) { const timeoutAttr = element.getAttribute("data-undo-timeout"); if (timeoutAttr) { return parseInt(timeoutAttr); } const text = element.textContent || ""; const match = text.match(/(\d+)\s*s/); if (match) { return parseInt(match[1]) * 1e3; } return this.options.defaultUndoTimeout; } isSpanishUndoMessage(message) { const spanishPatterns = [ /deshacer/i, /acción.*completada/i, /se.*deshizo/i, /revertido/i ]; return spanishPatterns.some((pattern) => pattern.test(message)); } captureState(container) { return { innerHTML: container.innerHTML, childElementCount: container.childElementCount, textContent: container.textContent, timestamp: Date.now() }; } statesEqual(state1, state2) { return state1.childElementCount === state2.childElementCount && state1.textContent === state2.textContent; } }; function expectNoConfirmationDialogs(container) { const validator = new UndoPatternValidator({ detectConfirmationDialogs: true }); const violations = validator.detectConfirmationDialogs(container || document.body); if (violations.length > 0) { const errorMessages = violations.map((v) => v.message).join("\n"); throw new Error( `Confirmation dialog anti-patterns detected: ${errorMessages} Use undo patterns instead of confirmation dialogs.` ); } } function expectUndoForDestructiveActions(container) { const validator = new UndoPatternValidator({ validateUndoPresence: true }); const violations = validator.validateUndoPresence(container || document.body); if (violations.length > 0) { const errorMessages = violations.map((v) => v.message).join("\n"); throw new Error( `Missing undo functionality detected: ${errorMessages} All destructive actions must have undo functionality.` ); } } async function expectUndoWorks(actionElement, container) { const validator = new UndoPatternValidator(); const result = await validator.testUndoFunctionality( actionElement, container || document.body ); if (!result.undoAvailable) { throw new Error("Undo functionality not available for destructive action"); } if (!result.undoTriggered) { throw new Error("Undo action failed to restore original state"); } if (!result.undoMessageSpanish) { console.warn("Undo message not in Spanish - consider adding Spanish labels"); } } function expectReasonableUndoTimeout(undoElement, minMs = 3e3, maxMs = 3e4) { const validator = new UndoPatternValidator(); const timeout = validator.extractUndoTimeout(undoElement); if (timeout < minMs) { throw new Error( `Undo timeout ${timeout}ms is too short (minimum: ${minMs}ms)` ); } if (timeout > maxMs) { throw new Error( `Undo timeout ${timeout}ms is too long (maximum: ${maxMs}ms)` ); } } exports.UndoPatternValidator = UndoPatternValidator; exports.expectNoConfirmationDialogs = expectNoConfirmationDialogs; exports.expectReasonableUndoTimeout = expectReasonableUndoTimeout; exports.expectUndoForDestructiveActions = expectUndoForDestructiveActions; exports.expectUndoWorks = expectUndoWorks; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map