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,189 @@
import { V8Coverage } from "collect-v8-coverage";
import { CoverageMap, CoverageMapData } from "istanbul-lib-coverage";
import { ConsoleBuffer } from "@jest/console";
import { Circus, Config, TransformTypes } from "@jest/types";
import { IHasteFS, IModuleMap } from "jest-haste-map";
import Resolver from "jest-resolve";
//#region src/types.d.ts
type RuntimeTransformResult = TransformTypes.TransformResult;
type V8CoverageResult = Array<{
codeTransformResult: RuntimeTransformResult | undefined;
result: V8Coverage[number];
}>;
type SerializableError = TestResult.SerializableError;
type FailedAssertion = {
matcherName?: string;
message?: string;
actual?: unknown;
pass?: boolean;
passed?: boolean;
expected?: unknown;
isNot?: boolean;
stack?: string;
error?: unknown;
};
type AssertionLocation = {
fullName: string;
path: string;
};
type Status = AssertionResult['status'];
type AssertionResult = TestResult.AssertionResult;
type FormattedAssertionResult = Pick<AssertionResult, 'ancestorTitles' | 'fullName' | 'location' | 'status' | 'title' | 'duration'> & {
failureMessages: AssertionResult['failureMessages'] | null;
};
type AggregatedResultWithoutCoverage = {
numFailedTests: number;
numFailedTestSuites: number;
numPassedTests: number;
numPassedTestSuites: number;
numPendingTests: number;
numTodoTests: number;
numPendingTestSuites: number;
numRuntimeErrorTestSuites: number;
numTotalTests: number;
numTotalTestSuites: number;
openHandles: Array<Error>;
snapshot: SnapshotSummary;
startTime: number;
success: boolean;
testResults: Array<TestResult>;
wasInterrupted: boolean;
runExecError?: SerializableError;
};
type AggregatedResult = AggregatedResultWithoutCoverage & {
coverageMap?: CoverageMap | null;
};
type TestResultsProcessor = (results: AggregatedResult) => AggregatedResult | Promise<AggregatedResult>;
type Suite = {
title: string;
suites: Array<Suite>;
tests: Array<AssertionResult>;
};
type TestCaseResult = AssertionResult & {
startedAt?: number | null;
};
type TestResult = {
console?: ConsoleBuffer;
coverage?: CoverageMapData;
displayName?: Config.DisplayName;
/**
* Whether [`test.failing()`](https://jestjs.io/docs/api#testfailingname-fn-timeout)
* was used.
*/
failing?: boolean;
failureMessage?: string | null;
leaks: boolean;
memoryUsage?: number;
numFailingTests: number;
numPassingTests: number;
numPendingTests: number;
numTodoTests: number;
openHandles: Array<Error>;
perfStats: {
end: number;
loadTestEnvironmentEnd: number;
loadTestEnvironmentStart: number;
runtime: number;
setupAfterEnvEnd: number;
setupAfterEnvStart: number;
setupFilesEnd: number;
setupFilesStart: number;
slow: boolean;
start: number;
};
skipped: boolean;
snapshot: {
added: number;
fileDeleted: boolean;
matched: number;
unchecked: number;
uncheckedKeys: Array<string>;
unmatched: number;
updated: number;
};
testExecError?: SerializableError;
testFilePath: string;
testResults: Array<AssertionResult>;
v8Coverage?: V8CoverageResult;
};
type FormattedTestResult = {
message: string;
name: string;
summary: string;
status: 'failed' | 'passed' | 'skipped' | 'focused';
startTime: number;
endTime: number;
coverage: unknown;
assertionResults: Array<FormattedAssertionResult>;
};
type FormattedTestResults = {
coverageMap?: CoverageMap | null | undefined;
numFailedTests: number;
numFailedTestSuites: number;
numPassedTests: number;
numPassedTestSuites: number;
numPendingTests: number;
numPendingTestSuites: number;
numRuntimeErrorTestSuites: number;
numTotalTests: number;
numTotalTestSuites: number;
snapshot: SnapshotSummary;
startTime: number;
success: boolean;
testResults: Array<FormattedTestResult>;
wasInterrupted: boolean;
};
type CodeCoverageReporter = unknown;
type CodeCoverageFormatter = (coverage: CoverageMapData | null | undefined, reporter: CodeCoverageReporter) => Record<string, unknown> | null | undefined;
type UncheckedSnapshot = {
filePath: string;
keys: Array<string>;
};
type SnapshotSummary = {
added: number;
didUpdate: boolean;
failure: boolean;
filesAdded: number;
filesRemoved: number;
filesRemovedList: Array<string>;
filesUnmatched: number;
filesUpdated: number;
matched: number;
total: number;
unchecked: number;
uncheckedKeysByFile: Array<UncheckedSnapshot>;
unmatched: number;
updated: number;
};
type Test = {
context: TestContext;
duration?: number;
path: string;
};
type TestContext = {
config: Config.ProjectConfig;
hasteFS: IHasteFS;
moduleMap: IModuleMap;
resolver: Resolver;
};
type TestEvents = {
'test-file-start': [Test];
'test-file-success': [Test, TestResult];
'test-file-failure': [Test, SerializableError];
'test-case-start': [string, Circus.TestCaseStartInfo];
'test-case-result': [string, TestCaseResult];
};
type TestFileEvent<T extends keyof TestEvents = keyof TestEvents> = (eventName: T, args: TestEvents[T]) => unknown;
//#endregion
//#region src/formatTestResults.d.ts
declare function formatTestResults(results: AggregatedResult, codeCoverageFormatter?: CodeCoverageFormatter, reporter?: CodeCoverageReporter): FormattedTestResults;
//#endregion
//#region src/helpers.d.ts
declare const makeEmptyAggregatedTestResult: () => AggregatedResult;
declare const buildFailureTestResult: (testPath: string, err: SerializableError) => TestResult;
declare const addResult: (aggregatedResults: AggregatedResult, testResult: TestResult) => void;
declare const createEmptyTestResult: () => TestResult;
//#endregion
export { AggregatedResult, AssertionLocation, AssertionResult, FailedAssertion, FormattedTestResults, RuntimeTransformResult, SerializableError, SnapshotSummary, Status, Suite, Test, TestCaseResult, TestContext, TestEvents, TestFileEvent, TestResult, TestResultsProcessor, V8CoverageResult, addResult, buildFailureTestResult, createEmptyTestResult, formatTestResults, makeEmptyAggregatedTestResult };

View File

@@ -0,0 +1,243 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {V8Coverage} from 'collect-v8-coverage';
import {CoverageMap, CoverageMapData} from 'istanbul-lib-coverage';
import {ConsoleBuffer} from '@jest/console';
import {
Circus,
Config,
TestResult as TestResult_2,
TransformTypes,
} from '@jest/types';
import {IHasteFS, IModuleMap} from 'jest-haste-map';
import Resolver from 'jest-resolve';
export declare const addResult: (
aggregatedResults: AggregatedResult,
testResult: TestResult,
) => void;
export declare type AggregatedResult = AggregatedResultWithoutCoverage & {
coverageMap?: CoverageMap | null;
};
declare type AggregatedResultWithoutCoverage = {
numFailedTests: number;
numFailedTestSuites: number;
numPassedTests: number;
numPassedTestSuites: number;
numPendingTests: number;
numTodoTests: number;
numPendingTestSuites: number;
numRuntimeErrorTestSuites: number;
numTotalTests: number;
numTotalTestSuites: number;
openHandles: Array<Error>;
snapshot: SnapshotSummary;
startTime: number;
success: boolean;
testResults: Array<TestResult>;
wasInterrupted: boolean;
runExecError?: SerializableError;
};
export declare type AssertionLocation = {
fullName: string;
path: string;
};
export declare type AssertionResult = TestResult_2.AssertionResult;
export declare const buildFailureTestResult: (
testPath: string,
err: SerializableError,
) => TestResult;
declare type CodeCoverageFormatter = (
coverage: CoverageMapData | null | undefined,
reporter: CodeCoverageReporter,
) => Record<string, unknown> | null | undefined;
declare type CodeCoverageReporter = unknown;
export declare const createEmptyTestResult: () => TestResult;
export declare type FailedAssertion = {
matcherName?: string;
message?: string;
actual?: unknown;
pass?: boolean;
passed?: boolean;
expected?: unknown;
isNot?: boolean;
stack?: string;
error?: unknown;
};
declare type FormattedAssertionResult = Pick<
AssertionResult,
'ancestorTitles' | 'fullName' | 'location' | 'status' | 'title' | 'duration'
> & {
failureMessages: AssertionResult['failureMessages'] | null;
};
declare type FormattedTestResult = {
message: string;
name: string;
summary: string;
status: 'failed' | 'passed' | 'skipped' | 'focused';
startTime: number;
endTime: number;
coverage: unknown;
assertionResults: Array<FormattedAssertionResult>;
};
export declare type FormattedTestResults = {
coverageMap?: CoverageMap | null | undefined;
numFailedTests: number;
numFailedTestSuites: number;
numPassedTests: number;
numPassedTestSuites: number;
numPendingTests: number;
numPendingTestSuites: number;
numRuntimeErrorTestSuites: number;
numTotalTests: number;
numTotalTestSuites: number;
snapshot: SnapshotSummary;
startTime: number;
success: boolean;
testResults: Array<FormattedTestResult>;
wasInterrupted: boolean;
};
export declare function formatTestResults(
results: AggregatedResult,
codeCoverageFormatter?: CodeCoverageFormatter,
reporter?: CodeCoverageReporter,
): FormattedTestResults;
export declare const makeEmptyAggregatedTestResult: () => AggregatedResult;
export declare type RuntimeTransformResult = TransformTypes.TransformResult;
export declare type SerializableError = TestResult_2.SerializableError;
export declare type SnapshotSummary = {
added: number;
didUpdate: boolean;
failure: boolean;
filesAdded: number;
filesRemoved: number;
filesRemovedList: Array<string>;
filesUnmatched: number;
filesUpdated: number;
matched: number;
total: number;
unchecked: number;
uncheckedKeysByFile: Array<UncheckedSnapshot>;
unmatched: number;
updated: number;
};
export declare type Status = AssertionResult['status'];
export declare type Suite = {
title: string;
suites: Array<Suite>;
tests: Array<AssertionResult>;
};
export declare type Test = {
context: TestContext;
duration?: number;
path: string;
};
export declare type TestCaseResult = AssertionResult & {
startedAt?: number | null;
};
export declare type TestContext = {
config: Config.ProjectConfig;
hasteFS: IHasteFS;
moduleMap: IModuleMap;
resolver: Resolver;
};
export declare type TestEvents = {
'test-file-start': [Test];
'test-file-success': [Test, TestResult];
'test-file-failure': [Test, SerializableError];
'test-case-start': [string, Circus.TestCaseStartInfo];
'test-case-result': [string, TestCaseResult];
};
export declare type TestFileEvent<
T extends keyof TestEvents = keyof TestEvents,
> = (eventName: T, args: TestEvents[T]) => unknown;
export declare type TestResult = {
console?: ConsoleBuffer;
coverage?: CoverageMapData;
displayName?: Config.DisplayName;
/**
* Whether [`test.failing()`](https://jestjs.io/docs/api#testfailingname-fn-timeout)
* was used.
*/
failing?: boolean;
failureMessage?: string | null;
leaks: boolean;
memoryUsage?: number;
numFailingTests: number;
numPassingTests: number;
numPendingTests: number;
numTodoTests: number;
openHandles: Array<Error>;
perfStats: {
end: number;
loadTestEnvironmentEnd: number;
loadTestEnvironmentStart: number;
runtime: number;
setupAfterEnvEnd: number;
setupAfterEnvStart: number;
setupFilesEnd: number;
setupFilesStart: number;
slow: boolean;
start: number;
};
skipped: boolean;
snapshot: {
added: number;
fileDeleted: boolean;
matched: number;
unchecked: number;
uncheckedKeys: Array<string>;
unmatched: number;
updated: number;
};
testExecError?: SerializableError;
testFilePath: string;
testResults: Array<AssertionResult>;
v8Coverage?: V8CoverageResult;
};
export declare type TestResultsProcessor = (
results: AggregatedResult,
) => AggregatedResult | Promise<AggregatedResult>;
declare type UncheckedSnapshot = {
filePath: string;
keys: Array<string>;
};
export declare type V8CoverageResult = Array<{
codeTransformResult: RuntimeTransformResult | undefined;
result: V8Coverage[number];
}>;
export {};

330
frontend/node_modules/@jest/test-result/build/index.js generated vendored Normal file
View File

@@ -0,0 +1,330 @@
/*!
* /**
* * Copyright (c) Meta Platforms, Inc. and affiliates.
* *
* * This source code is licensed under the MIT license found in the
* * LICENSE file in the root directory of this source tree.
* * /
*/
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./src/formatTestResults.ts":
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports["default"] = formatTestResults;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const formatTestResult = (testResult, codeCoverageFormatter, reporter) => {
if (testResult.testExecError) {
const now = Date.now();
return {
assertionResults: testResult.testResults,
coverage: {},
endTime: now,
message: testResult.failureMessage ?? testResult.testExecError.message,
name: testResult.testFilePath,
startTime: now,
status: 'failed',
summary: ''
};
}
if (testResult.skipped) {
const now = Date.now();
return {
assertionResults: testResult.testResults,
coverage: {},
endTime: now,
message: testResult.failureMessage ?? '',
name: testResult.testFilePath,
startTime: now,
status: 'skipped',
summary: ''
};
}
const allTestsExecuted = testResult.numPendingTests === 0;
const allTestsPassed = testResult.numFailingTests === 0;
return {
assertionResults: testResult.testResults,
coverage: codeCoverageFormatter == null ? testResult.coverage : codeCoverageFormatter(testResult.coverage, reporter),
endTime: testResult.perfStats.end,
message: testResult.failureMessage ?? '',
name: testResult.testFilePath,
startTime: testResult.perfStats.start,
status: allTestsPassed ? allTestsExecuted ? 'passed' : 'focused' : 'failed',
summary: ''
};
};
function formatTestResults(results, codeCoverageFormatter, reporter) {
const testResults = results.testResults.map(testResult => formatTestResult(testResult, codeCoverageFormatter, reporter));
return {
...results,
testResults
};
}
/***/ }),
/***/ "./src/helpers.ts":
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports.makeEmptyAggregatedTestResult = exports.createEmptyTestResult = exports.buildFailureTestResult = exports.addResult = void 0;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const makeEmptyAggregatedTestResult = () => ({
numFailedTestSuites: 0,
numFailedTests: 0,
numPassedTestSuites: 0,
numPassedTests: 0,
numPendingTestSuites: 0,
numPendingTests: 0,
numRuntimeErrorTestSuites: 0,
numTodoTests: 0,
numTotalTestSuites: 0,
numTotalTests: 0,
openHandles: [],
snapshot: {
added: 0,
didUpdate: false,
// is set only after the full run
failure: false,
filesAdded: 0,
// combines individual test results + removed files after the full run
filesRemoved: 0,
filesRemovedList: [],
filesUnmatched: 0,
filesUpdated: 0,
matched: 0,
total: 0,
unchecked: 0,
uncheckedKeysByFile: [],
unmatched: 0,
updated: 0
},
startTime: 0,
success: true,
testResults: [],
wasInterrupted: false
});
exports.makeEmptyAggregatedTestResult = makeEmptyAggregatedTestResult;
const buildFailureTestResult = (testPath, err) => ({
console: undefined,
displayName: undefined,
failureMessage: null,
leaks: false,
numFailingTests: 0,
numPassingTests: 0,
numPendingTests: 0,
numTodoTests: 0,
openHandles: [],
perfStats: {
end: 0,
loadTestEnvironmentEnd: 0,
loadTestEnvironmentStart: 0,
runtime: 0,
setupAfterEnvEnd: 0,
setupAfterEnvStart: 0,
setupFilesEnd: 0,
setupFilesStart: 0,
slow: false,
start: 0
},
skipped: false,
snapshot: {
added: 0,
fileDeleted: false,
matched: 0,
unchecked: 0,
uncheckedKeys: [],
unmatched: 0,
updated: 0
},
testExecError: err,
testFilePath: testPath,
testResults: []
});
// Add individual test result to an aggregated test result
exports.buildFailureTestResult = buildFailureTestResult;
const addResult = (aggregatedResults, testResult) => {
// `todos` are new as of Jest 24, and not all runners return it.
// Set it to `0` to avoid `NaN`
if (!testResult.numTodoTests) {
testResult.numTodoTests = 0;
}
aggregatedResults.testResults.push(testResult);
aggregatedResults.numTotalTests += testResult.numPassingTests + testResult.numFailingTests + testResult.numPendingTests + testResult.numTodoTests;
aggregatedResults.numFailedTests += testResult.numFailingTests;
aggregatedResults.numPassedTests += testResult.numPassingTests;
aggregatedResults.numPendingTests += testResult.numPendingTests;
aggregatedResults.numTodoTests += testResult.numTodoTests;
if (testResult.testExecError) {
aggregatedResults.numRuntimeErrorTestSuites++;
}
if (testResult.skipped) {
aggregatedResults.numPendingTestSuites++;
} else if (testResult.numFailingTests > 0 || testResult.testExecError) {
aggregatedResults.numFailedTestSuites++;
} else {
aggregatedResults.numPassedTestSuites++;
}
// Snapshot data
if (testResult.snapshot.added) {
aggregatedResults.snapshot.filesAdded++;
}
if (testResult.snapshot.fileDeleted) {
aggregatedResults.snapshot.filesRemoved++;
}
if (testResult.snapshot.unmatched) {
aggregatedResults.snapshot.filesUnmatched++;
}
if (testResult.snapshot.updated) {
aggregatedResults.snapshot.filesUpdated++;
}
aggregatedResults.snapshot.added += testResult.snapshot.added;
aggregatedResults.snapshot.matched += testResult.snapshot.matched;
aggregatedResults.snapshot.unchecked += testResult.snapshot.unchecked;
if (testResult.snapshot.uncheckedKeys != null && testResult.snapshot.uncheckedKeys.length > 0) {
aggregatedResults.snapshot.uncheckedKeysByFile.push({
filePath: testResult.testFilePath,
keys: testResult.snapshot.uncheckedKeys
});
}
aggregatedResults.snapshot.unmatched += testResult.snapshot.unmatched;
aggregatedResults.snapshot.updated += testResult.snapshot.updated;
aggregatedResults.snapshot.total += testResult.snapshot.added + testResult.snapshot.matched + testResult.snapshot.unmatched + testResult.snapshot.updated;
};
exports.addResult = addResult;
const createEmptyTestResult = () => ({
leaks: false,
// That's legacy code, just adding it as needed for typing
numFailingTests: 0,
numPassingTests: 0,
numPendingTests: 0,
numTodoTests: 0,
openHandles: [],
perfStats: {
end: 0,
loadTestEnvironmentEnd: 0,
loadTestEnvironmentStart: 0,
runtime: 0,
setupAfterEnvEnd: 0,
setupAfterEnvStart: 0,
setupFilesEnd: 0,
setupFilesStart: 0,
slow: false,
start: 0
},
skipped: false,
snapshot: {
added: 0,
fileDeleted: false,
matched: 0,
unchecked: 0,
uncheckedKeys: [],
unmatched: 0,
updated: 0
},
testFilePath: '',
testResults: []
});
exports.createEmptyTestResult = createEmptyTestResult;
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry needs to be wrapped in an IIFE because it uses a non-standard name for the exports (exports).
(() => {
var exports = __webpack_exports__;
Object.defineProperty(exports, "__esModule", ({
value: true
}));
Object.defineProperty(exports, "addResult", ({
enumerable: true,
get: function () {
return _helpers.addResult;
}
}));
Object.defineProperty(exports, "buildFailureTestResult", ({
enumerable: true,
get: function () {
return _helpers.buildFailureTestResult;
}
}));
Object.defineProperty(exports, "createEmptyTestResult", ({
enumerable: true,
get: function () {
return _helpers.createEmptyTestResult;
}
}));
Object.defineProperty(exports, "formatTestResults", ({
enumerable: true,
get: function () {
return _formatTestResults.default;
}
}));
Object.defineProperty(exports, "makeEmptyAggregatedTestResult", ({
enumerable: true,
get: function () {
return _helpers.makeEmptyAggregatedTestResult;
}
}));
var _formatTestResults = _interopRequireDefault(__webpack_require__("./src/formatTestResults.ts"));
var _helpers = __webpack_require__("./src/helpers.ts");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
})();
module.exports = __webpack_exports__;
/******/ })()
;

View File

@@ -0,0 +1,7 @@
import cjsModule from './index.js';
export const addResult = cjsModule.addResult;
export const buildFailureTestResult = cjsModule.buildFailureTestResult;
export const createEmptyTestResult = cjsModule.createEmptyTestResult;
export const formatTestResults = cjsModule.formatTestResults;
export const makeEmptyAggregatedTestResult = cjsModule.makeEmptyAggregatedTestResult;