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,14 @@
// jsdom does not implement Blob.text()
function readBlobText(blob, FileReader) {
return new Promise((res, rej)=>{
const fr = new FileReader();
fr.onerror = rej;
fr.onabort = rej;
fr.onload = ()=>{
res(String(fr.result));
};
fr.readAsText(blob);
});
}
export { readBlobText };

View File

@@ -0,0 +1,162 @@
import { getWindow } from '../misc/getWindow.js';
import { readBlobText } from './Blob.js';
import { createDataTransfer, getBlobFromDataTransferItem } from './DataTransfer.js';
// Clipboard is not available in jsdom
function _define_property(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
// MDN lists string|Blob|Promise<Blob|string> as possible types in ClipboardItemData
// lib.dom.d.ts lists only Promise<Blob|string>
// https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem/ClipboardItem#syntax
function createClipboardItem(window, ...blobs) {
const dataMap = Object.fromEntries(blobs.map((b)=>[
typeof b === 'string' ? 'text/plain' : b.type,
Promise.resolve(b)
]));
// use real ClipboardItem if available
/* istanbul ignore if */ if (typeof window.ClipboardItem !== 'undefined') {
return new window.ClipboardItem(dataMap);
}
return new class ClipboardItem {
get types() {
return Array.from(Object.keys(this.data));
}
async getType(type) {
const value = await this.data[type];
if (!value) {
throw new Error(`${type} is not one of the available MIME types on this item.`);
}
return value instanceof window.Blob ? value : new window.Blob([
value
], {
type
});
}
constructor(d){
_define_property(this, "data", undefined);
this.data = d;
}
}(dataMap);
}
const ClipboardStubControl = Symbol('Manage ClipboardSub');
function createClipboardStub(window, control) {
return Object.assign(new class Clipboard extends window.EventTarget {
async read() {
return Array.from(this.items);
}
async readText() {
let text = '';
for (const item of this.items){
const type = item.types.includes('text/plain') ? 'text/plain' : item.types.find((t)=>t.startsWith('text/'));
if (type) {
text += await item.getType(type).then((b)=>readBlobText(b, window.FileReader));
}
}
return text;
}
async write(data) {
this.items = data;
}
async writeText(text) {
this.items = [
createClipboardItem(window, text)
];
}
constructor(...args){
super(...args), _define_property(this, "items", []);
}
}(), {
[ClipboardStubControl]: control
});
}
function isClipboardStub(clipboard) {
return !!(clipboard === null || clipboard === undefined ? undefined : clipboard[ClipboardStubControl]);
}
function attachClipboardStubToView(window) {
if (isClipboardStub(window.navigator.clipboard)) {
return window.navigator.clipboard[ClipboardStubControl];
}
const realClipboard = Object.getOwnPropertyDescriptor(window.navigator, 'clipboard');
let stub;
const control = {
resetClipboardStub: ()=>{
stub = createClipboardStub(window, control);
},
detachClipboardStub: ()=>{
/* istanbul ignore if */ if (realClipboard) {
Object.defineProperty(window.navigator, 'clipboard', realClipboard);
} else {
Object.defineProperty(window.navigator, 'clipboard', {
value: undefined,
configurable: true
});
}
}
};
stub = createClipboardStub(window, control);
Object.defineProperty(window.navigator, 'clipboard', {
get: ()=>stub,
configurable: true
});
return stub[ClipboardStubControl];
}
function resetClipboardStubOnView(window) {
if (isClipboardStub(window.navigator.clipboard)) {
window.navigator.clipboard[ClipboardStubControl].resetClipboardStub();
}
}
function detachClipboardStubFromView(window) {
if (isClipboardStub(window.navigator.clipboard)) {
window.navigator.clipboard[ClipboardStubControl].detachClipboardStub();
}
}
async function readDataTransferFromClipboard(document) {
const window = document.defaultView;
const clipboard = window === null || window === undefined ? undefined : window.navigator.clipboard;
const items = clipboard && await clipboard.read();
if (!items) {
throw new Error('The Clipboard API is unavailable.');
}
const dt = createDataTransfer(window);
for (const item of items){
for (const type of item.types){
dt.setData(type, await item.getType(type).then((b)=>readBlobText(b, window.FileReader)));
}
}
return dt;
}
async function writeDataTransferToClipboard(document, clipboardData) {
const window = getWindow(document);
const clipboard = window.navigator.clipboard;
const items = [];
for(let i = 0; i < clipboardData.items.length; i++){
const dtItem = clipboardData.items[i];
const blob = await getBlobFromDataTransferItem(window, dtItem);
items.push(createClipboardItem(window, blob));
}
const written = clipboard && await clipboard.write(items).then(()=>true, // Can happen with other implementations that e.g. require permissions
/* istanbul ignore next */ ()=>false);
if (!written) {
throw new Error('The Clipboard API is unavailable.');
}
}
const g = globalThis;
/* istanbul ignore else */ if (typeof g.afterEach === 'function') {
g.afterEach(()=>resetClipboardStubOnView(globalThis.window));
}
/* istanbul ignore else */ if (typeof g.afterAll === 'function') {
g.afterAll(()=>detachClipboardStubFromView(globalThis.window));
}
export { attachClipboardStubToView, createClipboardItem, detachClipboardStubFromView, readDataTransferFromClipboard, resetClipboardStubOnView, writeDataTransferToClipboard };

View File

@@ -0,0 +1,133 @@
import { createFileList } from './FileList.js';
function _define_property(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
// DataTransfer is not implemented in jsdom.
// DataTransfer with FileList is being created by the browser on certain events.
class DataTransferItemStub {
getAsFile() {
return this.file;
}
getAsString(callback) {
if (typeof this.data === 'string') {
callback(this.data);
}
}
/* istanbul ignore next */ webkitGetAsEntry() {
throw new Error('not implemented');
}
constructor(dataOrFile, type){
_define_property(this, "kind", undefined);
_define_property(this, "type", undefined);
_define_property(this, "file", null);
_define_property(this, "data", undefined);
if (typeof dataOrFile === 'string') {
this.kind = 'string';
this.type = String(type);
this.data = dataOrFile;
} else {
this.kind = 'file';
this.type = dataOrFile.type;
this.file = dataOrFile;
}
}
}
class DataTransferItemListStub extends Array {
add(...args) {
const item = new DataTransferItemStub(args[0], args[1]);
this.push(item);
return item;
}
clear() {
this.splice(0, this.length);
}
remove(index) {
this.splice(index, 1);
}
}
function getTypeMatcher(type, exact) {
const [group, sub] = type.split('/');
const isGroup = !sub || sub === '*';
return (item)=>{
return exact ? item.type === (isGroup ? group : type) : isGroup ? item.type.startsWith(`${group}/`) : item.type === group;
};
}
function createDataTransferStub(window) {
return new class DataTransferStub {
getData(format) {
var _this_items_find;
const match = (_this_items_find = this.items.find(getTypeMatcher(format, true))) !== null && _this_items_find !== undefined ? _this_items_find : this.items.find(getTypeMatcher(format, false));
let text = '';
match === null || match === undefined ? undefined : match.getAsString((t)=>{
text = t;
});
return text;
}
setData(format, data) {
const matchIndex = this.items.findIndex(getTypeMatcher(format, true));
const item = new DataTransferItemStub(data, format);
if (matchIndex >= 0) {
this.items.splice(matchIndex, 1, item);
} else {
this.items.push(item);
}
}
clearData(format) {
if (format) {
const matchIndex = this.items.findIndex(getTypeMatcher(format, true));
if (matchIndex >= 0) {
this.items.remove(matchIndex);
}
} else {
this.items.clear();
}
}
get types() {
const t = [];
if (this.files.length) {
t.push('Files');
}
this.items.forEach((i)=>t.push(i.type));
Object.freeze(t);
return t;
}
/* istanbul ignore next */ setDragImage() {}
constructor(){
_define_property(this, "dropEffect", 'none');
_define_property(this, "effectAllowed", 'uninitialized');
_define_property(this, "items", new DataTransferItemListStub());
_define_property(this, "files", createFileList(window, []));
}
}();
}
function createDataTransfer(window, files = []) {
// Use real DataTransfer if available
const dt = typeof window.DataTransfer === 'undefined' ? createDataTransferStub(window) : /* istanbul ignore next */ new window.DataTransfer();
Object.defineProperty(dt, 'files', {
get: ()=>createFileList(window, files)
});
return dt;
}
async function getBlobFromDataTransferItem(window, item) {
if (item.kind === 'file') {
return item.getAsFile();
}
return new window.Blob([
await new Promise((r)=>item.getAsString(r))
], {
type: item.type
});
}
export { createDataTransfer, getBlobFromDataTransferItem };

View File

@@ -0,0 +1,22 @@
// FileList can not be created per constructor.
function createFileList(window, files) {
const list = {
...files,
length: files.length,
item: (index)=>list[index],
[Symbol.iterator]: function* nextFile() {
for(let i = 0; i < list.length; i++){
yield list[i];
}
}
};
list.constructor = window.FileList;
// guard for environments without FileList
/* istanbul ignore else */ if (window.FileList) {
Object.setPrototypeOf(list, window.FileList.prototype);
}
Object.freeze(list);
return list;
}
export { createFileList };