diff --git a/e2e/each.test.ts b/e2e/each.test.ts
index 9fb19441b..47744a716 100644
--- a/e2e/each.test.ts
+++ b/e2e/each.test.ts
@@ -84,10 +84,11 @@ describe('jill each', () => {
expect(res.code).toBe(0);
expect(res.screen.screen).toMatchLines([
- expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
- expect.ignoreColor(/^. Run start in wks-b \(took [0-9.]+m?s\)$/),
- expect.ignoreColor(/^. Run build in wks-b \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^. Run start in wks-a \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. Run build in wks-b \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. Run start in wks-b \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 4 done$/),
]);
// Check script result
@@ -111,13 +112,14 @@ describe('jill each', () => {
expect(res.code).toBe(0);
expect(res.screen.screen).toMatchLines([
- expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
- expect.ignoreColor(/^. In sequence \(took [0-9.]+m?s\)$/),
- expect.ignoreColor(/^ {2}. Run build in wks-b \(took [0-9.]+m?s\)$/),
- expect.ignoreColor(/^ {2}. Run start in wks-b \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^. In sequence \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run build in wks-a \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run start in wks-a \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. In sequence \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^ {2}. Run build in wks-b \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^ {2}. Run start in wks-b \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 5 done$/),
]);
// Check script result
@@ -144,9 +146,10 @@ describe('jill each', () => {
expect(res.code).toBe(0);
expect(res.screen.screen).toMatchLines([
+ expect.ignoreColor(/^. Run hooked in wks-b \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^. Run hooked in wks-c \(took [0-9.]+m?s\)$/),
- expect.ignoreColor(/^. Run hooked in wks-b \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 7 done$/),
]);
// Check script result
@@ -179,9 +182,10 @@ describe('jill each', () => {
expect(res.code).toBe(1);
expect(res.screen.screen).toMatchLines([
- expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^. Run fails in wks-b \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}.( yarn exec)? node -e "process.exit\(1\)" \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 1 done, . 1 failed$/),
]);
// Check script result
diff --git a/e2e/exec.test.ts b/e2e/exec.test.ts
index 61035aa9b..4c24f428f 100644
--- a/e2e/exec.test.ts
+++ b/e2e/exec.test.ts
@@ -57,6 +57,7 @@ describe('jill exec', () => {
expect(res.screen.screen).toMatchLines([
expect.ignoreColor(/^.( yarn exec)? node -e "require\('node:fs'\).+ \(took [0-9.]+m?s\)/),
+ expect.ignoreColor(/^. 1 done$/),
]);
// Check script result
@@ -72,6 +73,7 @@ describe('jill exec', () => {
expect(res.screen.screen).toMatchLines([
expect.ignoreColor(/^.( yarn exec)? echo toto \(took [0-9.]+m?s\)/),
+ expect.ignoreColor(/^. 1 done$/),
]);
expect(res.stderr).toMatchLines([
@@ -98,6 +100,7 @@ describe('jill exec', () => {
expect(res.screen.screen).toMatchLines([
expect.ignoreColor(/^.( yarn exec)? node -e "process.exit\(1\)" \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 1 failed$/),
]);
});
@@ -110,6 +113,7 @@ describe('jill exec', () => {
expect(res.screen.screen).toMatchLines([
expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^.( yarn exec)? node -e "require\('node:fs'\).+ \(took [0-9.]+m?s\)/),
+ expect.ignoreColor(/^. 2 done$/),
]);
// Check scripts result
diff --git a/e2e/group.test.ts b/e2e/group.test.ts
index 814901ea6..8b356dad0 100644
--- a/e2e/group.test.ts
+++ b/e2e/group.test.ts
@@ -77,6 +77,7 @@ describe('jill group', () => {
expect.ignoreColor(/^. In parallel \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run test1 in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run test2 in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 2 done$/),
]);
// Check script result
@@ -95,6 +96,7 @@ describe('jill group', () => {
expect.ignoreColor(/^ {2}. Run test1 in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run fails in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {4}.( yarn exec)? node -e "process.exit\(1\)" \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 1 done, . 1 failed$/),
]);
// Check script result
@@ -109,10 +111,11 @@ describe('jill group', () => {
expect(res.code).toBe(0);
expect(res.screen.screen).toMatchLines([
- expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^. In parallel \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run test1 in wks-b \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run test2 in wks-b \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 3 done$/),
]);
// Check script result
@@ -135,6 +138,7 @@ describe('jill group', () => {
expect.ignoreColor(/^. In sequence \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run test1 in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run test2 in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 2 done$/),
]);
// Check script result
@@ -153,6 +157,7 @@ describe('jill group', () => {
expect.ignoreColor(/^ {2}. Run test1 in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run fails in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {4}.( yarn exec)? node -e "process.exit\(1\)" \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 1 done, . 1 failed$/),
]);
// Check script result
@@ -167,10 +172,11 @@ describe('jill group', () => {
expect(res.code).toBe(0);
expect(res.screen.screen).toMatchLines([
- expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^. In sequence \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run test1 in wks-b \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run test2 in wks-b \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 3 done$/),
]);
// Check script result
@@ -193,6 +199,7 @@ describe('jill group', () => {
expect.ignoreColor(/^. Fallbacks \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run test1 in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run fails in wks-c$/),
+ expect.ignoreColor(/^. 1 done$/),
]);
// Check script result
@@ -211,6 +218,7 @@ describe('jill group', () => {
expect.ignoreColor(/^ {2}. Run fails in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {4}.( yarn exec)? node -e "process.exit\(1\)" \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run test2 in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 1 done, . 1 failed$/),
]);
// Check script result
@@ -230,6 +238,7 @@ describe('jill group', () => {
expect.ignoreColor(/^ {4}.( yarn exec)? node -e "process.exit\(1\)" \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run fails2 in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {4}.( yarn exec)? node -e "process.exit\(1\)" \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 2 failed/),
]);
});
@@ -240,11 +249,12 @@ describe('jill group', () => {
expect(res.code).toBe(0);
expect(res.screen.screen).toMatchLines([
- expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^. Fallbacks \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run fails in wks-b \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {4}.( yarn exec)? node -e "process.exit\(1\)" \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run test2 in wks-b \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 2 done, . 1 failed$/),
]);
// Check script result
diff --git a/e2e/run.test.ts b/e2e/run.test.ts
index e9bd3c4fb..8b9650410 100644
--- a/e2e/run.test.ts
+++ b/e2e/run.test.ts
@@ -72,6 +72,7 @@ describe('jill run', () => {
expect(res.screen.screen).toMatchLines([
expect.ignoreColor(/^. Run start in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 1 done$/),
]);
// Check script result
@@ -89,6 +90,7 @@ describe('jill run', () => {
expect.ignoreColor(/^. In sequence \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run build in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run start in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 2 done$/),
]);
// Check script result
@@ -107,6 +109,7 @@ describe('jill run', () => {
expect(res.screen.screen).toMatchLines([
expect.ignoreColor(/^. Run hooked in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 3 done$/),
]);
// Check script result
@@ -129,6 +132,7 @@ describe('jill run', () => {
expect(res.screen.screen).toMatchLines([
expect.ignoreColor(/^. Run fails in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}.( yarn exec)? node -e "process.exit\(1\)" \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 1 failed$/),
]);
});
@@ -143,6 +147,7 @@ describe('jill run', () => {
expect.ignoreColor(/^ {2}. Run build in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {2}. Run fails in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^ {4}.( yarn exec)? node -e "process.exit\(1\)" \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 1 done, . 1 failed$/),
]);
});
@@ -164,8 +169,9 @@ describe('jill run', () => {
expect(res.code).toBe(0);
expect(res.screen.screen).toMatchLines([
- expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
expect.ignoreColor(/^. Run start in wks-b \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. Run build in wks-c \(took [0-9.]+m?s\)$/),
+ expect.ignoreColor(/^. 2 done$/),
]);
// Check scripts result
diff --git a/src/index.ts b/src/index.ts
index 75c780377..f995e9d8f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -40,18 +40,12 @@ export { CONFIG } from './config/config-loader.ts';
export { type IConfig } from './config/types.ts';
// Ui
-export { default as GroupTaskSpinner } from './ui/group-task-spinner.tsx';
-export * from './ui/group-task-spinner.tsx';
-
export { default as Layout } from './ui/layout.tsx';
export * from './ui/layout.tsx';
export { default as List } from './ui/list.tsx';
export * from './ui/list.tsx';
-export { default as TaskManagerSpinner } from './ui/task-manager-spinner.tsx';
-export * from './ui/task-manager-spinner.tsx';
-
export { default as TaskName } from './ui/task-name.tsx';
export * from './ui/task-name.tsx';
diff --git a/src/modules/task-command.tsx b/src/modules/task-command.tsx
index 64f6bb7f6..9a00eb44c 100644
--- a/src/modules/task-command.tsx
+++ b/src/modules/task-command.tsx
@@ -10,7 +10,8 @@ import { isScriptCtx } from '@/src/tasks/script-task.ts';
import { TASK_MANAGER } from '@/src/tasks/task-manager.config.ts';
import { type AwaitableGenerator } from '@/src/types.ts';
import List from '@/src/ui/list.tsx';
-import TaskManagerSpinner from '@/src/ui/task-manager-spinner.tsx';
+import TaskTreeCompleted from '@/src/ui/task-tree-completed.tsx';
+import TaskTreeSpinner from '@/src/ui/task-tree-spinner.tsx';
import { ExitException } from '@/src/utils/exit.ts';
import { printJson } from '@/src/utils/json.ts';
@@ -73,13 +74,16 @@ export abstract class TaskCommand extends InkCommand {
}
} else if (tasks.tasks.length > 0) {
// Render
- yield ;
+ yield ;
// Start tasks
tasks.start(this.manager);
const result = await waitFor$(tasks, 'finished');
+ this.app.clear();
+ yield ;
+
if (result.failed > 0) {
throw new ExitException(1);
}
diff --git a/src/tasks/task-expression.service.ts b/src/tasks/task-expression.service.ts
index 1e4c54c6c..a731a9872 100644
--- a/src/tasks/task-expression.service.ts
+++ b/src/tasks/task-expression.service.ts
@@ -214,11 +214,11 @@ export class TaskExpressionService {
let group: GroupTask;
if (node.operator === '//') {
- group = new ParallelGroup('In parallel', {}, {
+ group = new ParallelGroup('In parallel', { workspace }, {
logger: this._logger,
});
} else if (node.operator === '||') {
- group = new FallbackGroup('Fallbacks', {}, {
+ group = new FallbackGroup('Fallbacks', { workspace }, {
logger: this._logger,
});
} else {
@@ -227,7 +227,7 @@ export class TaskExpressionService {
TaskExpressionService._sequenceOperatorWarn = true;
}
- group = new SequenceGroup('In sequence', {}, {
+ group = new SequenceGroup('In sequence', { workspace }, {
logger: this._logger,
});
}
diff --git a/src/ui/group-task-spinner.tsx b/src/ui/group-task-spinner.tsx
deleted file mode 100644
index 81630f22c..000000000
--- a/src/ui/group-task-spinner.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { GroupTask } from '@jujulego/tasks';
-import { Box } from 'ink';
-import { Fragment, useLayoutEffect, useMemo, useState } from 'react';
-
-import { CONFIG } from '@/src/config/config-loader.ts';
-import { container } from '@/src/inversify.config.ts';
-import { isCommandCtx } from '@/src/tasks/command-task.ts';
-
-import TaskSpinner from './task-spinner.tsx';
-
-// Types
-export interface GroupTaskSpinnerProps {
- group: GroupTask;
-}
-
-// Components
-export default function GroupTaskSpinner({ group }: GroupTaskSpinnerProps) {
- // State
- const [verbose, setVerbose] = useState(false);
- const [status, setStatus] = useState(group.status);
- const [tasks, setTasks] = useState([...group.tasks]);
- const [canReduce, setCanReduce] = useState(true);
-
- // Memo
- const forceExtended = useMemo(() => verbose || tasks.some((tsk) => !isCommandCtx(tsk.context)), [verbose, tasks]);
- const isReduced = useMemo(() => !forceExtended && canReduce, [forceExtended, canReduce]);
-
- // Effects
- useLayoutEffect(() => {
- const config = container.get(CONFIG);
-
- if (config.verbose) {
- setVerbose(['verbose', 'debug'].includes(config.verbose));
- }
- }, []);
-
- useLayoutEffect(() => {
- return group.on('status', (event) => {
- setStatus(event.status);
- });
- }, [group]);
-
- useLayoutEffect(() => {
- let dirty = false;
-
- return group.on('task.added', () => {
- if (!dirty) {
- dirty = true;
-
- queueMicrotask(() => {
- setTasks([...group.tasks]);
- dirty = false;
- });
- }
- });
- }, [group]);
-
- useLayoutEffect(() => {
- if (status === 'running') {
- setCanReduce(false);
- } else if (status === 'done') {
- setCanReduce(true);
- }
- }, [status]);
-
- // Render
- return (
- <>
-
- { isReduced || (
-
- { tasks.map((task) => (
-
- { (task instanceof GroupTask) ? (
-
- ) : (
-
- ) }
-
- )) }
-
- ) }
- >
- );
-}
diff --git a/src/ui/hooks/useFlatTaskTree.ts b/src/ui/hooks/useFlatTaskTree.ts
new file mode 100644
index 000000000..9b6bf8e06
--- /dev/null
+++ b/src/ui/hooks/useFlatTaskTree.ts
@@ -0,0 +1,128 @@
+import { GroupTask, Task, TaskManager } from '@jujulego/tasks';
+import { useLayoutEffect, useMemo, useState } from 'react';
+
+import { Workspace } from '@/src/project/workspace.ts';
+import { CommandTask, isCommandCtx } from '@/src/tasks/command-task.ts';
+import { ScriptTask } from '@/src/tasks/script-task.ts';
+import { useIsVerbose } from '@/src/ui/hooks/useIsVerbose.ts';
+
+// Types
+export interface FlatTask {
+ task: Task;
+ level: number;
+}
+
+// Utils
+/**
+ * Sorts tasks according to workspace and script, keeping command at the end
+ */
+export function taskComparator(a: Task, b: Task) {
+ // 1 - compare kind
+ const kindA = a instanceof CommandTask ? 0 : 1;
+ const kindB = b instanceof CommandTask ? 0 : 1;
+
+ if (kindA !== kindB) {
+ return kindB - kindA;
+ }
+
+ // 2 - compare workspaces
+ const wksA = 'workspace' in a.context ? (a.context.workspace as Workspace).name : '\uffff';
+ const wksB = 'workspace' in b.context ? (b.context.workspace as Workspace).name : '\uffff';
+ const wksDiff = wksA.localeCompare(wksB);
+
+ if (wksDiff !== 0) {
+ return wksDiff;
+ }
+
+ // 1 - compare scripts
+ const scriptA = 'script' in a.context ? a.context.script as string : '\uffff';
+ const scriptB = 'script' in b.context ? b.context.script as string : '\uffff';
+
+ return scriptA.localeCompare(scriptB);
+}
+
+/**
+ * Extract tasks to be printed, with their level in the tree
+ */
+export function * flatTasks(tasks: readonly Task[], isVerbose: boolean, groupId?: string, level = 0): Generator {
+ for (const task of tasks) {
+ if (task.group?.id !== groupId) {
+ continue;
+ }
+
+ yield { task, level };
+
+ if (task instanceof GroupTask) {
+ const isCommandGroup = task.tasks.some((t) => !isCommandCtx(t.context));
+ const hasFailed = task.tasks.some((t) => t.status === 'failed');
+ const isStarted = task.status === 'running';
+
+ if (isVerbose || isCommandGroup || hasFailed || isStarted) {
+ let tasks = task.tasks;
+
+ if (task instanceof ScriptTask) {
+ tasks = [...tasks].sort(taskComparator);
+ }
+
+ yield* flatTasks(tasks, isVerbose, task.id, level + 1);
+ }
+ }
+ }
+}
+
+// Hook
+export function useFlatTaskTree(manager: TaskManager): FlatTask[] {
+ const isVerbose = useIsVerbose();
+
+ const [tasks, setTasks] = useState([...manager.tasks].sort(taskComparator));
+ const [version, setVersion] = useState(0);
+
+ useLayoutEffect(() => {
+ let dirty = false;
+
+ return manager.on('added', () => {
+ if (!dirty) {
+ dirty = true;
+
+ queueMicrotask(() => {
+ setTasks([...manager.tasks].sort(taskComparator));
+ dirty = false;
+ });
+ }
+ });
+ }, [manager]);
+
+ useLayoutEffect(() => {
+ let dirty = false;
+
+ return manager.on('started', () => {
+ if (!dirty) {
+ dirty = true;
+
+ setTimeout(() => {
+ setVersion((old) => ++old);
+ dirty = false;
+ });
+ }
+ });
+ }, [manager]);
+
+ useLayoutEffect(() => {
+ let dirty = false;
+
+ return manager.on('completed', () => {
+ if (!dirty) {
+ dirty = true;
+
+ setTimeout(() => {
+ setVersion((old) => ++old);
+ dirty = false;
+ });
+ }
+ });
+ }, [manager]);
+
+ return useMemo(() => {
+ return Array.from(flatTasks(tasks, isVerbose));
+ }, [tasks, isVerbose, version]);
+}
diff --git a/src/ui/hooks/useIsVerbose.ts b/src/ui/hooks/useIsVerbose.ts
new file mode 100644
index 000000000..f595e3111
--- /dev/null
+++ b/src/ui/hooks/useIsVerbose.ts
@@ -0,0 +1,16 @@
+import { useMemo } from 'react';
+
+import { CONFIG } from '@/src/config/config-loader.ts';
+import { container } from '@/src/inversify.config.ts';
+
+export function useIsVerbose() {
+ return useMemo(() => {
+ const config = container.get(CONFIG);
+
+ if (config.verbose) {
+ return ['verbose', 'debug'].includes(config.verbose);
+ } else {
+ return false;
+ }
+ }, []);
+}
diff --git a/src/ui/hooks/useStdoutDimensions.ts b/src/ui/hooks/useStdoutDimensions.ts
new file mode 100644
index 000000000..c59a65503
--- /dev/null
+++ b/src/ui/hooks/useStdoutDimensions.ts
@@ -0,0 +1,24 @@
+import { useStdout } from 'ink';
+import { useEffect, useState } from 'react';
+
+export function useStdoutDimensions() {
+ const { stdout } = useStdout();
+ const [dimensions, setDimensions] = useState({
+ columns: stdout.columns ?? Infinity,
+ rows: stdout.rows ?? Infinity,
+ });
+
+ useEffect(() => {
+ const handler = () => setDimensions({
+ columns: stdout.columns ?? Infinity,
+ rows: stdout.rows ?? Infinity,
+ });
+ stdout.on('resize', handler);
+
+ return () => {
+ stdout.off('resize', handler);
+ };
+ }, [stdout]);
+
+ return dimensions;
+}
diff --git a/src/ui/task-manager-spinner.tsx b/src/ui/task-manager-spinner.tsx
deleted file mode 100644
index 7581d4e3c..000000000
--- a/src/ui/task-manager-spinner.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { GroupTask, Task, type TaskManager } from '@jujulego/tasks';
-import { useLayoutEffect, useState } from 'react';
-
-import GroupTaskSpinner from './group-task-spinner.tsx';
-import TaskSpinner from './task-spinner.tsx';
-
-// Types
-export interface TasksSpinnerProps {
- manager: TaskManager;
-}
-
-// Utils
-function taskPredicate(task: Task): boolean {
- if (task.group) {
- return false;
- }
-
- if ('hidden' in task.context && task.context.hidden) {
- return false;
- }
-
- return true;
-}
-
-// Components
-export default function TaskManagerSpinner({ manager }: TasksSpinnerProps) {
- const [tasks, setTasks] = useState(manager.tasks.filter(taskPredicate));
-
- useLayoutEffect(() => {
- let dirty = false;
-
- return manager.on('added', () => {
- if (!dirty) {
- dirty = true;
-
- queueMicrotask(() => {
- setTasks(manager.tasks.filter(taskPredicate));
- dirty = false;
- });
- }
- });
- }, [manager]);
-
- return (
- <>
- { tasks.map((task) =>
- (task instanceof GroupTask) ? (
-
- ) : (
-
- )
- ) }
- >
- );
-}
diff --git a/src/ui/task-spinner.tsx b/src/ui/task-spinner.tsx
index 3b1fc26f7..f7e062ccd 100644
--- a/src/ui/task-spinner.tsx
+++ b/src/ui/task-spinner.tsx
@@ -19,7 +19,7 @@ export interface TaskSpinnerProps {
export default function TaskSpinner({ task }: TaskSpinnerProps) {
// State
const [status, setStatus] = useState(task.status);
- const [time, setTime] = useState(0);
+ const [time, setTime] = useState(task.duration);
// Effects
useLayoutEffect(() => {
diff --git a/src/ui/task-tree-completed.tsx b/src/ui/task-tree-completed.tsx
new file mode 100644
index 000000000..fab594246
--- /dev/null
+++ b/src/ui/task-tree-completed.tsx
@@ -0,0 +1,38 @@
+import { TaskManager } from '@jujulego/tasks';
+import { Box, Static } from 'ink';
+import { useMemo } from 'react';
+
+import { flatTasks, taskComparator } from '@/src/ui/hooks/useFlatTaskTree.ts';
+import { useIsVerbose } from '@/src/ui/hooks/useIsVerbose.ts';
+import TaskSpinner from '@/src/ui/task-spinner.tsx';
+import TaskTreeStats from '@/src/ui/task-tree-stats.tsx';
+
+// Types
+export interface TaskTreeCompletedProps {
+ readonly manager: TaskManager;
+}
+
+// Component
+export default function TaskTreeCompleted({ manager }: TaskTreeCompletedProps) {
+ // Config
+ const isVerbose = useIsVerbose();
+
+ // Extract all tasks
+ const flat = useMemo(() => {
+ return Array.from(flatTasks([...manager.tasks].sort(taskComparator), isVerbose));
+ }, [manager, isVerbose]);
+
+ // Render
+ return (
+ <>
+
+ { ({ task, level }) => (
+
+
+
+ ) }
+
+
+ >
+ );
+}
diff --git a/src/ui/task-tree-full-spinner.tsx b/src/ui/task-tree-full-spinner.tsx
new file mode 100644
index 000000000..67ae26c1e
--- /dev/null
+++ b/src/ui/task-tree-full-spinner.tsx
@@ -0,0 +1,32 @@
+import { TaskManager } from '@jujulego/tasks';
+import { Box, Text } from 'ink';
+
+import { useFlatTaskTree } from '@/src/ui/hooks/useFlatTaskTree.ts';
+import TaskSpinner from '@/src/ui/task-spinner.tsx';
+import TaskTreeStats from '@/src/ui/task-tree-stats.tsx';
+
+// Types
+export interface TaskTreeFullSpinnerProps {
+ readonly manager: TaskManager;
+}
+
+// Component
+export default function TaskTreeFullSpinner({ manager }: TaskTreeFullSpinnerProps) {
+ const flat = useFlatTaskTree(manager);
+
+ // Render
+ return (
+ <>
+
+ { flat.map(({ task, level }) => (
+
+
+
+ )) }
+
+
+
+
+ >
+ );
+}
diff --git a/src/ui/task-tree-scrollable-spinner.tsx b/src/ui/task-tree-scrollable-spinner.tsx
new file mode 100644
index 000000000..f7a9a3cc8
--- /dev/null
+++ b/src/ui/task-tree-scrollable-spinner.tsx
@@ -0,0 +1,65 @@
+import { TaskManager } from '@jujulego/tasks';
+import { Box, Text, useInput } from 'ink';
+import { useEffect, useMemo, useState } from 'react';
+
+import { useStdoutDimensions } from '@/src/ui/hooks/useStdoutDimensions.ts';
+import { useFlatTaskTree } from '@/src/ui/hooks/useFlatTaskTree.ts';
+import TaskSpinner from '@/src/ui/task-spinner.tsx';
+import TaskTreeStats from '@/src/ui/task-tree-stats.tsx';
+
+// Types
+export interface TaskTreeScrollableSpinnerProps {
+ readonly manager: TaskManager;
+}
+
+// Component
+export default function TaskTreeScrollableSpinner({ manager }: TaskTreeScrollableSpinnerProps) {
+ const { rows: termHeight } = useStdoutDimensions();
+
+ // Extract all tasks
+ const flat = useFlatTaskTree(manager);
+
+ const maxHeight = useMemo(
+ () => Math.min(termHeight - 4, flat.length),
+ [termHeight, flat]
+ );
+
+ // Manage scroll
+ const [start, setStart] = useState(0);
+
+ const slice = useMemo(
+ () => flat.slice(start, start + maxHeight),
+ [flat, start, maxHeight]
+ );
+
+ useEffect(() => {
+ if (start + maxHeight > flat.length) {
+ setStart(Math.max(flat.length - maxHeight, 0));
+ }
+ }, [start, flat, maxHeight]);
+
+ useInput((_, key) => {
+ if (key.upArrow) {
+ setStart((old) => Math.max(0, old - 1));
+ } else if (key.downArrow) {
+ setStart((old) => Math.min(flat.length - maxHeight, old + 1));
+ }
+ });
+
+ // Render
+ return (
+ <>
+
+ { slice.map(({ task, level }) => (
+
+
+
+ )) }
+
+
+
+ { (maxHeight < flat.length) && ( - use keyboard arrows to scroll) }
+
+ >
+ );
+}
diff --git a/src/ui/task-tree-spinner.tsx b/src/ui/task-tree-spinner.tsx
new file mode 100644
index 000000000..b6f0ab260
--- /dev/null
+++ b/src/ui/task-tree-spinner.tsx
@@ -0,0 +1,21 @@
+import { TaskManager } from '@jujulego/tasks';
+import { useStdin } from 'ink';
+
+import TaskTreeScrollableSpinner from '@/src/ui/task-tree-scrollable-spinner.tsx';
+import TaskTreeFullSpinner from '@/src/ui/task-tree-full-spinner.tsx';
+
+// Types
+export interface TaskTreeSpinnerProps {
+ readonly manager: TaskManager;
+}
+
+// Component
+export default function TaskTreeSpinner({ manager }: TaskTreeSpinnerProps) {
+ const stdin = useStdin();
+
+ if (stdin.isRawModeSupported) {
+ return ;
+ } else {
+ return ;
+ }
+}
diff --git a/src/ui/task-tree-stats.tsx b/src/ui/task-tree-stats.tsx
new file mode 100644
index 000000000..1cfd6141f
--- /dev/null
+++ b/src/ui/task-tree-stats.tsx
@@ -0,0 +1,69 @@
+import { TaskManager } from '@jujulego/tasks';
+import { Text } from 'ink';
+import Spinner from 'ink-spinner';
+import symbols from 'log-symbols';
+import { useLayoutEffect, useState } from 'react';
+
+// Types
+export interface TaskTreeStatsProps {
+ readonly manager: TaskManager;
+}
+
+// Component
+export default function TaskTreeStats({ manager }: TaskTreeStatsProps) {
+ // Follow stats
+ const [stats, setStats] = useState(() => {
+ const base = { running: 0, done: 0, failed: 0 };
+
+ for (const task of manager.tasks) {
+ switch (task.status) {
+ case 'starting':
+ case 'running':
+ base.running += task.weight;
+ break;
+
+ case 'done':
+ base.done += task.weight;
+ break;
+
+ case 'failed':
+ base.failed += task.weight;
+ break;
+ }
+ }
+
+ return base;
+ });
+
+ useLayoutEffect(() => manager.on('started', (task) => {
+ setStats((old) => ({
+ ...old,
+ running: old.running + task.weight
+ }));
+ }), [manager]);
+
+ useLayoutEffect(() => manager.on('completed', (task) => {
+ setStats((old) => ({
+ running: old.running - task.weight,
+ done: task.status === 'done' ? old.done + task.weight : old.done,
+ failed: task.status === 'failed' ? old.failed + task.weight : old.failed,
+ }));
+ }), [manager]);
+
+ // Render
+ return (
+
+ { (stats.running !== 0) && (
+ <> { stats.running } running>
+ ) }
+ { (stats.running !== 0 && stats.done !== 0) && (<>, >) }
+ { (stats.done !== 0) && (
+ { symbols.success } { stats.done } done
+ ) }
+ { (stats.running + stats.done !== 0 && stats.failed !== 0) && (<>, >) }
+ { (stats.failed !== 0) && (
+ { symbols.error } { stats.failed } failed
+ ) }
+
+ );
+}
diff --git a/tests/commands/each.test.tsx b/tests/commands/each.test.tsx
index 0b9220e0c..6cc428d2f 100644
--- a/tests/commands/each.test.tsx
+++ b/tests/commands/each.test.tsx
@@ -1,5 +1,4 @@
import { Logger } from '@jujulego/logger';
-import { type TaskManager } from '@jujulego/tasks';
import { cleanup, render } from 'ink-testing-library';
import symbols from 'log-symbols';
import { vi } from 'vitest';
@@ -12,7 +11,7 @@ import { TASK_MANAGER } from '@/src/tasks/task-manager.config.js';
import Layout from '@/src/ui/layout.js';
import { TestBed } from '@/tools/test-bed.js';
-import { TestScriptTask } from '@/tools/test-tasks.js';
+import { TestScriptTask, TestTaskManager } from '@/tools/test-tasks.js';
import { flushPromises, spyLogger, wrapInkTestApp } from '@/tools/utils.js';
import { ExitException } from '@/src/utils/exit.js';
import { ContextService } from '@/src/commons/context.service.js';
@@ -20,7 +19,7 @@ import { ContextService } from '@/src/commons/context.service.js';
// Setup
let app: ReturnType;
let command: CommandModule;
-let manager: TaskManager;
+let manager: TestTaskManager;
let context: ContextService;
let logger: Logger;
@@ -39,13 +38,17 @@ beforeEach(async () => {
// Project
bed = new TestBed();
+ logger = container.get(Logger);
+ context = container.get(ContextService);
+
+ manager = new TestTaskManager({ logger });
+ container.rebind(TASK_MANAGER).toConstantValue(manager);
+
app = render();
+ Object.assign(app.stdin, { ref: () => this, unref: () => this });
container.rebind(INK_APP).toConstantValue(wrapInkTestApp(app));
command = await bed.prepareCommand(EachCommand);
- manager = container.get(TASK_MANAGER);
- context = container.get(ContextService);
- logger = container.get(Logger);
// Mocks
vi.restoreAllMocks();
@@ -71,8 +74,8 @@ describe('jill each', () => {
// Setup tasks
const tasks = [
- new TestScriptTask(workspaces[0], 'cmd', [], { logger: spyLogger }),
- new TestScriptTask(workspaces[1], 'cmd', [], { logger: spyLogger }),
+ new TestScriptTask(workspaces[0], 'cmd', [], { logger: spyLogger, weight: 1 }),
+ new TestScriptTask(workspaces[1], 'cmd', [], { logger: spyLogger, weight: 1 }),
];
vi.spyOn(manager, 'tasks', 'get').mockReturnValue(tasks);
@@ -97,12 +100,14 @@ describe('jill each', () => {
// should print task spinners
expect(app.lastFrame()).toMatchLines([
expect.ignoreColor(/^. Run cmd in wks-1$/),
- expect.ignoreColor(/^. Run cmd in wks-2$/),
+ expect.ignoreColor(/^. Run cmd in wks-2$/)
]);
// complete tasks
for (const task of tasks) {
vi.spyOn(task, 'status', 'get').mockReturnValue('done');
+ vi.spyOn(task, 'duration', 'get').mockReturnValue(100);
+
task.emit('status.done', { status: 'done', previous: 'running' });
task.emit('completed', { status: 'done', duration: 100 });
}
@@ -113,6 +118,7 @@ describe('jill each', () => {
expect(app.lastFrame()).toEqualLines([
expect.ignoreColor(`${symbols.success} Run cmd in wks-1 (took 100ms)`),
expect.ignoreColor(`${symbols.success} Run cmd in wks-2 (took 100ms)`),
+ expect.ignoreColor(`${symbols.success} 2 done`),
]);
});
diff --git a/tests/commands/exec.test.tsx b/tests/commands/exec.test.tsx
index 568072e38..f8cc0cbce 100644
--- a/tests/commands/exec.test.tsx
+++ b/tests/commands/exec.test.tsx
@@ -1,4 +1,4 @@
-import { type TaskManager } from '@jujulego/tasks';
+import { Logger } from '@jujulego/logger';
import { cleanup, render } from 'ink-testing-library';
import symbols from 'log-symbols';
import yargs, { type CommandModule } from 'yargs';
@@ -13,14 +13,14 @@ import { TASK_MANAGER } from '@/src/tasks/task-manager.config.js';
import Layout from '@/src/ui/layout.js';
import { TestBed } from '@/tools/test-bed.js';
-import { TestCommandTask } from '@/tools/test-tasks.js';
+import { TestCommandTask, TestTaskManager } from '@/tools/test-tasks.js';
import { flushPromises, spyLogger, wrapInkTestApp } from '@/tools/utils.js';
// Setup
let app: ReturnType;
let command: CommandModule;
let context: ContextService;
-let manager: TaskManager;
+let manager: TestTaskManager;
let bed: TestBed;
let wks: Workspace;
@@ -36,14 +36,19 @@ beforeEach(async () => {
bed = new TestBed();
wks = bed.addWorkspace('wks');
- task = new TestCommandTask(wks, 'cmd', [], { logger: spyLogger });
+ task = new TestCommandTask(wks, 'cmd', [], { logger: spyLogger, weight: 1 });
+
+ const logger = container.get(Logger);
+ context = container.get(ContextService);
+
+ manager = new TestTaskManager({ logger });
+ container.rebind(TASK_MANAGER).toConstantValue(manager);
app = render();
+ Object.assign(app.stdin, { ref: () => this, unref: () => this });
container.rebind(INK_APP).toConstantValue(wrapInkTestApp(app));
command = await bed.prepareCommand(ExecCommand, wks);
- context = container.get(ContextService);
- manager = container.get(TASK_MANAGER);
// Mocks
vi.restoreAllMocks();
@@ -79,13 +84,18 @@ describe('jill exec', () => {
// complete task
vi.spyOn(task, 'status', 'get').mockReturnValue('done');
+ vi.spyOn(task, 'duration', 'get').mockReturnValue(100);
+
task.emit('status.done', { status: 'done', previous: 'running' });
task.emit('completed', { status: 'done', duration: 100 });
await prom;
// should print task completed
- expect(app.lastFrame()).toEqual(expect.ignoreColor(`${symbols.success} cmd (took 100ms)`));
+ expect(app.lastFrame()).toEqualLines([
+ expect.ignoreColor(`${symbols.success} cmd (took 100ms)`),
+ expect.ignoreColor(`${symbols.success} 1 done`),
+ ]);
});
it('should use given dependency selection mode', async () => {
diff --git a/tests/commands/group.test.tsx b/tests/commands/group.test.tsx
index d01614d4e..8ab67d3fb 100644
--- a/tests/commands/group.test.tsx
+++ b/tests/commands/group.test.tsx
@@ -1,4 +1,4 @@
-import { type TaskManager } from '@jujulego/tasks';
+import { Logger } from '@jujulego/logger';
import { cleanup, render } from 'ink-testing-library';
import symbols from 'log-symbols';
import yargs, { type CommandModule } from 'yargs';
@@ -14,14 +14,14 @@ import { TASK_MANAGER } from '@/src/tasks/task-manager.config.js';
import Layout from '@/src/ui/layout.js';
import { TestBed } from '@/tools/test-bed.js';
-import { TestParallelGroup, TestScriptTask } from '@/tools/test-tasks.js';
+import { TestParallelGroup, TestScriptTask, TestTaskManager } from '@/tools/test-tasks.js';
import { flushPromises, spyLogger, wrapInkTestApp } from '@/tools/utils.js';
// Setup
let app: ReturnType;
let command: CommandModule;
let context: ContextService;
-let manager: TaskManager;
+let manager: TestTaskManager;
let taskExpr: TaskExpressionService;
let bed: TestBed;
@@ -40,18 +40,22 @@ beforeEach(async () => {
wks = bed.addWorkspace('wks');
task = new TestParallelGroup('Test group', {}, { logger: spyLogger });
- task.add(new TestScriptTask(wks, 'test1', [], { logger: spyLogger }));
- task.add(new TestScriptTask(wks, 'test2', [], { logger: spyLogger }));
+ task.add(new TestScriptTask(wks, 'test1', [], { logger: spyLogger, weight: 1 }));
+ task.add(new TestScriptTask(wks, 'test2', [], { logger: spyLogger, weight: 1 }));
+
+ const logger = container.get(Logger);
+ context = container.get(ContextService);
+ taskExpr = container.get(TaskExpressionService);
+
+ manager = new TestTaskManager({ logger });
+ container.rebind(TASK_MANAGER).toConstantValue(manager);
app = render();
+ Object.assign(app.stdin, { ref: () => this, unref: () => this });
container.rebind(INK_APP).toConstantValue(wrapInkTestApp(app));
command = await bed.prepareCommand(GroupCommand, wks);
- context = container.get(ContextService);
- manager = container.get(TASK_MANAGER);
- taskExpr = container.get(TaskExpressionService);
-
// Mocks
vi.restoreAllMocks();
@@ -98,14 +102,20 @@ describe('jill group', () => {
for (const child of task.tasks as TestScriptTask[]) {
vi.spyOn(child, 'status', 'get').mockReturnValue('done');
+ vi.spyOn(child, 'duration', 'get').mockReturnValue(100);
child.emit('status.done', { status: 'done', previous: 'running' });
child.emit('completed', { status: 'done', duration: 100 });
}
+ vi.spyOn(task, 'status', 'get').mockReturnValue('done');
+ vi.spyOn(task, 'duration', 'get').mockReturnValue(100);
+
task.emit('status.done', { status: 'done', previous: 'running' });
task.emit('completed', { status: 'done', duration: 100 });
+ vi.spyOn(manager, 'tasks', 'get').mockReturnValue([task, ...task.tasks]);
+
await prom;
// Should print all tasks
@@ -113,6 +123,7 @@ describe('jill group', () => {
expect.ignoreColor(`${symbols.success} Test group (took 100ms)`),
expect.ignoreColor(` ${symbols.success} Run test1 in wks (took 100ms)`),
expect.ignoreColor(` ${symbols.success} Run test2 in wks (took 100ms)`),
+ expect.ignoreColor(`${symbols.success} 2 done`),
]);
});
diff --git a/tests/commands/run.test.tsx b/tests/commands/run.test.tsx
index 1019d7c09..bde05ef3d 100644
--- a/tests/commands/run.test.tsx
+++ b/tests/commands/run.test.tsx
@@ -1,4 +1,4 @@
-import { type TaskManager } from '@jujulego/tasks';
+import { Logger } from '@jujulego/logger';
import { cleanup, render } from 'ink-testing-library';
import symbols from 'log-symbols';
import yargs, { type CommandModule } from 'yargs';
@@ -14,14 +14,14 @@ import Layout from '@/src/ui/layout.js';
import { ExitException } from '@/src/utils/exit.js';
import { TestBed } from '@/tools/test-bed.js';
-import { TestScriptTask } from '@/tools/test-tasks.js';
+import { TestScriptTask, TestTaskManager } from '@/tools/test-tasks.js';
import { flushPromises, spyLogger, wrapInkTestApp } from '@/tools/utils.js';
// Setup
let app: ReturnType;
let command: CommandModule;
let context: ContextService;
-let manager: TaskManager;
+let manager: TestTaskManager;
let bed: TestBed;
let wks: Workspace;
@@ -37,14 +37,19 @@ beforeEach(async () => {
bed = new TestBed();
wks = bed.addWorkspace('wks');
- task = new TestScriptTask(wks, 'cmd', [], { logger: spyLogger });
+ task = new TestScriptTask(wks, 'cmd', [], { logger: spyLogger, weight: 1 });
+
+ const logger = container.get(Logger);
+ context = container.get(ContextService);
+
+ manager = new TestTaskManager({ logger });
+ container.rebind(TASK_MANAGER).toConstantValue(manager);
app = render();
+ Object.assign(app.stdin, { ref: () => this, unref: () => this });
container.rebind(INK_APP).toConstantValue(wrapInkTestApp(app));
command = await bed.prepareCommand(RunCommand, wks);
- context = container.get(ContextService);
- manager = container.get(TASK_MANAGER);
// Mocks
vi.restoreAllMocks();
@@ -80,13 +85,18 @@ describe('jill run', () => {
// complete task
vi.spyOn(task, 'status', 'get').mockReturnValue('done');
+ vi.spyOn(task, 'duration', 'get').mockReturnValue(100);
+
task.emit('status.done', { status: 'done', previous: 'running' });
task.emit('completed', { status: 'done', duration: 100 });
await prom;
// should print task completed
- expect(app.lastFrame()).toEqual(expect.ignoreColor(`${symbols.success} Run cmd in wks (took 100ms)`));
+ expect(app.lastFrame()).toEqualLines([
+ expect.ignoreColor(`${symbols.success} Run cmd in wks (took 100ms)`),
+ expect.ignoreColor(`${symbols.success} 1 done`),
+ ]);
});
it('should use given dependency selection mode', async () => {
diff --git a/tests/modules/task-command.test.tsx b/tests/modules/task-command.test.tsx
index 3a3d522c4..003e197ec 100644
--- a/tests/modules/task-command.test.tsx
+++ b/tests/modules/task-command.test.tsx
@@ -44,9 +44,10 @@ beforeEach(async () => {
bed = new TestBed();
wks = bed.addWorkspace('wks');
- task = new TestScriptTask(wks, 'cmd', [], { logger: spyLogger });
+ task = new TestScriptTask(wks, 'cmd', [], { logger: spyLogger, weight: 1 });
app = render();
+ Object.assign(app.stdin, { ref: () => this, unref: () => this });
container.rebind(INK_APP).toConstantValue(wrapInkTestApp(app));
command = container.resolve(TestTaskCommand);
@@ -84,13 +85,18 @@ describe('TaskCommand', () => {
// complete task
vi.spyOn(task, 'status', 'get').mockReturnValue('done');
+ vi.spyOn(task, 'duration', 'get').mockReturnValue(100);
+
task.emit('status.done', { status: 'done', previous: 'running' });
task.emit('completed', { status: 'done', duration: 100 });
await prom;
// should print task completed
- expect(app.lastFrame()).toEqual(expect.ignoreColor(`${symbols.success} Run cmd in wks (took 100ms)`));
+ expect(app.lastFrame()).toEqualLines([
+ expect.ignoreColor(`${symbols.success} Run cmd in wks (took 100ms)`),
+ expect.ignoreColor(`${symbols.success} 1 done`),
+ ]);
});
it('should exit 1 if a task fails', async () => {
@@ -100,13 +106,18 @@ describe('TaskCommand', () => {
// complete task
vi.spyOn(task, 'status', 'get').mockReturnValue('failed');
+ vi.spyOn(task, 'duration', 'get').mockReturnValue(100);
+
task.emit('status.failed', { status: 'failed', previous: 'running' });
task.emit('completed', { status: 'failed', duration: 100 });
await expect(prom).rejects.toEqual(new ExitException(1));
// should print task failed
- expect(app.lastFrame()).toEqual(expect.ignoreColor(`${symbols.error} Run cmd in wks (took 100ms)`));
+ expect(app.lastFrame()).toEqualLines([
+ expect.ignoreColor(`${symbols.error} Run cmd in wks (took 100ms)`),
+ expect.ignoreColor(`${symbols.error} 1 failed`)
+ ]);
});
it('should log and exit if no task were yielded', async () => {
diff --git a/tests/ui/group-task-spinner.test.tsx b/tests/ui/group-task-spinner.test.tsx
deleted file mode 100644
index 7ec6b1223..000000000
--- a/tests/ui/group-task-spinner.test.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { ParallelGroup, SpawnTask } from '@jujulego/tasks';
-import { render, cleanup } from 'ink-testing-library';
-import symbols from 'log-symbols';
-import { vi } from 'vitest';
-
-import GroupTaskSpinner from '@/src/ui/group-task-spinner.js';
-import { flushPromises, spyLogger } from '@/tools/utils.js';
-
-// Setup
-let taskA: SpawnTask;
-let taskB: SpawnTask;
-let taskC: SpawnTask;
-
-let group: ParallelGroup;
-
-beforeEach(() => {
- taskA = new SpawnTask('cmd-a', [], {}, { logger: spyLogger });
- taskB = new SpawnTask('cmd-b', [], {}, { logger: spyLogger });
- taskC = new SpawnTask('cmd-c', [], {}, { logger: spyLogger });
-
- vi.spyOn(taskA, 'status', 'get').mockReturnValue('done');
- vi.spyOn(taskB, 'status', 'get').mockReturnValue('done');
- vi.spyOn(taskC, 'status', 'get').mockReturnValue('done');
-
- group = new ParallelGroup('Test group', {}, { logger: spyLogger });
- group.add(taskA);
- group.add(taskB);
-
- vi.spyOn(group, 'status', 'get').mockReturnValue('done');
-});
-
-afterEach(() => {
- cleanup();
-});
-
-// Tests
-describe('', () => {
- it('should print group and it\'s dependencies', async () => {
- const { lastFrame } = render();
-
- expect(lastFrame()).toEqualLines([
- expect.ignoreColor(`${symbols.success} Test group (took 0ms)`),
- expect.ignoreColor(` ${symbols.success} cmd-a (took 0ms)`),
- expect.ignoreColor(` ${symbols.success} cmd-b (took 0ms)`),
- ]);
-
- // Latter added dependency
- group.add(taskC);
- await flushPromises();
-
- expect(lastFrame()).toEqualLines([
- expect.ignoreColor(`${symbols.success} Test group (took 0ms)`),
- expect.ignoreColor(` ${symbols.success} cmd-a (took 0ms)`),
- expect.ignoreColor(` ${symbols.success} cmd-b (took 0ms)`),
- expect.ignoreColor(` ${symbols.success} cmd-c (took 0ms)`),
- ]);
- });
-
- it('should print group children', () => {
- // Add a "root" group
- const root = new ParallelGroup('Root', {});
- root.add(group);
- root.add(taskC);
-
- vi.spyOn(root, 'status', 'get').mockReturnValue('done');
-
- const { lastFrame } = render();
-
- expect(lastFrame()).toEqualLines([
- expect.ignoreColor(`${symbols.success} Root (took 0ms)`),
- expect.ignoreColor(` ${symbols.success} Test group (took 0ms)`),
- expect.ignoreColor(` ${symbols.success} cmd-a (took 0ms)`),
- expect.ignoreColor(` ${symbols.success} cmd-b (took 0ms)`),
- expect.ignoreColor(` ${symbols.success} cmd-c (took 0ms)`),
- ]);
- });
-});
diff --git a/tests/ui/task-manager-spinner.test.tsx b/tests/ui/task-manager-spinner.test.tsx
deleted file mode 100644
index aee018c58..000000000
--- a/tests/ui/task-manager-spinner.test.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { SpawnTask, TaskManager } from '@jujulego/tasks';
-import { render, cleanup } from 'ink-testing-library';
-import symbols from 'log-symbols';
-import { vi } from 'vitest';
-
-import TaskManagerSpinner from '@/src/ui/task-manager-spinner.js';
-import { spyLogger } from '@/tools/utils.js';
-
-// Setup
-let taskA: SpawnTask;
-let taskB: SpawnTask;
-let taskC: SpawnTask;
-
-beforeEach(() => {
- taskA = new SpawnTask('cmd-a', [], {}, { logger: spyLogger });
- taskB = new SpawnTask('cmd-b', [], {}, { logger: spyLogger });
- taskC = new SpawnTask('cmd-c', [], {}, { logger: spyLogger });
-
- vi.spyOn(taskA, 'status', 'get').mockReturnValue('done');
- vi.spyOn(taskB, 'status', 'get').mockReturnValue('done');
- vi.spyOn(taskC, 'status', 'get').mockReturnValue('done');
-});
-
-afterEach(() => {
- cleanup();
-});
-
-// Tests
-describe('', () => {
- it('should print every managed tasks', () => {
- const manager = new TaskManager({ jobs: 0, logger: spyLogger });
- manager.add(taskA);
- manager.add(taskB);
-
- const { lastFrame } = render();
-
- expect(lastFrame()).toEqualLines([
- expect.ignoreColor(`${symbols.success} cmd-a (took 0ms)`),
- expect.ignoreColor(`${symbols.success} cmd-b (took 0ms)`),
- ]);
- });
-
- it('should print added tasks while running', async () => {
- const manager = new TaskManager({ jobs: 0, logger: spyLogger });
- manager.add(taskA);
- manager.add(taskB);
-
- const { lastFrame } = render();
-
- manager.add(taskC);
- await new Promise((resolve) => setTimeout(resolve, 0));
-
- expect(lastFrame()).toEqualLines([
- expect.ignoreColor(`${symbols.success} cmd-a (took 0ms)`),
- expect.ignoreColor(`${symbols.success} cmd-b (took 0ms)`),
- expect.ignoreColor(`${symbols.success} cmd-c (took 0ms)`),
- ]);
- });
-});
diff --git a/tools/test-bed.ts b/tools/test-bed.ts
index f8743cc16..944c0889d 100644
--- a/tools/test-bed.ts
+++ b/tools/test-bed.ts
@@ -1,9 +1,9 @@
-import { type Package } from 'normalize-package-data';
import { ContainerModule } from 'inversify';
-import { type CommandModule } from 'yargs';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
+import { type Package } from 'normalize-package-data';
+import { type CommandModule } from 'yargs';
import { ContextService } from '@/src/commons/context.service.ts';
import { CONFIG } from '@/src/config/config-loader.ts';
diff --git a/tools/test-tasks.ts b/tools/test-tasks.ts
index 0e1357d3c..27f9be7bc 100644
--- a/tools/test-tasks.ts
+++ b/tools/test-tasks.ts
@@ -1,9 +1,14 @@
-import { ParallelGroup, SpawnTask, type TaskContext } from '@jujulego/tasks';
+import { ParallelGroup, SpawnTask, type TaskContext, TaskManager } from '@jujulego/tasks';
import { CommandTask } from '@/src/tasks/command-task.js';
import { ScriptTask } from '@/src/tasks/script-task.js';
// Classes
+export class TestTaskManager extends TaskManager {
+ // Methods
+ readonly emit = this._events.emit;
+}
+
export class TestParallelGroup extends ParallelGroup {
// Methods
readonly emit = this._groupEvents.emit;
diff --git a/yarn.lock b/yarn.lock
index 9f50b4a0d..bfa424651 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1794,9 +1794,9 @@ __metadata:
linkType: hard
"cli-spinners@npm:^2.7.0":
- version: 2.9.1
- resolution: "cli-spinners@npm:2.9.1"
- checksum: 1780618be58309c469205bc315db697934bac68bce78cd5dfd46248e507a533172d623c7348ecfd904734f597ce0a4e5538684843d2cfb7af485d4466699940c
+ version: 2.9.2
+ resolution: "cli-spinners@npm:2.9.2"
+ checksum: 1bd588289b28432e4676cb5d40505cfe3e53f2e4e10fbe05c8a710a154d6fe0ce7836844b00d6858f740f2ffe67cdc36e0fce9c7b6a8430e80e6388d5aa4956c
languageName: node
linkType: hard
@@ -5531,7 +5531,7 @@ __metadata:
languageName: node
linkType: hard
-"vite@npm:5.0.0":
+"vite@npm:5.0.0, vite@npm:^3.0.0 || ^4.0.0 || ^5.0.0-0, vite@npm:^3.1.0 || ^4.0.0 || ^5.0.0-0":
version: 5.0.0
resolution: "vite@npm:5.0.0"
dependencies:
@@ -5571,46 +5571,6 @@ __metadata:
languageName: node
linkType: hard
-"vite@npm:^3.0.0 || ^4.0.0 || ^5.0.0-0, vite@npm:^3.1.0 || ^4.0.0 || ^5.0.0-0":
- version: 5.0.0-beta.18
- resolution: "vite@npm:5.0.0-beta.18"
- dependencies:
- esbuild: ^0.19.3
- fsevents: ~2.3.3
- postcss: ^8.4.31
- rollup: ^4.2.0
- peerDependencies:
- "@types/node": ^18.0.0 || >=20.0.0
- less: "*"
- lightningcss: ^1.21.0
- sass: "*"
- stylus: "*"
- sugarss: "*"
- terser: ^5.4.0
- dependenciesMeta:
- fsevents:
- optional: true
- peerDependenciesMeta:
- "@types/node":
- optional: true
- less:
- optional: true
- lightningcss:
- optional: true
- sass:
- optional: true
- stylus:
- optional: true
- sugarss:
- optional: true
- terser:
- optional: true
- bin:
- vite: bin/vite.js
- checksum: 9481814791af3932266fa05112779d51d2f330b964d3472ca3373861b0a3c33169c55b5bc5fa82752f7d046ff320dfaaa0db77f2b78ec8cbe3dd6310eb7e8d69
- languageName: node
- linkType: hard
-
"vitest@npm:0.34.6":
version: 0.34.6
resolution: "vitest@npm:0.34.6"