@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
JavaScript
;
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