UNPKG

@birhaus/test-utils

Version:

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

567 lines (564 loc) 21.2 kB
'use strict'; // src/spanish/SpanishCoverageValidator.ts var SpanishCoverageValidator = class { constructor(options = {}) { // Paraguay-specific financial terminology this.financialTerms = { // Currency and amounts "amount": { es: "monto", alternatives: ["cantidad", "importe"] }, "balance": { es: "saldo", alternatives: ["balance"] }, "income": { es: "ingreso", alternatives: ["entrada"] }, "expense": { es: "gasto", alternatives: ["egreso"] }, "transfer": { es: "transferencia", alternatives: [] }, "payment": { es: "pago", alternatives: [] }, "receipt": { es: "recibo", alternatives: ["comprobante"] }, "invoice": { es: "factura", alternatives: [] }, "tax": { es: "impuesto", alternatives: [] }, "total": { es: "total", alternatives: [] }, "subtotal": { es: "subtotal", alternatives: [] }, // Status terms "pending": { es: "pendiente", alternatives: ["en espera"] }, "approved": { es: "aprobado", alternatives: ["autorizado"] }, "rejected": { es: "rechazado", alternatives: ["denegado"] }, "completed": { es: "completado", alternatives: ["finalizado"] }, "cancelled": { es: "cancelado", alternatives: ["anulado"] }, // Actions "save": { es: "guardar", alternatives: [] }, "cancel": { es: "cancelar", alternatives: [] }, "edit": { es: "editar", alternatives: ["modificar"] }, "delete": { es: "eliminar", alternatives: ["borrar"] }, "approve": { es: "aprobar", alternatives: ["autorizar"] }, "reject": { es: "rechazar", alternatives: ["denegar"] }, "export": { es: "exportar", alternatives: [] }, "print": { es: "imprimir", alternatives: [] }, // Church-specific terms "donation": { es: "donaci\xF3n", alternatives: ["ofrenda"] }, "tithe": { es: "diezmo", alternatives: [] }, "offering": { es: "ofrenda", alternatives: [] }, "church": { es: "iglesia", alternatives: [] }, "pastor": { es: "pastor", alternatives: [] }, "member": { es: "miembro", alternatives: [] }, "treasurer": { es: "tesorero", alternatives: [] }, "administrator": { es: "administrador", alternatives: [] } }; // Common Spanish patterns for Paraguay this.spanishPatterns = [ /[ñáéíóúü]/i, // Spanish characters /ción\b/i, // -ción endings /dad\b/i, // -dad endings /mente\b/i, // -mente adverbs /\bel\b|\bla\b|\blos\b|\blas\b/i, // Articles /\ben\b|\bde\b|\bdel\b|\bal\b/i, // Prepositions /\by\b|\bo\b|\bpero\b|\bsi\b/i // Conjunctions ]; this.options = { requireSpanishPrimary: true, requireEnglishFallback: true, strictFinancialTerms: true, validateParaguayanSpanish: true, validateFinancialVocabulary: true, validateCurrencyFormats: true, checkBirhausComponents: true, checkErrorMessages: true, checkButtonLabels: true, checkFormLabels: true, birhausSelector: "[data-birhaus-component]", interactiveSelector: 'button, a, input, select, textarea, [role="button"]', contentSelector: 'h1, h2, h3, h4, h5, h6, p, span, div[class*="text"], label', ...options }; } /** * Validate all Spanish coverage requirements */ validateAll(container = document.body) { const violations = []; if (this.options.checkBirhausComponents) { violations.push(...this.validateBirhausComponents(container)); } if (this.options.checkButtonLabels) { violations.push(...this.validateButtonLabels(container)); } if (this.options.checkFormLabels) { violations.push(...this.validateFormLabels(container)); } if (this.options.checkErrorMessages) { violations.push(...this.validateErrorMessages(container)); } if (this.options.validateFinancialVocabulary) { violations.push(...this.validateFinancialTerminology(container)); } if (this.options.validateCurrencyFormats) { violations.push(...this.validateCurrencyFormats(container)); } return violations; } /** * Validate BIRHAUS components have Spanish-first labels */ validateBirhausComponents(container) { const violations = []; const birhausComponents = container.querySelectorAll(this.options.birhausSelector); birhausComponents.forEach((component) => { const componentType = component.getAttribute("data-birhaus-component"); const hasSpanishLabel = this.checkSpanishLabels(component); const hasEnglishFallback = this.checkEnglishFallbacks(component); if (this.options.requireSpanishPrimary && !hasSpanishLabel) { violations.push({ type: "missing-spanish", element: component, severity: "serious", message: `BIRHAUS ${componentType} component missing Spanish labels`, messageEs: `Componente BIRHAUS ${componentType} carece de etiquetas en espa\xF1ol`, suggestedSpanish: this.suggestSpanishLabel(component), birhausPrinciple: 7, context: this.getElementContext(component) }); } if (this.options.requireEnglishFallback && hasSpanishLabel && !hasEnglishFallback) { violations.push({ type: "missing-english", element: component, severity: "moderate", message: `BIRHAUS ${componentType} component missing English fallback labels`, messageEs: `Componente BIRHAUS ${componentType} carece de etiquetas de respaldo en ingl\xE9s`, suggestedEnglish: this.suggestEnglishLabel(component), birhausPrinciple: 7, context: this.getElementContext(component) }); } }); return violations; } /** * Validate button labels are in Spanish-first */ validateButtonLabels(container) { const violations = []; const buttons = container.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]'); buttons.forEach((button) => { const text = this.getElementText(button); const hasSpanish = this.isSpanishText(text); if (!hasSpanish && text.length > 0) { const suggestedSpanish = this.translateButtonText(text); violations.push({ type: "missing-spanish", element: button, severity: "moderate", message: `Button text "${text}" should be in Spanish`, messageEs: `Texto del bot\xF3n "${text}" deber\xEDa estar en espa\xF1ol`, currentText: text, suggestedSpanish, birhausPrinciple: 7, context: "button" }); } }); return violations; } /** * Validate form labels and placeholders */ validateFormLabels(container) { const violations = []; const formElements = container.querySelectorAll("input, select, textarea"); formElements.forEach((element) => { const label = this.getAssociatedLabel(element); if (label) { const labelText = label.textContent?.trim() || ""; if (!this.isSpanishText(labelText) && labelText.length > 0) { violations.push({ type: "missing-spanish", element: label, severity: "moderate", message: `Form label "${labelText}" should be in Spanish`, messageEs: `Etiqueta de formulario "${labelText}" deber\xEDa estar en espa\xF1ol`, currentText: labelText, suggestedSpanish: this.translateFormLabel(labelText), birhausPrinciple: 7, context: "form" }); } } const placeholder = element.getAttribute("placeholder"); if (placeholder && !this.isSpanishText(placeholder)) { violations.push({ type: "missing-spanish", element, severity: "minor", message: `Placeholder "${placeholder}" should be in Spanish`, messageEs: `Marcador de posici\xF3n "${placeholder}" deber\xEDa estar en espa\xF1ol`, currentText: placeholder, suggestedSpanish: this.translatePlaceholder(placeholder), birhausPrinciple: 7, context: "form" }); } }); return violations; } /** * Validate error messages are in Spanish with "Qué pasó + cómo resolver" pattern */ validateErrorMessages(container) { const violations = []; const errorElements = container.querySelectorAll( '.error, .alert-error, [role="alert"], [class*="error"], [data-error], [aria-invalid="true"] + *' ); errorElements.forEach((element) => { const errorText = element.textContent?.trim() || ""; if (errorText.length > 0) { const hasSpanish = this.isSpanishText(errorText); const hasProperPattern = this.hasProperErrorPattern(errorText); if (!hasSpanish) { violations.push({ type: "missing-spanish", element, severity: "serious", message: `Error message "${errorText}" should be in Spanish`, messageEs: `Mensaje de error "${errorText}" deber\xEDa estar en espa\xF1ol`, currentText: errorText, suggestedSpanish: this.translateErrorMessage(errorText), birhausPrinciple: 7, context: "error" }); } else if (!hasProperPattern) { violations.push({ type: "invalid-pattern", element, severity: "moderate", message: `Error message should follow "Qu\xE9 pas\xF3 + c\xF3mo resolver" pattern`, messageEs: `Mensaje de error deber\xEDa seguir el patr\xF3n "Qu\xE9 pas\xF3 + c\xF3mo resolver"`, currentText: errorText, suggestedSpanish: this.improveErrorPattern(errorText), birhausPrinciple: 7, context: "error" }); } } }); return violations; } /** * Validate financial terminology is properly translated */ validateFinancialTerminology(container) { const violations = []; const textElements = container.querySelectorAll(this.options.contentSelector); textElements.forEach((element) => { const text = element.textContent?.toLowerCase() || ""; Object.entries(this.financialTerms).forEach(([englishTerm, spanishData]) => { const regex = new RegExp(`\\b${englishTerm}\\b`, "gi"); if (regex.test(text)) { violations.push({ type: "financial-term", element, severity: this.options.strictFinancialTerms ? "serious" : "moderate", message: `Financial term "${englishTerm}" should use Spanish "${spanishData.es}"`, messageEs: `T\xE9rmino financiero "${englishTerm}" deber\xEDa usar espa\xF1ol "${spanishData.es}"`, currentText: englishTerm, suggestedSpanish: spanishData.es, birhausPrinciple: 7, context: "financial" }); } }); }); return violations; } /** * Validate currency formats follow Paraguay conventions */ validateCurrencyFormats(container) { const violations = []; const textElements = container.querySelectorAll(this.options.contentSelector); textElements.forEach((element) => { const text = element.textContent || ""; const currencyPatterns = [ /\$[\d,]+/g, // Dollar signs /USD\s*[\d,]+/g, // USD prefix /[\d,]+\s*USD/g, // USD suffix /₲[\d,]+/g, // Guaraní symbol /PYG\s*[\d,]+/g, // PYG prefix /[\d,]+\s*PYG/g // PYG suffix ]; currencyPatterns.forEach((pattern) => { const matches = text.match(pattern); if (matches) { matches.forEach((match) => { if (!this.isValidParaguayanCurrency(match)) { violations.push({ type: "invalid-pattern", element, severity: "moderate", message: `Currency format "${match}" should follow Paraguay conventions`, messageEs: `Formato de moneda "${match}" deber\xEDa seguir convenciones de Paraguay`, currentText: match, suggestedSpanish: this.formatAsParaguayanCurrency(match), birhausPrinciple: 7, context: "financial" }); } }); } }); }); return violations; } // Helper methods checkSpanishLabels(element) { const spanishAttributes = [ "data-label-es", "labelEs", "aria-label", "title" ]; return spanishAttributes.some((attr) => { const value = element.getAttribute(attr); return value && this.isSpanishText(value); }); } checkEnglishFallbacks(element) { const englishAttributes = [ "data-label-en", "labelEn" ]; return englishAttributes.some((attr) => { const value = element.getAttribute(attr); return value && value.length > 0; }); } isSpanishText(text) { if (!text || text.length === 0) return false; return this.spanishPatterns.some((pattern) => pattern.test(text)) || // Check for Spanish financial terms Object.values(this.financialTerms).some( (term) => text.toLowerCase().includes(term.es) || term.alternatives.some((alt) => text.toLowerCase().includes(alt)) ); } getElementText(element) { return element.textContent?.trim() || element.getAttribute("aria-label") || element.getAttribute("title") || element.getAttribute("alt") || ""; } getElementContext(element) { const tagName = element.tagName.toLowerCase(); const role = element.getAttribute("role"); const className = element.className; if (tagName === "button" || role === "button") return "button"; if (tagName === "nav" || role === "navigation") return "navigation"; if (["input", "select", "textarea"].includes(tagName)) return "form"; if (className.includes("error") || element.getAttribute("role") === "alert") return "error"; if (this.isFinancialElement(element)) return "financial"; return "content"; } isFinancialElement(element) { const text = this.getElementText(element).toLowerCase(); const className = element.className.toLowerCase(); return Object.keys(this.financialTerms).some( (term) => text.includes(term) || className.includes(term) ); } suggestSpanishLabel(element) { const componentType = element.getAttribute("data-birhaus-component"); const currentText = this.getElementText(element); if (currentText) { return this.translateButtonText(currentText); } const suggestions = { "button": "Bot\xF3n", "input": "Campo de entrada", "select": "Lista desplegable", "card": "Tarjeta", "dialog": "Di\xE1logo", "table": "Tabla", "tabs": "Pesta\xF1as", "checkbox": "Casilla de verificaci\xF3n" }; return suggestions[componentType || ""] || "Elemento"; } suggestEnglishLabel(element) { const spanishText = this.getElementText(element); return this.translateToEnglish(spanishText); } translateButtonText(text) { const translations = { "save": "Guardar", "cancel": "Cancelar", "edit": "Editar", "delete": "Eliminar", "submit": "Enviar", "reset": "Restablecer", "close": "Cerrar", "open": "Abrir", "add": "Agregar", "remove": "Quitar", "approve": "Aprobar", "reject": "Rechazar", "export": "Exportar", "import": "Importar", "print": "Imprimir", "search": "Buscar", "filter": "Filtrar", "sort": "Ordenar" }; const lowerText = text.toLowerCase(); return translations[lowerText] || text; } translateFormLabel(text) { const translations = { "name": "Nombre", "email": "Correo electr\xF3nico", "phone": "Tel\xE9fono", "address": "Direcci\xF3n", "amount": "Monto", "date": "Fecha", "description": "Descripci\xF3n", "category": "Categor\xEDa", "status": "Estado", "type": "Tipo", "notes": "Notas", "comments": "Comentarios" }; const lowerText = text.toLowerCase(); return translations[lowerText] || text; } translatePlaceholder(text) { const translations = { "enter name": "Ingrese el nombre", "enter email": "Ingrese el correo electr\xF3nico", "enter amount": "Ingrese el monto", "select date": "Seleccione la fecha", "search...": "Buscar...", "type here": "Escriba aqu\xED", "optional": "Opcional" }; const lowerText = text.toLowerCase(); return translations[lowerText] || text; } translateErrorMessage(text) { const translations = { "required field": "Campo obligatorio. Complete este campo para continuar.", "invalid email": "Correo electr\xF3nico inv\xE1lido. Verifique el formato (ejemplo@dominio.com).", "invalid amount": "Monto inv\xE1lido. Ingrese un n\xFAmero v\xE1lido mayor a cero.", "network error": "Error de conexi\xF3n. Verifique su conexi\xF3n a internet e intente nuevamente.", "server error": "Error del servidor. El problema ser\xE1 resuelto pronto, intente m\xE1s tarde." }; const lowerText = text.toLowerCase(); return translations[lowerText] || `Error: ${text}. Verifique los datos e intente nuevamente.`; } translateToEnglish(spanishText) { const reverseTranslations = { "guardar": "Save", "cancelar": "Cancel", "editar": "Edit", "eliminar": "Delete", "enviar": "Submit", "cerrar": "Close", "abrir": "Open", "agregar": "Add", "quitar": "Remove" }; const lowerText = spanishText.toLowerCase(); return reverseTranslations[lowerText] || spanishText; } hasProperErrorPattern(text) { const hasExplanation = /error|problema|fallo|inválido|incorrecto/i.test(text); const hasSolution = /verifique|intente|complete|corrija|revise/i.test(text); return hasExplanation && hasSolution; } improveErrorPattern(text) { const solutions = [ "Verifique los datos e intente nuevamente.", "Complete la informaci\xF3n requerida.", "Revise el formato e intente otra vez.", "Intente m\xE1s tarde o contacte soporte." ]; const randomSolution = solutions[Math.floor(Math.random() * solutions.length)]; return `${text} ${randomSolution}`; } getAssociatedLabel(element) { const labelledBy = element.getAttribute("aria-labelledby"); if (labelledBy) { return document.getElementById(labelledBy); } const id = element.getAttribute("id"); if (id) { return document.querySelector(`label[for="${id}"]`); } return element.closest("label"); } isValidParaguayanCurrency(currencyText) { const paraguayanPatterns = [ /^₲\s?\d{1,3}(\.\d{3})*$/, /^PYG\s\d{1,3}(\.\d{3})*$/, /^\d{1,3}(\.\d{3})*\sPYG$/ ]; return paraguayanPatterns.some((pattern) => pattern.test(currencyText)); } formatAsParaguayanCurrency(currencyText) { const number = currencyText.replace(/[^\d]/g, ""); const formatted = new Intl.NumberFormat("es-PY").format(parseInt(number)); return `\u20B2 ${formatted}`; } }; function expectSpanishFirst(container, strict = true) { const validator = new SpanishCoverageValidator({ requireSpanishPrimary: true, strictFinancialTerms: strict }); const violations = validator.validateAll(container); const seriousViolations = violations.filter((v) => v.severity === "serious" || v.severity === "critical"); if (seriousViolations.length > 0) { const errorMessages = seriousViolations.map((v) => v.messageEs).join("\n"); throw new Error( `Spanish-first violations detected: ${errorMessages} Total violations: ${violations.length} (${seriousViolations.length} serious)` ); } } function expectBilingualCompleteness(container) { const validator = new SpanishCoverageValidator({ requireSpanishPrimary: true, requireEnglishFallback: true }); const violations = validator.validateAll(container); if (violations.length > 0) { const errorMessages = violations.map((v) => v.message).join("\n"); throw new Error( `Bilingual completeness violations detected: ${errorMessages}` ); } } function expectFinancialSpanish(container) { const validator = new SpanishCoverageValidator({ validateFinancialVocabulary: true, strictFinancialTerms: true }); const violations = validator.validateFinancialTerminology(container || document.body); if (violations.length > 0) { const errorMessages = violations.map((v) => `${v.currentText} \u2192 ${v.suggestedSpanish}`).join("\n"); throw new Error( `Financial terminology violations detected: ${errorMessages}` ); } } exports.SpanishCoverageValidator = SpanishCoverageValidator; exports.expectBilingualCompleteness = expectBilingualCompleteness; exports.expectFinancialSpanish = expectFinancialSpanish; exports.expectSpanishFirst = expectSpanishFirst; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map