Set up comprehensive frontend testing infrastructure
- Install Jest for unit testing with React Testing Library - Install Playwright for end-to-end testing - Configure Jest with proper TypeScript support and module mapping - Create test setup files and utilities for both unit and e2e tests Components: * Jest configuration with coverage thresholds * Playwright configuration with browser automation * Unit tests for LoginForm, AuthContext, and useSocketIO hook * E2E tests for authentication, dashboard, and agents workflows * GitHub Actions workflow for automated testing * Mock data and API utilities for consistent testing * Test documentation with best practices Testing features: - Unit tests with 70% coverage threshold - E2E tests with API mocking and user journey testing - CI/CD integration for automated test runs - Cross-browser testing support with Playwright - Authentication system testing end-to-end 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
124
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/cursor.js
generated
vendored
Normal file
124
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/cursor.js
generated
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
import { isContentEditable } from '../edit/isContentEditable.js';
|
||||
import { isElementType } from '../misc/isElementType.js';
|
||||
|
||||
function getNextCursorPosition(node, offset, direction, inputType) {
|
||||
// The behavior at text node zero offset is inconsistent.
|
||||
// When walking backwards:
|
||||
// Firefox always moves to zero offset and jumps over last offset.
|
||||
// Chrome jumps over zero offset per default but over last offset when Shift is pressed.
|
||||
// The cursor always moves to zero offset if the focus area (contenteditable or body) ends there.
|
||||
// When walking forward both ignore zero offset.
|
||||
// When walking over input elements the cursor moves before or after that element.
|
||||
// When walking over line breaks the cursor moves inside any following text node.
|
||||
if (isTextNode(node) && offset + direction >= 0 && offset + direction <= node.nodeValue.length) {
|
||||
return {
|
||||
node,
|
||||
offset: offset + direction
|
||||
};
|
||||
}
|
||||
const nextNode = getNextCharacterContentNode(node, offset, direction);
|
||||
if (nextNode) {
|
||||
if (isTextNode(nextNode)) {
|
||||
return {
|
||||
node: nextNode,
|
||||
offset: direction > 0 ? Math.min(1, nextNode.nodeValue.length) : Math.max(nextNode.nodeValue.length - 1, 0)
|
||||
};
|
||||
} else if (isElementType(nextNode, 'br')) {
|
||||
const nextPlusOne = getNextCharacterContentNode(nextNode, undefined, direction);
|
||||
if (!nextPlusOne) {
|
||||
// The behavior when there is no possible cursor position beyond the line break is inconsistent.
|
||||
// In Chrome outside of contenteditable moving before a leading line break is possible.
|
||||
// A leading line break can still be removed per deleteContentBackward.
|
||||
// A trailing line break on the other hand is not removed by deleteContentForward.
|
||||
if (direction < 0 && inputType === 'deleteContentBackward') {
|
||||
return {
|
||||
node: nextNode.parentNode,
|
||||
offset: getOffset(nextNode)
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
} else if (isTextNode(nextPlusOne)) {
|
||||
return {
|
||||
node: nextPlusOne,
|
||||
offset: direction > 0 ? 0 : nextPlusOne.nodeValue.length
|
||||
};
|
||||
} else if (direction < 0 && isElementType(nextPlusOne, 'br')) {
|
||||
return {
|
||||
node: nextNode.parentNode,
|
||||
offset: getOffset(nextNode)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
node: nextPlusOne.parentNode,
|
||||
offset: getOffset(nextPlusOne) + (direction > 0 ? 0 : 1)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
node: nextNode.parentNode,
|
||||
offset: getOffset(nextNode) + (direction > 0 ? 1 : 0)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
function getNextCharacterContentNode(node, offset, direction) {
|
||||
const nextOffset = Number(offset) + (direction < 0 ? -1 : 0);
|
||||
if (offset !== undefined && isElement(node) && nextOffset >= 0 && nextOffset < node.children.length) {
|
||||
node = node.children[nextOffset];
|
||||
}
|
||||
return walkNodes(node, direction === 1 ? 'next' : 'previous', isTreatedAsCharacterContent);
|
||||
}
|
||||
function isTreatedAsCharacterContent(node) {
|
||||
if (isTextNode(node)) {
|
||||
return true;
|
||||
}
|
||||
if (isElement(node)) {
|
||||
if (isElementType(node, [
|
||||
'input',
|
||||
'textarea'
|
||||
])) {
|
||||
return node.type !== 'hidden';
|
||||
} else if (isElementType(node, 'br')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function getOffset(node) {
|
||||
let i = 0;
|
||||
while(node.previousSibling){
|
||||
i++;
|
||||
node = node.previousSibling;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
function isElement(node) {
|
||||
return node.nodeType === 1;
|
||||
}
|
||||
function isTextNode(node) {
|
||||
return node.nodeType === 3;
|
||||
}
|
||||
function walkNodes(node, direction, callback) {
|
||||
for(;;){
|
||||
var _node_ownerDocument;
|
||||
const sibling = node[`${direction}Sibling`];
|
||||
if (sibling) {
|
||||
node = getDescendant(sibling, direction === 'next' ? 'first' : 'last');
|
||||
if (callback(node)) {
|
||||
return node;
|
||||
}
|
||||
} else if (node.parentNode && (!isElement(node.parentNode) || !isContentEditable(node.parentNode) && node.parentNode !== ((_node_ownerDocument = node.ownerDocument) === null || _node_ownerDocument === undefined ? undefined : _node_ownerDocument.body))) {
|
||||
node = node.parentNode;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
function getDescendant(node, direction) {
|
||||
while(node.hasChildNodes()){
|
||||
node = node[`${direction}Child`];
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
export { getNextCursorPosition };
|
||||
20
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/getActiveElement.js
generated
vendored
Normal file
20
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/getActiveElement.js
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { isDisabled } from '../misc/isDisabled.js';
|
||||
|
||||
function getActiveElement(document) {
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement === null || activeElement === undefined ? undefined : activeElement.shadowRoot) {
|
||||
return getActiveElement(activeElement.shadowRoot);
|
||||
} else {
|
||||
// Browser does not yield disabled elements as document.activeElement - jsdom does
|
||||
if (isDisabled(activeElement)) {
|
||||
return document.ownerDocument ? /* istanbul ignore next */ document.ownerDocument.body : document.body;
|
||||
}
|
||||
return activeElement;
|
||||
}
|
||||
}
|
||||
function getActiveElementOrBody(document) {
|
||||
var _getActiveElement;
|
||||
return (_getActiveElement = getActiveElement(document)) !== null && _getActiveElement !== undefined ? _getActiveElement : /* istanbul ignore next */ document.body;
|
||||
}
|
||||
|
||||
export { getActiveElement, getActiveElementOrBody };
|
||||
78
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/getTabDestination.js
generated
vendored
Normal file
78
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/getTabDestination.js
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
import { isDisabled } from '../misc/isDisabled.js';
|
||||
import { isElementType } from '../misc/isElementType.js';
|
||||
import { isVisible } from '../misc/isVisible.js';
|
||||
import { FOCUSABLE_SELECTOR } from './selector.js';
|
||||
|
||||
function getTabDestination(activeElement, shift) {
|
||||
const document = activeElement.ownerDocument;
|
||||
const focusableElements = document.querySelectorAll(FOCUSABLE_SELECTOR);
|
||||
const enabledElements = Array.from(focusableElements).filter((el)=>el === activeElement || !(Number(el.getAttribute('tabindex')) < 0 || isDisabled(el)));
|
||||
// tabindex has no effect if the active element has negative tabindex
|
||||
if (Number(activeElement.getAttribute('tabindex')) >= 0) {
|
||||
enabledElements.sort((a, b)=>{
|
||||
const i = Number(a.getAttribute('tabindex'));
|
||||
const j = Number(b.getAttribute('tabindex'));
|
||||
if (i === j) {
|
||||
return 0;
|
||||
} else if (i === 0) {
|
||||
return 1;
|
||||
} else if (j === 0) {
|
||||
return -1;
|
||||
}
|
||||
return i - j;
|
||||
});
|
||||
}
|
||||
const checkedRadio = {};
|
||||
let prunedElements = [
|
||||
document.body
|
||||
];
|
||||
const activeRadioGroup = isElementType(activeElement, 'input', {
|
||||
type: 'radio'
|
||||
}) ? activeElement.name : undefined;
|
||||
enabledElements.forEach((currentElement)=>{
|
||||
const el = currentElement;
|
||||
// For radio groups keep only the active radio
|
||||
// If there is no active radio, keep only the checked radio
|
||||
// If there is no checked radio, treat like everything else
|
||||
if (isElementType(el, 'input', {
|
||||
type: 'radio'
|
||||
}) && el.name) {
|
||||
// If the active element is part of the group, add only that
|
||||
if (el === activeElement) {
|
||||
prunedElements.push(el);
|
||||
return;
|
||||
} else if (el.name === activeRadioGroup) {
|
||||
return;
|
||||
}
|
||||
// If we stumble upon a checked radio, remove the others
|
||||
if (el.checked) {
|
||||
prunedElements = prunedElements.filter((e)=>!isElementType(e, 'input', {
|
||||
type: 'radio',
|
||||
name: el.name
|
||||
}));
|
||||
prunedElements.push(el);
|
||||
checkedRadio[el.name] = el;
|
||||
return;
|
||||
}
|
||||
// If we already found the checked one, skip
|
||||
if (typeof checkedRadio[el.name] !== 'undefined') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
prunedElements.push(el);
|
||||
});
|
||||
for(let index = prunedElements.findIndex((el)=>el === activeElement);;){
|
||||
index += shift ? -1 : 1;
|
||||
// loop at overflow
|
||||
if (index === prunedElements.length) {
|
||||
index = 0;
|
||||
} else if (index === -1) {
|
||||
index = prunedElements.length - 1;
|
||||
}
|
||||
if (prunedElements[index] === activeElement || prunedElements[index] === document.body || isVisible(prunedElements[index])) {
|
||||
return prunedElements[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { getTabDestination };
|
||||
7
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/isFocusable.js
generated
vendored
Normal file
7
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/isFocusable.js
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import { FOCUSABLE_SELECTOR } from './selector.js';
|
||||
|
||||
function isFocusable(element) {
|
||||
return element.matches(FOCUSABLE_SELECTOR);
|
||||
}
|
||||
|
||||
export { isFocusable };
|
||||
17
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/selection.js
generated
vendored
Normal file
17
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/selection.js
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { isClickableInput } from '../click/isClickableInput.js';
|
||||
import { isEditableInputOrTextArea } from '../edit/isEditable.js';
|
||||
|
||||
/**
|
||||
* Determine if the element has its own selection implementation
|
||||
* and does not interact with the Document Selection API.
|
||||
*/ function hasOwnSelection(node) {
|
||||
return isElement(node) && isEditableInputOrTextArea(node);
|
||||
}
|
||||
function hasNoSelection(node) {
|
||||
return isElement(node) && isClickableInput(node);
|
||||
}
|
||||
function isElement(node) {
|
||||
return node.nodeType === 1;
|
||||
}
|
||||
|
||||
export { hasNoSelection, hasOwnSelection };
|
||||
12
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/selector.js
generated
vendored
Normal file
12
frontend/node_modules/@testing-library/user-event/dist/esm/utils/focus/selector.js
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
const FOCUSABLE_SELECTOR = [
|
||||
'input:not([type=hidden]):not([disabled])',
|
||||
'button:not([disabled])',
|
||||
'select:not([disabled])',
|
||||
'textarea:not([disabled])',
|
||||
'[contenteditable=""]',
|
||||
'[contenteditable="true"]',
|
||||
'a[href]',
|
||||
'[tabindex]:not([disabled])'
|
||||
].join(', ');
|
||||
|
||||
export { FOCUSABLE_SELECTOR };
|
||||
Reference in New Issue
Block a user