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:
79
frontend/node_modules/@testing-library/user-event/dist/esm/document/UI.js
generated
vendored
Normal file
79
frontend/node_modules/@testing-library/user-event/dist/esm/document/UI.js
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
const UIValue = Symbol('Displayed value in UI');
|
||||
const UISelection = Symbol('Displayed selection in UI');
|
||||
const InitialValue = Symbol('Initial value to compare on blur');
|
||||
function isUIValue(value) {
|
||||
return typeof value === 'object' && UIValue in value;
|
||||
}
|
||||
function isUISelectionStart(start) {
|
||||
return !!start && typeof start === 'object' && UISelection in start;
|
||||
}
|
||||
function setUIValue(element, value) {
|
||||
if (element[InitialValue] === undefined) {
|
||||
element[InitialValue] = element.value;
|
||||
}
|
||||
element[UIValue] = value;
|
||||
// eslint-disable-next-line no-new-wrappers
|
||||
element.value = Object.assign(new String(value), {
|
||||
[UIValue]: true
|
||||
});
|
||||
}
|
||||
function getUIValue(element) {
|
||||
return element[UIValue] === undefined ? element.value : String(element[UIValue]);
|
||||
}
|
||||
/** Flag the IDL value as clean. This does not change the value.*/ function setUIValueClean(element) {
|
||||
element[UIValue] = undefined;
|
||||
}
|
||||
function clearInitialValue(element) {
|
||||
element[InitialValue] = undefined;
|
||||
}
|
||||
function getInitialValue(element) {
|
||||
return element[InitialValue];
|
||||
}
|
||||
function setUISelectionRaw(element, selection) {
|
||||
element[UISelection] = selection;
|
||||
}
|
||||
function setUISelection(element, { focusOffset: focusOffsetParam, anchorOffset: anchorOffsetParam = focusOffsetParam }, mode = 'replace') {
|
||||
const valueLength = getUIValue(element).length;
|
||||
const sanitizeOffset = (o)=>Math.max(0, Math.min(valueLength, o));
|
||||
const anchorOffset = mode === 'replace' || element[UISelection] === undefined ? sanitizeOffset(anchorOffsetParam) : element[UISelection].anchorOffset;
|
||||
const focusOffset = sanitizeOffset(focusOffsetParam);
|
||||
const startOffset = Math.min(anchorOffset, focusOffset);
|
||||
const endOffset = Math.max(anchorOffset, focusOffset);
|
||||
element[UISelection] = {
|
||||
anchorOffset,
|
||||
focusOffset
|
||||
};
|
||||
if (element.selectionStart === startOffset && element.selectionEnd === endOffset) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-new-wrappers
|
||||
const startObj = Object.assign(new Number(startOffset), {
|
||||
[UISelection]: true
|
||||
});
|
||||
try {
|
||||
element.setSelectionRange(startObj, endOffset);
|
||||
} catch {
|
||||
// DOMException for invalid state is expected when calling this
|
||||
// on an element without support for setSelectionRange
|
||||
}
|
||||
}
|
||||
function getUISelection(element) {
|
||||
var _element_selectionStart, _element_selectionEnd, _element_UISelection;
|
||||
const sel = (_element_UISelection = element[UISelection]) !== null && _element_UISelection !== undefined ? _element_UISelection : {
|
||||
anchorOffset: (_element_selectionStart = element.selectionStart) !== null && _element_selectionStart !== undefined ? _element_selectionStart : 0,
|
||||
focusOffset: (_element_selectionEnd = element.selectionEnd) !== null && _element_selectionEnd !== undefined ? _element_selectionEnd : 0
|
||||
};
|
||||
return {
|
||||
...sel,
|
||||
startOffset: Math.min(sel.anchorOffset, sel.focusOffset),
|
||||
endOffset: Math.max(sel.anchorOffset, sel.focusOffset)
|
||||
};
|
||||
}
|
||||
function hasUISelection(element) {
|
||||
return !!element[UISelection];
|
||||
}
|
||||
/** Flag the IDL selection as clean. This does not change the selection. */ function setUISelectionClean(element) {
|
||||
element[UISelection] = undefined;
|
||||
}
|
||||
|
||||
export { clearInitialValue, getInitialValue, getUISelection, getUIValue, hasUISelection, isUISelectionStart, isUIValue, setUISelection, setUISelectionClean, setUISelectionRaw, setUIValue, setUIValueClean };
|
||||
27
frontend/node_modules/@testing-library/user-event/dist/esm/document/copySelection.js
generated
vendored
Normal file
27
frontend/node_modules/@testing-library/user-event/dist/esm/document/copySelection.js
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createDataTransfer } from '../utils/dataTransfer/DataTransfer.js';
|
||||
import '../utils/dataTransfer/Clipboard.js';
|
||||
import { getWindow } from '../utils/misc/getWindow.js';
|
||||
import { hasOwnSelection } from '../utils/focus/selection.js';
|
||||
import { getUISelection, getUIValue } from './UI.js';
|
||||
|
||||
function copySelection(target) {
|
||||
const data = hasOwnSelection(target) ? {
|
||||
'text/plain': readSelectedValueFromInput(target)
|
||||
} : {
|
||||
'text/plain': String(target.ownerDocument.getSelection())
|
||||
};
|
||||
const dt = createDataTransfer(getWindow(target));
|
||||
for(const type in data){
|
||||
if (data[type]) {
|
||||
dt.setData(type, data[type]);
|
||||
}
|
||||
}
|
||||
return dt;
|
||||
}
|
||||
function readSelectedValueFromInput(target) {
|
||||
const sel = getUISelection(target);
|
||||
const val = getUIValue(target);
|
||||
return val.substring(sel.startOffset, sel.endOffset);
|
||||
}
|
||||
|
||||
export { copySelection };
|
||||
16
frontend/node_modules/@testing-library/user-event/dist/esm/document/getValueOrTextContent.js
generated
vendored
Normal file
16
frontend/node_modules/@testing-library/user-event/dist/esm/document/getValueOrTextContent.js
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import '../utils/dataTransfer/Clipboard.js';
|
||||
import { isContentEditable } from '../utils/edit/isContentEditable.js';
|
||||
import { getUIValue } from './UI.js';
|
||||
|
||||
function getValueOrTextContent(element) {
|
||||
// istanbul ignore if
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
if (isContentEditable(element)) {
|
||||
return element.textContent;
|
||||
}
|
||||
return getUIValue(element);
|
||||
}
|
||||
|
||||
export { getValueOrTextContent };
|
||||
4
frontend/node_modules/@testing-library/user-event/dist/esm/document/index.js
generated
vendored
Normal file
4
frontend/node_modules/@testing-library/user-event/dist/esm/document/index.js
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export { clearInitialValue, getUISelection, getUIValue, setUISelection, setUIValue } from './UI.js';
|
||||
export { getValueOrTextContent } from './getValueOrTextContent.js';
|
||||
export { copySelection } from './copySelection.js';
|
||||
export { commitValueAfterInput } from './trackValue.js';
|
||||
99
frontend/node_modules/@testing-library/user-event/dist/esm/document/interceptor.js
generated
vendored
Normal file
99
frontend/node_modules/@testing-library/user-event/dist/esm/document/interceptor.js
generated
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
import { isElementType } from '../utils/misc/isElementType.js';
|
||||
import '../utils/dataTransfer/Clipboard.js';
|
||||
import { startTrackValue, trackOrSetValue } from './trackValue.js';
|
||||
import { setUISelectionClean, setUISelectionRaw, getUIValue, setUIValueClean, isUIValue, isUISelectionStart } from './UI.js';
|
||||
|
||||
const Interceptor = Symbol('Interceptor for programmatical calls');
|
||||
function prepareInterceptor(element, propName, interceptorImpl) {
|
||||
const prototypeDescriptor = Object.getOwnPropertyDescriptor(element.constructor.prototype, propName);
|
||||
const objectDescriptor = Object.getOwnPropertyDescriptor(element, propName);
|
||||
const target = (prototypeDescriptor === null || prototypeDescriptor === undefined ? undefined : prototypeDescriptor.set) ? 'set' : 'value';
|
||||
/* istanbul ignore if */ if (typeof (prototypeDescriptor === null || prototypeDescriptor === undefined ? undefined : prototypeDescriptor[target]) !== 'function' || prototypeDescriptor[target][Interceptor]) {
|
||||
throw new Error(`Element ${element.tagName} does not implement "${String(propName)}".`);
|
||||
}
|
||||
function intercept(...args) {
|
||||
const { applyNative = false, realArgs, then } = interceptorImpl.call(this, ...args);
|
||||
const realFunc = (!applyNative && objectDescriptor || prototypeDescriptor)[target];
|
||||
if (target === 'set') {
|
||||
realFunc.call(this, realArgs);
|
||||
} else {
|
||||
realFunc.call(this, ...realArgs);
|
||||
}
|
||||
then === null || then === undefined ? undefined : then();
|
||||
}
|
||||
intercept[Interceptor] = Interceptor;
|
||||
Object.defineProperty(element, propName, {
|
||||
...objectDescriptor !== null && objectDescriptor !== undefined ? objectDescriptor : prototypeDescriptor,
|
||||
[target]: intercept
|
||||
});
|
||||
}
|
||||
function prepareValueInterceptor(element) {
|
||||
prepareInterceptor(element, 'value', function interceptorImpl(v) {
|
||||
const isUI = isUIValue(v);
|
||||
if (isUI) {
|
||||
startTrackValue(this);
|
||||
}
|
||||
return {
|
||||
applyNative: !!isUI,
|
||||
realArgs: sanitizeValue(this, v),
|
||||
then: isUI ? undefined : ()=>trackOrSetValue(this, String(v))
|
||||
};
|
||||
});
|
||||
}
|
||||
function sanitizeValue(element, v) {
|
||||
// Workaround for JSDOM
|
||||
if (isElementType(element, 'input', {
|
||||
type: 'number'
|
||||
}) && String(v) !== '' && !Number.isNaN(Number(v))) {
|
||||
// Setting value to "1." results in `null` in JSDOM
|
||||
return String(Number(v));
|
||||
}
|
||||
return String(v);
|
||||
}
|
||||
function prepareSelectionInterceptor(element) {
|
||||
prepareInterceptor(element, 'setSelectionRange', function interceptorImpl(start, ...others) {
|
||||
const isUI = isUISelectionStart(start);
|
||||
return {
|
||||
applyNative: !!isUI,
|
||||
realArgs: [
|
||||
Number(start),
|
||||
...others
|
||||
],
|
||||
then: ()=>isUI ? undefined : setUISelectionClean(element)
|
||||
};
|
||||
});
|
||||
prepareInterceptor(element, 'selectionStart', function interceptorImpl(v) {
|
||||
return {
|
||||
realArgs: v,
|
||||
then: ()=>setUISelectionClean(element)
|
||||
};
|
||||
});
|
||||
prepareInterceptor(element, 'selectionEnd', function interceptorImpl(v) {
|
||||
return {
|
||||
realArgs: v,
|
||||
then: ()=>setUISelectionClean(element)
|
||||
};
|
||||
});
|
||||
prepareInterceptor(element, 'select', function interceptorImpl() {
|
||||
return {
|
||||
realArgs: [],
|
||||
then: ()=>setUISelectionRaw(element, {
|
||||
anchorOffset: 0,
|
||||
focusOffset: getUIValue(element).length
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
function prepareRangeTextInterceptor(element) {
|
||||
prepareInterceptor(element, 'setRangeText', function interceptorImpl(...realArgs) {
|
||||
return {
|
||||
realArgs,
|
||||
then: ()=>{
|
||||
setUIValueClean(element);
|
||||
setUISelectionClean(element);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export { prepareInterceptor, prepareRangeTextInterceptor, prepareSelectionInterceptor, prepareValueInterceptor };
|
||||
101
frontend/node_modules/@testing-library/user-event/dist/esm/document/patchFocus.js
generated
vendored
Normal file
101
frontend/node_modules/@testing-library/user-event/dist/esm/document/patchFocus.js
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
import { dispatchDOMEvent } from '../event/dispatchEvent.js';
|
||||
import '../utils/dataTransfer/Clipboard.js';
|
||||
import { getActiveElement } from '../utils/focus/getActiveElement.js';
|
||||
import '@testing-library/dom';
|
||||
|
||||
const patched = Symbol('patched focus/blur methods');
|
||||
function patchFocus(HTMLElement) {
|
||||
if (HTMLElement.prototype[patched]) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const { focus, blur } = HTMLElement.prototype;
|
||||
Object.defineProperties(HTMLElement.prototype, {
|
||||
focus: {
|
||||
configurable: true,
|
||||
get: ()=>patchedFocus
|
||||
},
|
||||
blur: {
|
||||
configurable: true,
|
||||
get: ()=>patchedBlur
|
||||
},
|
||||
[patched]: {
|
||||
configurable: true,
|
||||
get: ()=>({
|
||||
focus,
|
||||
blur
|
||||
})
|
||||
}
|
||||
});
|
||||
let activeCall;
|
||||
function patchedFocus(options) {
|
||||
if (this.ownerDocument.visibilityState !== 'hidden') {
|
||||
return focus.call(this, options);
|
||||
}
|
||||
const blurred = getActiveTarget(this.ownerDocument);
|
||||
if (blurred === this) {
|
||||
return;
|
||||
}
|
||||
const thisCall = Symbol('focus call');
|
||||
activeCall = thisCall;
|
||||
if (blurred) {
|
||||
blur.call(blurred);
|
||||
dispatchDOMEvent(blurred, 'blur', {
|
||||
relatedTarget: this
|
||||
});
|
||||
dispatchDOMEvent(blurred, 'focusout', {
|
||||
relatedTarget: activeCall === thisCall ? this : null
|
||||
});
|
||||
}
|
||||
if (activeCall === thisCall) {
|
||||
focus.call(this, options);
|
||||
dispatchDOMEvent(this, 'focus', {
|
||||
relatedTarget: blurred
|
||||
});
|
||||
}
|
||||
if (activeCall === thisCall) {
|
||||
dispatchDOMEvent(this, 'focusin', {
|
||||
relatedTarget: blurred
|
||||
});
|
||||
}
|
||||
}
|
||||
function patchedBlur() {
|
||||
if (this.ownerDocument.visibilityState !== 'hidden') {
|
||||
return blur.call(this);
|
||||
}
|
||||
const blurred = getActiveTarget(this.ownerDocument);
|
||||
if (blurred !== this) {
|
||||
return;
|
||||
}
|
||||
const thisCall = Symbol('blur call');
|
||||
activeCall = thisCall;
|
||||
blur.call(this);
|
||||
dispatchDOMEvent(blurred, 'blur', {
|
||||
relatedTarget: null
|
||||
});
|
||||
dispatchDOMEvent(blurred, 'focusout', {
|
||||
relatedTarget: null
|
||||
});
|
||||
}
|
||||
}
|
||||
function getActiveTarget(document) {
|
||||
const active = getActiveElement(document);
|
||||
return (active === null || active === undefined ? undefined : active.tagName) === 'BODY' ? null : active;
|
||||
}
|
||||
function restoreFocus(HTMLElement) {
|
||||
if (HTMLElement.prototype[patched]) {
|
||||
const { focus, blur } = HTMLElement.prototype[patched];
|
||||
Object.defineProperties(HTMLElement.prototype, {
|
||||
focus: {
|
||||
configurable: true,
|
||||
get: ()=>focus
|
||||
},
|
||||
blur: {
|
||||
configurable: true,
|
||||
get: ()=>blur
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { patchFocus, restoreFocus };
|
||||
56
frontend/node_modules/@testing-library/user-event/dist/esm/document/prepareDocument.js
generated
vendored
Normal file
56
frontend/node_modules/@testing-library/user-event/dist/esm/document/prepareDocument.js
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
import { dispatchDOMEvent } from '../event/dispatchEvent.js';
|
||||
import { isElementType } from '../utils/misc/isElementType.js';
|
||||
import '../utils/dataTransfer/Clipboard.js';
|
||||
import { getInitialValue, clearInitialValue } from './UI.js';
|
||||
import '@testing-library/dom';
|
||||
import { prepareValueInterceptor, prepareSelectionInterceptor, prepareRangeTextInterceptor } from './interceptor.js';
|
||||
|
||||
const isPrepared = Symbol('Node prepared with document state workarounds');
|
||||
function prepareDocument(document) {
|
||||
if (document[isPrepared]) {
|
||||
return;
|
||||
}
|
||||
document.addEventListener('focus', (e)=>{
|
||||
const el = e.target;
|
||||
prepareElement(el);
|
||||
}, {
|
||||
capture: true,
|
||||
passive: true
|
||||
});
|
||||
// Our test environment defaults to `document.body` as `activeElement`.
|
||||
// In other environments this might be `null` when preparing.
|
||||
// istanbul ignore else
|
||||
if (document.activeElement) {
|
||||
prepareElement(document.activeElement);
|
||||
}
|
||||
document.addEventListener('blur', (e)=>{
|
||||
const el = e.target;
|
||||
const initialValue = getInitialValue(el);
|
||||
if (initialValue !== undefined) {
|
||||
if (el.value !== initialValue) {
|
||||
dispatchDOMEvent(el, 'change');
|
||||
}
|
||||
clearInitialValue(el);
|
||||
}
|
||||
}, {
|
||||
capture: true,
|
||||
passive: true
|
||||
});
|
||||
document[isPrepared] = isPrepared;
|
||||
}
|
||||
function prepareElement(el) {
|
||||
if (el[isPrepared]) {
|
||||
return;
|
||||
}
|
||||
if (isElementType(el, [
|
||||
'input',
|
||||
'textarea'
|
||||
])) {
|
||||
prepareValueInterceptor(el);
|
||||
prepareSelectionInterceptor(el);
|
||||
prepareRangeTextInterceptor(el);
|
||||
}
|
||||
el[isPrepared] = isPrepared;
|
||||
}
|
||||
|
||||
export { prepareDocument };
|
||||
53
frontend/node_modules/@testing-library/user-event/dist/esm/document/trackValue.js
generated
vendored
Normal file
53
frontend/node_modules/@testing-library/user-event/dist/esm/document/trackValue.js
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
import '../utils/dataTransfer/Clipboard.js';
|
||||
import { getWindow } from '../utils/misc/getWindow.js';
|
||||
import { setUIValueClean, setUISelection, hasUISelection } from './UI.js';
|
||||
|
||||
const TrackChanges = Symbol('Track programmatic changes for React workaround');
|
||||
// When the input event happens in the browser, React executes all event handlers
|
||||
// and if they change state of a controlled value, nothing happens.
|
||||
// But when we trigger the event handlers in test environment with React@17,
|
||||
// the changes are rolled back before the state update is applied.
|
||||
// This results in a reset cursor.
|
||||
// There might be a better way to work around if we figure out
|
||||
// why the batched update is executed differently in our test environment.
|
||||
function isReact17Element(element) {
|
||||
return Object.getOwnPropertyNames(element).some((k)=>k.startsWith('__react')) && getWindow(element).REACT_VERSION === 17;
|
||||
}
|
||||
function startTrackValue(element) {
|
||||
if (!isReact17Element(element)) {
|
||||
return;
|
||||
}
|
||||
element[TrackChanges] = {
|
||||
previousValue: String(element.value),
|
||||
tracked: []
|
||||
};
|
||||
}
|
||||
function trackOrSetValue(element, v) {
|
||||
var _element_TrackChanges_tracked, _element_TrackChanges;
|
||||
(_element_TrackChanges = element[TrackChanges]) === null || _element_TrackChanges === undefined ? undefined : (_element_TrackChanges_tracked = _element_TrackChanges.tracked) === null || _element_TrackChanges_tracked === undefined ? undefined : _element_TrackChanges_tracked.push(v);
|
||||
if (!element[TrackChanges]) {
|
||||
setUIValueClean(element);
|
||||
setUISelection(element, {
|
||||
focusOffset: v.length
|
||||
});
|
||||
}
|
||||
}
|
||||
function commitValueAfterInput(element, cursorOffset) {
|
||||
var _changes_tracked;
|
||||
const changes = element[TrackChanges];
|
||||
element[TrackChanges] = undefined;
|
||||
if (!(changes === null || changes === undefined ? undefined : (_changes_tracked = changes.tracked) === null || _changes_tracked === undefined ? undefined : _changes_tracked.length)) {
|
||||
return;
|
||||
}
|
||||
const isJustReactStateUpdate = changes.tracked.length === 2 && changes.tracked[0] === changes.previousValue && changes.tracked[1] === element.value;
|
||||
if (!isJustReactStateUpdate) {
|
||||
setUIValueClean(element);
|
||||
}
|
||||
if (hasUISelection(element)) {
|
||||
setUISelection(element, {
|
||||
focusOffset: isJustReactStateUpdate ? cursorOffset : element.value.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { commitValueAfterInput, startTrackValue, trackOrSetValue };
|
||||
Reference in New Issue
Block a user