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:
anthonyrawlins
2025-07-11 14:06:34 +10:00
parent c6d69695a8
commit aacb45156b
6109 changed files with 777927 additions and 1 deletions

View File

@@ -0,0 +1,92 @@
'use strict';
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;
}
exports.clearInitialValue = clearInitialValue;
exports.getInitialValue = getInitialValue;
exports.getUISelection = getUISelection;
exports.getUIValue = getUIValue;
exports.hasUISelection = hasUISelection;
exports.isUISelectionStart = isUISelectionStart;
exports.isUIValue = isUIValue;
exports.setUISelection = setUISelection;
exports.setUISelectionClean = setUISelectionClean;
exports.setUISelectionRaw = setUISelectionRaw;
exports.setUIValue = setUIValue;
exports.setUIValueClean = setUIValueClean;

View File

@@ -0,0 +1,29 @@
'use strict';
var DataTransfer = require('../utils/dataTransfer/DataTransfer.js');
require('../utils/dataTransfer/Clipboard.js');
var getWindow = require('../utils/misc/getWindow.js');
var selection = require('../utils/focus/selection.js');
var UI = require('./UI.js');
function copySelection(target) {
const data = selection.hasOwnSelection(target) ? {
'text/plain': readSelectedValueFromInput(target)
} : {
'text/plain': String(target.ownerDocument.getSelection())
};
const dt = DataTransfer.createDataTransfer(getWindow.getWindow(target));
for(const type in data){
if (data[type]) {
dt.setData(type, data[type]);
}
}
return dt;
}
function readSelectedValueFromInput(target) {
const sel = UI.getUISelection(target);
const val = UI.getUIValue(target);
return val.substring(sel.startOffset, sel.endOffset);
}
exports.copySelection = copySelection;

View File

@@ -0,0 +1,18 @@
'use strict';
require('../utils/dataTransfer/Clipboard.js');
var isContentEditable = require('../utils/edit/isContentEditable.js');
var UI = require('./UI.js');
function getValueOrTextContent(element) {
// istanbul ignore if
if (!element) {
return null;
}
if (isContentEditable.isContentEditable(element)) {
return element.textContent;
}
return UI.getUIValue(element);
}
exports.getValueOrTextContent = getValueOrTextContent;

View File

@@ -0,0 +1,17 @@
'use strict';
var UI = require('./UI.js');
var getValueOrTextContent = require('./getValueOrTextContent.js');
var copySelection = require('./copySelection.js');
var trackValue = require('./trackValue.js');
exports.clearInitialValue = UI.clearInitialValue;
exports.getUISelection = UI.getUISelection;
exports.getUIValue = UI.getUIValue;
exports.setUISelection = UI.setUISelection;
exports.setUIValue = UI.setUIValue;
exports.getValueOrTextContent = getValueOrTextContent.getValueOrTextContent;
exports.copySelection = copySelection.copySelection;
exports.commitValueAfterInput = trackValue.commitValueAfterInput;

View File

@@ -0,0 +1,104 @@
'use strict';
var isElementType = require('../utils/misc/isElementType.js');
require('../utils/dataTransfer/Clipboard.js');
var trackValue = require('./trackValue.js');
var UI = require('./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 = UI.isUIValue(v);
if (isUI) {
trackValue.startTrackValue(this);
}
return {
applyNative: !!isUI,
realArgs: sanitizeValue(this, v),
then: isUI ? undefined : ()=>trackValue.trackOrSetValue(this, String(v))
};
});
}
function sanitizeValue(element, v) {
// Workaround for JSDOM
if (isElementType.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 = UI.isUISelectionStart(start);
return {
applyNative: !!isUI,
realArgs: [
Number(start),
...others
],
then: ()=>isUI ? undefined : UI.setUISelectionClean(element)
};
});
prepareInterceptor(element, 'selectionStart', function interceptorImpl(v) {
return {
realArgs: v,
then: ()=>UI.setUISelectionClean(element)
};
});
prepareInterceptor(element, 'selectionEnd', function interceptorImpl(v) {
return {
realArgs: v,
then: ()=>UI.setUISelectionClean(element)
};
});
prepareInterceptor(element, 'select', function interceptorImpl() {
return {
realArgs: [],
then: ()=>UI.setUISelectionRaw(element, {
anchorOffset: 0,
focusOffset: UI.getUIValue(element).length
})
};
});
}
function prepareRangeTextInterceptor(element) {
prepareInterceptor(element, 'setRangeText', function interceptorImpl(...realArgs) {
return {
realArgs,
then: ()=>{
UI.setUIValueClean(element);
UI.setUISelectionClean(element);
}
};
});
}
exports.prepareInterceptor = prepareInterceptor;
exports.prepareRangeTextInterceptor = prepareRangeTextInterceptor;
exports.prepareSelectionInterceptor = prepareSelectionInterceptor;
exports.prepareValueInterceptor = prepareValueInterceptor;

View File

@@ -0,0 +1,104 @@
'use strict';
var dispatchEvent = require('../event/dispatchEvent.js');
require('../utils/dataTransfer/Clipboard.js');
var getActiveElement = require('../utils/focus/getActiveElement.js');
require('@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);
dispatchEvent.dispatchDOMEvent(blurred, 'blur', {
relatedTarget: this
});
dispatchEvent.dispatchDOMEvent(blurred, 'focusout', {
relatedTarget: activeCall === thisCall ? this : null
});
}
if (activeCall === thisCall) {
focus.call(this, options);
dispatchEvent.dispatchDOMEvent(this, 'focus', {
relatedTarget: blurred
});
}
if (activeCall === thisCall) {
dispatchEvent.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);
dispatchEvent.dispatchDOMEvent(blurred, 'blur', {
relatedTarget: null
});
dispatchEvent.dispatchDOMEvent(blurred, 'focusout', {
relatedTarget: null
});
}
}
function getActiveTarget(document) {
const active = getActiveElement.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
}
});
}
}
exports.patchFocus = patchFocus;
exports.restoreFocus = restoreFocus;

View File

@@ -0,0 +1,58 @@
'use strict';
var dispatchEvent = require('../event/dispatchEvent.js');
var isElementType = require('../utils/misc/isElementType.js');
require('../utils/dataTransfer/Clipboard.js');
var UI = require('./UI.js');
require('@testing-library/dom');
var interceptor = require('./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 = UI.getInitialValue(el);
if (initialValue !== undefined) {
if (el.value !== initialValue) {
dispatchEvent.dispatchDOMEvent(el, 'change');
}
UI.clearInitialValue(el);
}
}, {
capture: true,
passive: true
});
document[isPrepared] = isPrepared;
}
function prepareElement(el) {
if (el[isPrepared]) {
return;
}
if (isElementType.isElementType(el, [
'input',
'textarea'
])) {
interceptor.prepareValueInterceptor(el);
interceptor.prepareSelectionInterceptor(el);
interceptor.prepareRangeTextInterceptor(el);
}
el[isPrepared] = isPrepared;
}
exports.prepareDocument = prepareDocument;

View File

@@ -0,0 +1,57 @@
'use strict';
require('../utils/dataTransfer/Clipboard.js');
var getWindow = require('../utils/misc/getWindow.js');
var UI = require('./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.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]) {
UI.setUIValueClean(element);
UI.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) {
UI.setUIValueClean(element);
}
if (UI.hasUISelection(element)) {
UI.setUISelection(element, {
focusOffset: isJustReactStateUpdate ? cursorOffset : element.value.length
});
}
}
exports.commitValueAfterInput = commitValueAfterInput;
exports.startTrackValue = startTrackValue;
exports.trackOrSetValue = trackOrSetValue;