 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>
		
			
				
	
	
		
			312 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			312 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| let path = require('path');
 | |
| let fs = require('fs');
 | |
| let Task = require('./task/task').Task;
 | |
| 
 | |
| // Split a task to two parts, name space and task name.
 | |
| // For example, given 'foo:bin/a%.c', return an object with
 | |
| // - 'ns'     : foo
 | |
| // - 'name'   : bin/a%.c
 | |
| function splitNs(task) {
 | |
|   let parts = task.split(':');
 | |
|   let name = parts.pop();
 | |
|   let ns = resolveNs(parts);
 | |
|   return {
 | |
|     'name' : name,
 | |
|     'ns'   : ns
 | |
|   };
 | |
| }
 | |
| 
 | |
| // Return the namespace based on an array of names.
 | |
| // For example, given ['foo', 'baz' ], return the namespace
 | |
| //
 | |
| //   default -> foo -> baz
 | |
| //
 | |
| // where default is the global root namespace
 | |
| // and -> means child namespace.
 | |
| function resolveNs(parts) {
 | |
|   let  ns = jake.defaultNamespace;
 | |
|   for(let i = 0, l = parts.length; ns && i < l; i++) {
 | |
|     ns = ns.childNamespaces[parts[i]];
 | |
|   }
 | |
|   return ns;
 | |
| }
 | |
| 
 | |
| // Given a pattern p, say 'foo:bin/a%.c'
 | |
| // Return an object with
 | |
| // - 'ns'     : foo
 | |
| // - 'dir'    : bin
 | |
| // - 'prefix' : a
 | |
| // - 'suffix' : .c
 | |
| function resolve(p) {
 | |
|   let task = splitNs(p);
 | |
|   let name  = task.name;
 | |
|   let ns    = task.ns;
 | |
|   let split = path.basename(name).split('%');
 | |
|   return {
 | |
|     ns: ns,
 | |
|     dir: path.dirname(name),
 | |
|     prefix: split[0],
 | |
|     suffix: split[1]
 | |
|   };
 | |
| }
 | |
| 
 | |
| // Test whether string a is a suffix of string b
 | |
| function stringEndWith(a, b) {
 | |
|   let l;
 | |
|   return (l = b.lastIndexOf(a)) == -1 ? false : l + a.length == b.length;
 | |
| }
 | |
| 
 | |
| // Replace the suffix a of the string s with b.
 | |
| // Note that, it is assumed a is a suffix of s.
 | |
| function stringReplaceSuffix(s, a, b) {
 | |
|   return s.slice(0, s.lastIndexOf(a)) + b;
 | |
| }
 | |
| 
 | |
| class Rule {
 | |
|   constructor(opts) {
 | |
|     this.pattern = opts.pattern;
 | |
|     this.source = opts.source;
 | |
|     this.prereqs = opts.prereqs;
 | |
|     this.action = opts.action;
 | |
|     this.opts = opts.opts;
 | |
|     this.desc =  opts.desc;
 | |
|     this.ns = opts.ns;
 | |
|   }
 | |
| 
 | |
|   // Create a file task based on this rule for the specified
 | |
|   // task-name
 | |
|   // ======
 | |
|   // FIXME: Right now this just throws away any passed-in args
 | |
|   // for the synthsized task (taskArgs param)
 | |
|   // ======
 | |
|   createTask(fullName, level) {
 | |
|     let self = this;
 | |
|     let pattern;
 | |
|     let source;
 | |
|     let action;
 | |
|     let opts;
 | |
|     let prereqs;
 | |
|     let valid;
 | |
|     let src;
 | |
|     let tNs;
 | |
|     let createdTask;
 | |
|     let name = Task.getBaseTaskName(fullName);
 | |
|     let nsPath = Task.getBaseNamespacePath(fullName);
 | |
|     let ns = this.ns.resolveNamespace(nsPath);
 | |
| 
 | |
|     pattern = this.pattern;
 | |
|     source = this.source;
 | |
| 
 | |
|     if (typeof source == 'string') {
 | |
|       src = Rule.getSource(name, pattern, source);
 | |
|     }
 | |
|     else {
 | |
|       src = source(name);
 | |
|     }
 | |
| 
 | |
|     // TODO: Write a utility function that appends a
 | |
|     // taskname to a namespace path
 | |
|     src = nsPath.split(':').filter(function (item) {
 | |
|       return !!item;
 | |
|     }).concat(src).join(':');
 | |
| 
 | |
|     // Generate the prerequisite for the matching task.
 | |
|     //    It is the original prerequisites plus the prerequisite
 | |
|     //    representing source file, i.e.,
 | |
|     //
 | |
|     //      rule( '%.o', '%.c', ['some.h'] ...
 | |
|     //
 | |
|     //    If the objective is main.o, then new task should be
 | |
|     //
 | |
|     //      file( 'main.o', ['main.c', 'some.h' ] ...
 | |
|     prereqs = this.prereqs.slice(); // Get a copy to work with
 | |
|     prereqs.unshift(src);
 | |
| 
 | |
|     // Prereq should be:
 | |
|     // 1. an existing task
 | |
|     // 2. an existing file on disk
 | |
|     // 3. a valid rule (i.e., not at too deep a level)
 | |
|     valid = prereqs.some(function (p) {
 | |
|       let ns = self.ns;
 | |
|       return ns.resolveTask(p) ||
 | |
|         fs.existsSync(Task.getBaseTaskName(p)) ||
 | |
|         jake.attemptRule(p, ns, level + 1);
 | |
|     });
 | |
| 
 | |
|     // If any of the prereqs aren't valid, the rule isn't valid
 | |
|     if (!valid) {
 | |
|       return null;
 | |
|     }
 | |
|     // Otherwise, hunky-dory, finish creating the task for the rule
 | |
|     else {
 | |
|       // Create the action for the task
 | |
|       action = function () {
 | |
|         let task = this;
 | |
|         self.action.apply(task);
 | |
|       };
 | |
| 
 | |
|       opts = this.opts;
 | |
| 
 | |
|       // Insert the file task into Jake
 | |
|       //
 | |
|       // Since createTask function stores the task as a child task
 | |
|       // of currentNamespace. Here we temporariliy switch the namespace.
 | |
|       // FIXME: Should allow optional ns passed in instead of this hack
 | |
|       tNs = jake.currentNamespace;
 | |
|       jake.currentNamespace = ns;
 | |
|       createdTask = jake.createTask('file', name, prereqs, action, opts);
 | |
|       createdTask.source = src.split(':').pop();
 | |
|       jake.currentNamespace = tNs;
 | |
| 
 | |
|       return createdTask;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   match(name) {
 | |
|     return Rule.match(this.pattern, name);
 | |
|   }
 | |
| 
 | |
|   // Test wether the a prerequisite matchs the pattern.
 | |
|   // The arg 'pattern' does not have namespace as prefix.
 | |
|   // For example, the following tests are true
 | |
|   //
 | |
|   //   pattern      |    name
 | |
|   //   bin/%.o      |    bin/main.o
 | |
|   //   bin/%.o      |    foo:bin/main.o
 | |
|   //
 | |
|   // The following tests are false (trivally)
 | |
|   //
 | |
|   //   pattern      |    name
 | |
|   //   bin/%.o      |    foobin/main.o
 | |
|   //   bin/%.o      |    bin/main.oo
 | |
|   static match(pattern, name) {
 | |
|     let p;
 | |
|     let task;
 | |
|     let obj;
 | |
|     let filename;
 | |
| 
 | |
|     if (pattern instanceof RegExp) {
 | |
|       return pattern.test(name);
 | |
|     }
 | |
|     else if (pattern.indexOf('%') == -1) {
 | |
|       // No Pattern. No Folder. No Namespace.
 | |
|       // A Simple Suffix Rule. Just test suffix
 | |
|       return stringEndWith(pattern, name);
 | |
|     }
 | |
|     else {
 | |
|       // Resolve the dir, prefix and suffix of pattern
 | |
|       p = resolve(pattern);
 | |
| 
 | |
|       // Resolve the namespace and task-name
 | |
|       task = splitNs(name);
 | |
|       name = task.name;
 | |
| 
 | |
|       // Set the objective as the task-name
 | |
|       obj = name;
 | |
| 
 | |
|       // Namespace is already matched.
 | |
| 
 | |
|       // Check dir
 | |
|       if (path.dirname(obj) != p.dir) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       filename = path.basename(obj);
 | |
| 
 | |
|       // Check file name length
 | |
|       if ((p.prefix.length + p.suffix.length + 1) > filename.length) {
 | |
|         // Length does not match.
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       // Check prefix
 | |
|       if (filename.indexOf(p.prefix) !== 0) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       // Check suffix
 | |
|       if (!stringEndWith(p.suffix, filename)) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       // OK. Find a match.
 | |
|       return true;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Generate the source based on
 | |
|   //  - name    name for the synthesized task
 | |
|   //  - pattern    pattern for the objective
 | |
|   //  - source    pattern for the source
 | |
|   //
 | |
|   // Return the source with properties
 | |
|   //  - dep      the prerequisite of source
 | |
|   //             (with the namespace)
 | |
|   //
 | |
|   //  - file     the file name of source
 | |
|   //             (without the namespace)
 | |
|   //
 | |
|   // For example, given
 | |
|   //
 | |
|   //  - name   foo:bin/main.o
 | |
|   //  - pattern    bin/%.o
 | |
|   //  - source    src/%.c
 | |
|   //
 | |
|   //    return 'foo:src/main.c',
 | |
|   //
 | |
|   static getSource(name, pattern, source) {
 | |
|     let dep;
 | |
|     let pat;
 | |
|     let match;
 | |
|     let file;
 | |
|     let src;
 | |
| 
 | |
|     // Regex pattern -- use to look up the extension
 | |
|     if (pattern instanceof RegExp) {
 | |
|       match = pattern.exec(name);
 | |
|       if (match) {
 | |
|         if (typeof source == 'function') {
 | |
|           src = source(name);
 | |
|         }
 | |
|         else {
 | |
|           src = stringReplaceSuffix(name, match[0], source);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     // Assume string
 | |
|     else {
 | |
|       // Simple string suffix replacement
 | |
|       if (pattern.indexOf('%') == -1) {
 | |
|         if (typeof source == 'function') {
 | |
|           src = source(name);
 | |
|         }
 | |
|         else {
 | |
|           src = stringReplaceSuffix(name, pattern, source);
 | |
|         }
 | |
|       }
 | |
|       // Percent-based substitution
 | |
|       else {
 | |
|         pat = pattern.replace('%', '(.*?)');
 | |
|         pat = new RegExp(pat);
 | |
|         match = pat.exec(name);
 | |
|         if (match) {
 | |
|           if (typeof source == 'function') {
 | |
|             src = source(name);
 | |
|           }
 | |
|           else {
 | |
|             file = match[1];
 | |
|             file = source.replace('%', file);
 | |
|             dep = match[0];
 | |
|             src = name.replace(dep, file);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return src;
 | |
|   }
 | |
| }
 | |
| 
 | |
| 
 | |
| exports.Rule = Rule;
 |