 aacb45156b
			
		
	
	aacb45156b
	
	
	
		
			
			- 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>
		
			
				
	
	
		
			299 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			299 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*
 | |
|  Copyright 2012-2015, Yahoo Inc.
 | |
|  Copyrights licensed under the New BSD License. See the accompanying LICENSE
 | |
|  file for terms.
 | |
|  */
 | |
| 'use strict';
 | |
| const { ReportBase } = require('istanbul-lib-report');
 | |
| 
 | |
| const NAME_COL = 4;
 | |
| const PCT_COLS = 7;
 | |
| const MISSING_COL = 17;
 | |
| const TAB_SIZE = 1;
 | |
| const DELIM = ' | ';
 | |
| 
 | |
| function padding(num, ch) {
 | |
|     let str = '';
 | |
|     let i;
 | |
|     ch = ch || ' ';
 | |
|     for (i = 0; i < num; i += 1) {
 | |
|         str += ch;
 | |
|     }
 | |
|     return str;
 | |
| }
 | |
| 
 | |
| function fill(str, width, right, tabs) {
 | |
|     tabs = tabs || 0;
 | |
|     str = String(str);
 | |
| 
 | |
|     const leadingSpaces = tabs * TAB_SIZE;
 | |
|     const remaining = width - leadingSpaces;
 | |
|     const leader = padding(leadingSpaces);
 | |
|     let fmtStr = '';
 | |
| 
 | |
|     if (remaining > 0) {
 | |
|         const strlen = str.length;
 | |
|         let fillStr;
 | |
| 
 | |
|         if (remaining >= strlen) {
 | |
|             fillStr = padding(remaining - strlen);
 | |
|         } else {
 | |
|             fillStr = '...';
 | |
|             const length = remaining - fillStr.length;
 | |
| 
 | |
|             str = str.substring(strlen - length);
 | |
|             right = true;
 | |
|         }
 | |
|         fmtStr = right ? fillStr + str : str + fillStr;
 | |
|     }
 | |
| 
 | |
|     return leader + fmtStr;
 | |
| }
 | |
| 
 | |
| function formatName(name, maxCols, level) {
 | |
|     return fill(name, maxCols, false, level);
 | |
| }
 | |
| 
 | |
| function formatPct(pct, width) {
 | |
|     return fill(pct, width || PCT_COLS, true, 0);
 | |
| }
 | |
| 
 | |
| function nodeMissing(node) {
 | |
|     if (node.isSummary()) {
 | |
|         return '';
 | |
|     }
 | |
| 
 | |
|     const metrics = node.getCoverageSummary();
 | |
|     const isEmpty = metrics.isEmpty();
 | |
|     const lines = isEmpty ? 0 : metrics.lines.pct;
 | |
| 
 | |
|     let coveredLines;
 | |
| 
 | |
|     const fileCoverage = node.getFileCoverage();
 | |
|     if (lines === 100) {
 | |
|         const branches = fileCoverage.getBranchCoverageByLine();
 | |
|         coveredLines = Object.entries(branches).map(([key, { coverage }]) => [
 | |
|             key,
 | |
|             coverage === 100
 | |
|         ]);
 | |
|     } else {
 | |
|         coveredLines = Object.entries(fileCoverage.getLineCoverage());
 | |
|     }
 | |
| 
 | |
|     let newRange = true;
 | |
|     const ranges = coveredLines
 | |
|         .reduce((acum, [line, hit]) => {
 | |
|             if (hit) newRange = true;
 | |
|             else {
 | |
|                 line = parseInt(line);
 | |
|                 if (newRange) {
 | |
|                     acum.push([line]);
 | |
|                     newRange = false;
 | |
|                 } else acum[acum.length - 1][1] = line;
 | |
|             }
 | |
| 
 | |
|             return acum;
 | |
|         }, [])
 | |
|         .map(range => {
 | |
|             const { length } = range;
 | |
| 
 | |
|             if (length === 1) return range[0];
 | |
| 
 | |
|             return `${range[0]}-${range[1]}`;
 | |
|         });
 | |
| 
 | |
|     return [].concat(...ranges).join(',');
 | |
| }
 | |
| 
 | |
| function nodeName(node) {
 | |
|     return node.getRelativeName() || 'All files';
 | |
| }
 | |
| 
 | |
| function depthFor(node) {
 | |
|     let ret = 0;
 | |
|     node = node.getParent();
 | |
|     while (node) {
 | |
|         ret += 1;
 | |
|         node = node.getParent();
 | |
|     }
 | |
|     return ret;
 | |
| }
 | |
| 
 | |
| function nullDepthFor() {
 | |
|     return 0;
 | |
| }
 | |
| 
 | |
| function findWidth(node, context, nodeExtractor, depthFor = nullDepthFor) {
 | |
|     let last = 0;
 | |
|     function compareWidth(node) {
 | |
|         last = Math.max(
 | |
|             last,
 | |
|             TAB_SIZE * depthFor(node) + nodeExtractor(node).length
 | |
|         );
 | |
|     }
 | |
|     const visitor = {
 | |
|         onSummary: compareWidth,
 | |
|         onDetail: compareWidth
 | |
|     };
 | |
|     node.visit(context.getVisitor(visitor));
 | |
|     return last;
 | |
| }
 | |
| 
 | |
| function makeLine(nameWidth, missingWidth) {
 | |
|     const name = padding(nameWidth, '-');
 | |
|     const pct = padding(PCT_COLS, '-');
 | |
|     const elements = [];
 | |
| 
 | |
|     elements.push(name);
 | |
|     elements.push(pct);
 | |
|     elements.push(padding(PCT_COLS + 1, '-'));
 | |
|     elements.push(pct);
 | |
|     elements.push(pct);
 | |
|     elements.push(padding(missingWidth, '-'));
 | |
|     return elements.join(DELIM.replace(/ /g, '-')) + '-';
 | |
| }
 | |
| 
 | |
| function tableHeader(maxNameCols, missingWidth) {
 | |
|     const elements = [];
 | |
|     elements.push(formatName('File', maxNameCols, 0));
 | |
|     elements.push(formatPct('% Stmts'));
 | |
|     elements.push(formatPct('% Branch', PCT_COLS + 1));
 | |
|     elements.push(formatPct('% Funcs'));
 | |
|     elements.push(formatPct('% Lines'));
 | |
|     elements.push(formatName('Uncovered Line #s', missingWidth));
 | |
|     return elements.join(DELIM) + ' ';
 | |
| }
 | |
| 
 | |
| function isFull(metrics) {
 | |
|     return (
 | |
|         metrics.statements.pct === 100 &&
 | |
|         metrics.branches.pct === 100 &&
 | |
|         metrics.functions.pct === 100 &&
 | |
|         metrics.lines.pct === 100
 | |
|     );
 | |
| }
 | |
| 
 | |
| function tableRow(
 | |
|     node,
 | |
|     context,
 | |
|     colorizer,
 | |
|     maxNameCols,
 | |
|     level,
 | |
|     skipEmpty,
 | |
|     skipFull,
 | |
|     missingWidth
 | |
| ) {
 | |
|     const name = nodeName(node);
 | |
|     const metrics = node.getCoverageSummary();
 | |
|     const isEmpty = metrics.isEmpty();
 | |
|     if (skipEmpty && isEmpty) {
 | |
|         return '';
 | |
|     }
 | |
|     if (skipFull && isFull(metrics)) {
 | |
|         return '';
 | |
|     }
 | |
| 
 | |
|     const mm = {
 | |
|         statements: isEmpty ? 0 : metrics.statements.pct,
 | |
|         branches: isEmpty ? 0 : metrics.branches.pct,
 | |
|         functions: isEmpty ? 0 : metrics.functions.pct,
 | |
|         lines: isEmpty ? 0 : metrics.lines.pct
 | |
|     };
 | |
|     const colorize = isEmpty
 | |
|         ? function(str) {
 | |
|               return str;
 | |
|           }
 | |
|         : function(str, key) {
 | |
|               return colorizer(str, context.classForPercent(key, mm[key]));
 | |
|           };
 | |
|     const elements = [];
 | |
| 
 | |
|     elements.push(colorize(formatName(name, maxNameCols, level), 'statements'));
 | |
|     elements.push(colorize(formatPct(mm.statements), 'statements'));
 | |
|     elements.push(colorize(formatPct(mm.branches, PCT_COLS + 1), 'branches'));
 | |
|     elements.push(colorize(formatPct(mm.functions), 'functions'));
 | |
|     elements.push(colorize(formatPct(mm.lines), 'lines'));
 | |
|     elements.push(
 | |
|         colorizer(
 | |
|             formatName(nodeMissing(node), missingWidth),
 | |
|             mm.lines === 100 ? 'medium' : 'low'
 | |
|         )
 | |
|     );
 | |
| 
 | |
|     return elements.join(DELIM) + ' ';
 | |
| }
 | |
| 
 | |
| class TextReport extends ReportBase {
 | |
|     constructor(opts) {
 | |
|         super(opts);
 | |
| 
 | |
|         opts = opts || {};
 | |
|         const { maxCols } = opts;
 | |
| 
 | |
|         this.file = opts.file || null;
 | |
|         this.maxCols = maxCols != null ? maxCols : process.stdout.columns || 80;
 | |
|         this.cw = null;
 | |
|         this.skipEmpty = opts.skipEmpty;
 | |
|         this.skipFull = opts.skipFull;
 | |
|     }
 | |
| 
 | |
|     onStart(root, context) {
 | |
|         this.cw = context.writer.writeFile(this.file);
 | |
|         this.nameWidth = Math.max(
 | |
|             NAME_COL,
 | |
|             findWidth(root, context, nodeName, depthFor)
 | |
|         );
 | |
|         this.missingWidth = Math.max(
 | |
|             MISSING_COL,
 | |
|             findWidth(root, context, nodeMissing)
 | |
|         );
 | |
| 
 | |
|         if (this.maxCols > 0) {
 | |
|             const pct_cols = DELIM.length + 4 * (PCT_COLS + DELIM.length) + 2;
 | |
| 
 | |
|             const maxRemaining = this.maxCols - (pct_cols + MISSING_COL);
 | |
|             if (this.nameWidth > maxRemaining) {
 | |
|                 this.nameWidth = maxRemaining;
 | |
|                 this.missingWidth = MISSING_COL;
 | |
|             } else if (this.nameWidth < maxRemaining) {
 | |
|                 const maxRemaining = this.maxCols - (this.nameWidth + pct_cols);
 | |
|                 if (this.missingWidth > maxRemaining) {
 | |
|                     this.missingWidth = maxRemaining;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         const line = makeLine(this.nameWidth, this.missingWidth);
 | |
|         this.cw.println(line);
 | |
|         this.cw.println(tableHeader(this.nameWidth, this.missingWidth));
 | |
|         this.cw.println(line);
 | |
|     }
 | |
| 
 | |
|     onSummary(node, context) {
 | |
|         const nodeDepth = depthFor(node);
 | |
|         const row = tableRow(
 | |
|             node,
 | |
|             context,
 | |
|             this.cw.colorize.bind(this.cw),
 | |
|             this.nameWidth,
 | |
|             nodeDepth,
 | |
|             this.skipEmpty,
 | |
|             this.skipFull,
 | |
|             this.missingWidth
 | |
|         );
 | |
|         if (row) {
 | |
|             this.cw.println(row);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     onDetail(node, context) {
 | |
|         return this.onSummary(node, context);
 | |
|     }
 | |
| 
 | |
|     onEnd() {
 | |
|         this.cw.println(makeLine(this.nameWidth, this.missingWidth));
 | |
|         this.cw.close();
 | |
|     }
 | |
| }
 | |
| 
 | |
| module.exports = TextReport;
 |