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,5 @@
function cloneEvent(event) {
return new event.constructor(event.type, event);
}
export { cloneEvent };

View File

@@ -0,0 +1,12 @@
function findClosest(element, callback) {
let el = element;
do {
if (callback(el)) {
return el;
}
el = el.parentElement;
}while (el && el !== element.ownerDocument.body)
return undefined;
}
export { findClosest };

View File

@@ -0,0 +1,8 @@
function getDocumentFromNode(el) {
return isDocument(el) ? el : el.ownerDocument;
}
function isDocument(node) {
return node.nodeType === 9;
}
export { getDocumentFromNode };

View File

@@ -0,0 +1,23 @@
function getTreeDiff(a, b) {
const treeA = [];
for(let el = a; el; el = el.parentElement){
treeA.push(el);
}
const treeB = [];
for(let el = b; el; el = el.parentElement){
treeB.push(el);
}
let i = 0;
for(;; i++){
if (i >= treeA.length || i >= treeB.length || treeA[treeA.length - 1 - i] !== treeB[treeB.length - 1 - i]) {
break;
}
}
return [
treeA.slice(0, treeA.length - i),
treeB.slice(0, treeB.length - i),
treeB.slice(treeB.length - i)
];
}
export { getTreeDiff };

View File

@@ -0,0 +1,17 @@
function getWindow(node) {
var _node_ownerDocument;
if (isDocument(node) && node.defaultView) {
return node.defaultView;
} else if ((_node_ownerDocument = node.ownerDocument) === null || _node_ownerDocument === undefined ? undefined : _node_ownerDocument.defaultView) {
return node.ownerDocument.defaultView;
}
throw new Error(`Could not determine window of node. Node was ${describe(node)}`);
}
function isDocument(node) {
return node.nodeType === 9;
}
function describe(val) {
return typeof val === 'function' ? `function ${val.name}` : val === null ? 'null' : String(val);
}
export { getWindow };

View File

@@ -0,0 +1,12 @@
function isDescendantOrSelf(potentialDescendant, potentialAncestor) {
let el = potentialDescendant;
do {
if (el === potentialAncestor) {
return true;
}
el = el.parentElement;
}while (el)
return false;
}
export { isDescendantOrSelf };

View File

@@ -0,0 +1,31 @@
import { isElementType } from './isElementType.js';
// This should probably just rely on the :disabled pseudo-class, but JSDOM doesn't implement it properly.
function isDisabled(element) {
for(let el = element; el; el = el.parentElement){
if (isElementType(el, [
'button',
'input',
'select',
'textarea',
'optgroup',
'option'
])) {
if (el.hasAttribute('disabled')) {
return true;
}
} else if (isElementType(el, 'fieldset')) {
var _el_querySelector;
if (el.hasAttribute('disabled') && !((_el_querySelector = el.querySelector(':scope > legend')) === null || _el_querySelector === undefined ? undefined : _el_querySelector.contains(element))) {
return true;
}
} else if (el.tagName.includes('-')) {
if (el.constructor.formAssociated && el.hasAttribute('disabled')) {
return true;
}
}
}
return false;
}
export { isDisabled };

View File

@@ -0,0 +1,18 @@
function isElementType(element, tag, props) {
if (element.namespaceURI && element.namespaceURI !== 'http://www.w3.org/1999/xhtml') {
return false;
}
tag = Array.isArray(tag) ? tag : [
tag
];
// tagName is uppercase in HTMLDocument and lowercase in XMLDocument
if (!tag.includes(element.tagName.toLowerCase())) {
return false;
}
if (props) {
return Object.entries(props).every(([k, v])=>element[k] === v);
}
return true;
}
export { isElementType };

View File

@@ -0,0 +1,17 @@
import { getWindow } from './getWindow.js';
function isVisible(element) {
const window = getWindow(element);
for(let el = element; el === null || el === undefined ? undefined : el.ownerDocument; el = el.parentElement){
const { display, visibility } = window.getComputedStyle(el);
if (display === 'none') {
return false;
}
if (visibility === 'hidden') {
return false;
}
}
return true;
}
export { isVisible };

View File

@@ -0,0 +1,13 @@
var ApiLevel = /*#__PURE__*/ function(ApiLevel) {
ApiLevel[ApiLevel["Trigger"] = 2] = "Trigger";
ApiLevel[ApiLevel["Call"] = 1] = "Call";
return ApiLevel;
}({});
function setLevelRef(instance, level) {
instance.levelRefs[level] = {};
}
function getLevelRef(instance, level) {
return instance.levelRefs[level];
}
export { ApiLevel, getLevelRef, setLevelRef };

View File

@@ -0,0 +1,12 @@
function wait(config) {
const delay = config.delay;
if (typeof delay !== 'number') {
return;
}
return Promise.all([
new Promise((resolve)=>globalThis.setTimeout(()=>resolve(), delay)),
config.advanceTimers(delay)
]);
}
export { wait };