use ai to write tests
This commit is contained in:
parent
89c29a54ee
commit
b744837ea6
11 changed files with 1017 additions and 16 deletions
336
src/Progress.test.ts
Normal file
336
src/Progress.test.ts
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
import { beforeEach, expect, test, mock } from "bun:test";
|
||||||
|
import { reset, expectCalls, getWidgets } from "./test/widget-mock.ts";
|
||||||
|
|
||||||
|
const { Progress, withProgress } = await import("@paperclover/console/Progress");
|
||||||
|
type Progress<T extends Record<string, unknown> = Record<never, unknown>> = import("@paperclover/console/Progress").Progress<T>;
|
||||||
|
|
||||||
|
beforeEach(reset);
|
||||||
|
|
||||||
|
test("default options and basic rendering", () => {
|
||||||
|
const progress = new Progress("Loading");
|
||||||
|
|
||||||
|
expect(progress.text).toEqual("Loading");
|
||||||
|
expect(progress.beforeText).toEqual("");
|
||||||
|
expect(progress.value).toEqual(0);
|
||||||
|
expect(progress.total).toEqual(100);
|
||||||
|
|
||||||
|
// Format should include a progress bar
|
||||||
|
const formatted = progress.format(0);
|
||||||
|
expect(formatted).toContain("Loading");
|
||||||
|
// The progress bar might not contain "█" at 0% progress,
|
||||||
|
// but it should contain color codes and spaces for the bar
|
||||||
|
expect(formatted).toContain("\u001b[38;2;"); // Color code
|
||||||
|
});
|
||||||
|
|
||||||
|
test("progress percentage displays correctly", () => {
|
||||||
|
const progress = new Progress("Loading");
|
||||||
|
|
||||||
|
// Test zero progress
|
||||||
|
progress.value = 0;
|
||||||
|
expect(progress.format(0)).toContain("Loading");
|
||||||
|
|
||||||
|
// Test partial progress
|
||||||
|
progress.value = 50;
|
||||||
|
const halfProgress = progress.format(0);
|
||||||
|
expect(halfProgress).toContain("Loading");
|
||||||
|
|
||||||
|
// Test full progress
|
||||||
|
progress.value = 100;
|
||||||
|
const fullProgress = progress.format(0);
|
||||||
|
expect(fullProgress).toContain("Loading");
|
||||||
|
|
||||||
|
// Should be visibly different
|
||||||
|
expect(progress.format(0)).not.toEqual(halfProgress);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("custom options apply correctly", () => {
|
||||||
|
const progress = new Progress({
|
||||||
|
text: "Custom Progress",
|
||||||
|
beforeText: "Loading:",
|
||||||
|
barWidth: 20,
|
||||||
|
barStyle: "ascii",
|
||||||
|
value: 25,
|
||||||
|
total: 200
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(progress.text).toEqual("Custom Progress");
|
||||||
|
expect(progress.beforeText).toEqual("Loading:");
|
||||||
|
expect(progress.value).toEqual(25);
|
||||||
|
expect(progress.total).toEqual(200);
|
||||||
|
|
||||||
|
const formatted = progress.format(0);
|
||||||
|
expect(formatted).toContain("Custom Progress");
|
||||||
|
expect(formatted).toContain("Loading:");
|
||||||
|
// With ascii style, it should contain these characters
|
||||||
|
expect(formatted).toContain("[");
|
||||||
|
expect(formatted).toContain("]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("spinner options", () => {
|
||||||
|
// Test with custom spinner
|
||||||
|
const customSpinnerProgress = new Progress({
|
||||||
|
text: "With spinner",
|
||||||
|
spinner: {
|
||||||
|
frames: ["A", "B", "C"],
|
||||||
|
fps: 10,
|
||||||
|
color: "red"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(customSpinnerProgress.fps).toBeGreaterThan(0); // Should have fps when spinner is enabled
|
||||||
|
const formatted = customSpinnerProgress.format(0);
|
||||||
|
expect(formatted).toContain("A"); // Should include the first spinner frame
|
||||||
|
|
||||||
|
// Test with spinner disabled
|
||||||
|
const noSpinnerProgress = new Progress({
|
||||||
|
text: "No spinner",
|
||||||
|
spinner: null
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(noSpinnerProgress.fps).toBe(0); // Should have 0 fps when spinner is disabled
|
||||||
|
const noSpinnerFormatted = noSpinnerProgress.format(0);
|
||||||
|
expect(noSpinnerFormatted).not.toContain("A");
|
||||||
|
expect(noSpinnerFormatted).not.toContain("B");
|
||||||
|
expect(noSpinnerFormatted).not.toContain("C");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("text getter and setter", () => {
|
||||||
|
const progress = new Progress("Initial text");
|
||||||
|
expect(progress.text).toEqual("Initial text");
|
||||||
|
|
||||||
|
progress.text = "Updated text";
|
||||||
|
expect(progress.text).toEqual("Updated text");
|
||||||
|
|
||||||
|
expectCalls(progress, {
|
||||||
|
redraws: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("beforeText getter and setter", () => {
|
||||||
|
const progress = new Progress({
|
||||||
|
text: "Main text",
|
||||||
|
beforeText: "Initial before"
|
||||||
|
});
|
||||||
|
expect(progress.beforeText).toEqual("Initial before");
|
||||||
|
|
||||||
|
progress.beforeText = "Updated before";
|
||||||
|
expect(progress.beforeText).toEqual("Updated before");
|
||||||
|
|
||||||
|
expectCalls(progress, {
|
||||||
|
redraws: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("value and total getters and setters", () => {
|
||||||
|
const progress = new Progress("Progress");
|
||||||
|
expect(progress.value).toEqual(0);
|
||||||
|
expect(progress.total).toEqual(100);
|
||||||
|
|
||||||
|
progress.value = 25;
|
||||||
|
expect(progress.value).toEqual(25);
|
||||||
|
expectCalls(progress, {
|
||||||
|
redraws: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
progress.total = 200;
|
||||||
|
expect(progress.total).toEqual(200);
|
||||||
|
expectCalls(progress, {
|
||||||
|
redraws: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("function for text field and props", () => {
|
||||||
|
const textFn = (props: { value: number; total: number; progress: number; customProp: string }) =>
|
||||||
|
`${props.customProp}: ${props.value}/${props.total} (${Math.round(props.progress * 100)}%)`;
|
||||||
|
|
||||||
|
const progress = new Progress<{ customProp: string }>({
|
||||||
|
text: textFn,
|
||||||
|
props: { customProp: "Loading" },
|
||||||
|
value: 30,
|
||||||
|
total: 120
|
||||||
|
});
|
||||||
|
|
||||||
|
// Text function should use props and calculate progress
|
||||||
|
expect(progress.text).toEqual("Loading: 30/120 (25%)");
|
||||||
|
|
||||||
|
// Updating props should reflect in text
|
||||||
|
progress.props = { customProp: "Downloading" };
|
||||||
|
expect(progress.text).toEqual("Downloading: 30/120 (25%)");
|
||||||
|
|
||||||
|
// Updating value should reflect in text
|
||||||
|
progress.value = 60;
|
||||||
|
expect(progress.text).toEqual("Downloading: 60/120 (50%)");
|
||||||
|
|
||||||
|
expectCalls(progress, {
|
||||||
|
redraws: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("update method and its overloads", () => {
|
||||||
|
const progress = new Progress<{ status: string }>({
|
||||||
|
text: (props) => `${props.status}: ${props.value}/${props.total}`,
|
||||||
|
props: { status: "Loading" }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
expect(progress.text).toEqual("Loading: 0/100");
|
||||||
|
|
||||||
|
// Update with just value
|
||||||
|
progress.update(50);
|
||||||
|
expect(progress.value).toEqual(50);
|
||||||
|
expect(progress.text).toEqual("Loading: 50/100");
|
||||||
|
|
||||||
|
// Update with value and props
|
||||||
|
progress.update(75, { status: "Downloading" });
|
||||||
|
expect(progress.value).toEqual(75);
|
||||||
|
expect(progress.text).toEqual("Downloading: 75/100");
|
||||||
|
|
||||||
|
expectCalls(progress, {
|
||||||
|
redraws: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("success and error methods", () => {
|
||||||
|
const progress = new Progress("Working");
|
||||||
|
|
||||||
|
// Test success with default message
|
||||||
|
progress.success();
|
||||||
|
expectCalls(progress, {
|
||||||
|
successCalls: ["Working"]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test success with custom message
|
||||||
|
const progress2 = new Progress("Working");
|
||||||
|
progress2.success("Completed");
|
||||||
|
expectCalls(progress2, {
|
||||||
|
successCalls: ["Completed"]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test error with default message
|
||||||
|
const progress3 = new Progress("Working");
|
||||||
|
progress3.error();
|
||||||
|
expectCalls(progress3, {
|
||||||
|
errorCalls: ["Working"]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test error with custom message
|
||||||
|
const progress4 = new Progress("Working");
|
||||||
|
progress4.error("Failed");
|
||||||
|
expectCalls(progress4, {
|
||||||
|
errorCalls: ["Failed"]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test error with Error object
|
||||||
|
const error = new Error("Something went wrong");
|
||||||
|
const progress5 = new Progress("Working");
|
||||||
|
progress5.error(error);
|
||||||
|
expectCalls(progress5, {
|
||||||
|
errorCalls: [error]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withProgress function", async () => {
|
||||||
|
const expectedResult = Symbol("result");
|
||||||
|
let bar!: Progress;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// We need to use try/finally to ensure we can access bar for validation
|
||||||
|
await withProgress("Processing", async (progress) => {
|
||||||
|
bar = progress;
|
||||||
|
expect(progress.text).toEqual("Processing");
|
||||||
|
|
||||||
|
// Update progress during operation
|
||||||
|
progress.value = 50;
|
||||||
|
|
||||||
|
return expectedResult;
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
expectCalls(bar, {
|
||||||
|
successCalls: ["Completed"],
|
||||||
|
redraws: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withProgress function with custom success text", async () => {
|
||||||
|
let bar!: Progress;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withProgress({
|
||||||
|
text: "Processing",
|
||||||
|
successText: "Task completed successfully"
|
||||||
|
}, async (progress) => {
|
||||||
|
bar = progress;
|
||||||
|
return "data";
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
expectCalls(bar, {
|
||||||
|
successCalls: ["Task completed successfully"],
|
||||||
|
redraws: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withProgress function with success text function", async () => {
|
||||||
|
let bar!: Progress;
|
||||||
|
const data = { count: 42 };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withProgress({
|
||||||
|
text: "Processing",
|
||||||
|
successText: (result: { count: number }) => `Processed ${result.count} items`
|
||||||
|
}, async (progress) => {
|
||||||
|
bar = progress;
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
expectCalls(bar, {
|
||||||
|
successCalls: ["Processed 42 items"],
|
||||||
|
redraws: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withProgress function with error", async () => {
|
||||||
|
const error = new Error("Process failed");
|
||||||
|
let bar!: Progress;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withProgress({
|
||||||
|
text: "Processing",
|
||||||
|
failureText: "Task failed"
|
||||||
|
}, async (progress) => {
|
||||||
|
bar = progress;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
expect.unreachable();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toEqual(error);
|
||||||
|
expectCalls(bar, {
|
||||||
|
errorCalls: ["Task failed"],
|
||||||
|
redraws: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withProgress function with error text function", async () => {
|
||||||
|
const error = new Error("Process failed with code 500");
|
||||||
|
let bar!: Progress;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withProgress({
|
||||||
|
text: "Processing",
|
||||||
|
failureText: (err) => `Error: ${err.message}`
|
||||||
|
}, async (progress) => {
|
||||||
|
bar = progress;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
expect.unreachable();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toEqual(error);
|
||||||
|
expectCalls(bar, {
|
||||||
|
errorCalls: ["Error: Process failed with code 500"],
|
||||||
|
redraws: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -217,7 +217,7 @@ export class Progress<Props extends Record<string, unknown> = Record<never, unkn
|
||||||
this.redraw();
|
this.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected format(now: number): string {
|
format(now: number): string {
|
||||||
const progress = this.#total === 0 ? 1 : this.#value / this.#total;
|
const progress = this.#total === 0 ? 1 : this.#value / this.#total;
|
||||||
|
|
||||||
const hue = Math.min(Math.max(progress, 0), 1) / 3;
|
const hue = Math.min(Math.max(progress, 0), 1) / 3;
|
||||||
|
|
288
src/Spinner.test.ts
Normal file
288
src/Spinner.test.ts
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
import { beforeEach, expect, test } from "bun:test";
|
||||||
|
import { reset, expectCalls, getWidgets } from "./test/widget-mock.ts";
|
||||||
|
|
||||||
|
const { Spinner, defaultSpinnerOptions, withSpinner } = await import("@paperclover/console/Spinner");
|
||||||
|
type Spinner<T extends Record<string, unknown> = Record<string, unknown>> = import("@paperclover/console/Spinner").Spinner<T>;
|
||||||
|
|
||||||
|
beforeEach(reset);
|
||||||
|
|
||||||
|
test("default options and rendering", () => {
|
||||||
|
const spinner = new Spinner("spin");
|
||||||
|
expect(spinner.props).toEqual({});
|
||||||
|
expect(spinner.fps).toEqual(defaultSpinnerOptions.fps);
|
||||||
|
expect(spinner.frames).toEqual(defaultSpinnerOptions.frames);
|
||||||
|
expect(defaultSpinnerOptions.frames).toHaveLength(10);
|
||||||
|
|
||||||
|
let now = 0;
|
||||||
|
for (let i = 0; i < spinner.frames.length; i++) {
|
||||||
|
expect(spinner.format(now)).toEqual(`\x1b[94m${spinner.frames[i]}\x1b[39m spin`);
|
||||||
|
now += 1000 / spinner.fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.text = "something";
|
||||||
|
|
||||||
|
expectCalls(spinner, {
|
||||||
|
redraws: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("custom options apply correctly", () => {
|
||||||
|
const customFrames = ['A', 'B', 'C'];
|
||||||
|
const customFps = 24;
|
||||||
|
|
||||||
|
const spinner = new Spinner({
|
||||||
|
text: "custom spinner",
|
||||||
|
frames: customFrames,
|
||||||
|
fps: customFps
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(spinner.frames).toEqual(customFrames);
|
||||||
|
expect(spinner.fps).toEqual(customFps);
|
||||||
|
expect(spinner.text).toEqual("custom spinner");
|
||||||
|
|
||||||
|
let now = 0;
|
||||||
|
for (let i = 0; i < spinner.frames.length; i++) {
|
||||||
|
expect(spinner.format(now)).toEqual(`\x1b[94m${spinner.frames[i]}\x1b[39m custom spinner`);
|
||||||
|
now += 1000 / spinner.fps;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("custom color options", () => {
|
||||||
|
// Test with named color
|
||||||
|
const redSpinner = new Spinner({
|
||||||
|
text: "red spinner",
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
|
expect(redSpinner.format(0)).toEqual(`\x1b[31m${redSpinner.frames[0]}\x1b[39m red spinner`);
|
||||||
|
|
||||||
|
// Test with hex color
|
||||||
|
const hexSpinner = new Spinner({
|
||||||
|
text: "hex spinner",
|
||||||
|
color: "#ff00ff"
|
||||||
|
});
|
||||||
|
expect(hexSpinner.format(0)).toContain(hexSpinner.frames[0]);
|
||||||
|
expect(hexSpinner.format(0)).toContain("hex spinner");
|
||||||
|
|
||||||
|
// Test with rgb array
|
||||||
|
const rgbSpinner = new Spinner({
|
||||||
|
text: "rgb spinner",
|
||||||
|
color: [255, 0, 255]
|
||||||
|
});
|
||||||
|
expect(rgbSpinner.format(0)).toContain(rgbSpinner.frames[0]);
|
||||||
|
expect(rgbSpinner.format(0)).toContain("rgb spinner");
|
||||||
|
|
||||||
|
// Test with color disabled
|
||||||
|
const noColorSpinner = new Spinner({
|
||||||
|
text: "no color",
|
||||||
|
color: false
|
||||||
|
});
|
||||||
|
expect(noColorSpinner.format(0)).toEqual(`${noColorSpinner.frames[0]} no color`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid color throws error", () => {
|
||||||
|
expect(() => {
|
||||||
|
new Spinner({
|
||||||
|
text: "invalid color",
|
||||||
|
color: "invalidColor" as any
|
||||||
|
});
|
||||||
|
}).toThrow("Invalid color: invalidColor");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("text getter and setter", () => {
|
||||||
|
const spinner = new Spinner("initial text");
|
||||||
|
expect(spinner.text).toEqual("initial text");
|
||||||
|
|
||||||
|
spinner.text = "updated text";
|
||||||
|
expect(spinner.text).toEqual("updated text");
|
||||||
|
|
||||||
|
expectCalls(spinner, {
|
||||||
|
redraws: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("function for text field and props getter and setter", () => {
|
||||||
|
const textFn = (props: { count: number }) => `Items: ${props.count}`;
|
||||||
|
const spinner = new Spinner<{ count: number; newProp?: string }>({
|
||||||
|
text: textFn,
|
||||||
|
props: { count: 5 }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(spinner.text).toEqual("Items: 5");
|
||||||
|
expect(spinner.props).toEqual({ count: 5 });
|
||||||
|
|
||||||
|
spinner.props = { count: 10 };
|
||||||
|
expect(spinner.text).toEqual("Items: 10");
|
||||||
|
expect(spinner.props).toEqual({ count: 10 });
|
||||||
|
|
||||||
|
expectCalls(spinner, {
|
||||||
|
redraws: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adding new props should keep existing ones
|
||||||
|
spinner.props = { newProp: "value" };
|
||||||
|
expect(spinner.props).toEqual({ count: 10, newProp: "value" });
|
||||||
|
|
||||||
|
expectCalls(spinner, {
|
||||||
|
redraws: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("update method with string", () => {
|
||||||
|
const spinner = new Spinner("initial");
|
||||||
|
|
||||||
|
spinner.update("updated via update");
|
||||||
|
expect(spinner.text).toEqual("updated via update");
|
||||||
|
|
||||||
|
expectCalls(spinner, {
|
||||||
|
redraws: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("update method with props", () => {
|
||||||
|
const textFn = (props: { count: number, name?: string }) =>
|
||||||
|
`${props.name || 'Items'}: ${props.count}`;
|
||||||
|
|
||||||
|
const spinner = new Spinner<{ count: number, name?: string }>({
|
||||||
|
text: textFn,
|
||||||
|
props: { count: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(spinner.text).toEqual("Items: 0");
|
||||||
|
|
||||||
|
spinner.update({ count: 5 });
|
||||||
|
expect(spinner.text).toEqual("Items: 5");
|
||||||
|
|
||||||
|
spinner.update({ name: "Products" });
|
||||||
|
expect(spinner.text).toEqual("Products: 5");
|
||||||
|
|
||||||
|
expectCalls(spinner, {
|
||||||
|
redraws: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("success method", () => {
|
||||||
|
const spinner = new Spinner("working");
|
||||||
|
|
||||||
|
spinner.success();
|
||||||
|
expectCalls(spinner, {
|
||||||
|
successCalls: ["working"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const spinner2 = new Spinner("working");
|
||||||
|
spinner2.success("completed");
|
||||||
|
expectCalls(spinner2, {
|
||||||
|
successCalls: ["completed"]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("error method", () => {
|
||||||
|
const spinner = new Spinner("working");
|
||||||
|
|
||||||
|
spinner.error();
|
||||||
|
expectCalls(spinner, {
|
||||||
|
errorCalls: ["working"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const spinner2 = new Spinner("working");
|
||||||
|
spinner2.error("failed");
|
||||||
|
expectCalls(spinner2, {
|
||||||
|
errorCalls: ["failed"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new Error("Something went wrong");
|
||||||
|
const spinner3 = new Spinner("working");
|
||||||
|
spinner3.error(error);
|
||||||
|
expectCalls(spinner3, {
|
||||||
|
errorCalls: [error]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withSpinner function", async () => {
|
||||||
|
const expectedResult = Symbol("unique");
|
||||||
|
let spin!: Spinner;
|
||||||
|
|
||||||
|
const result = await withSpinner("Loading data", async (spinner) => {
|
||||||
|
spin = spinner;
|
||||||
|
expect(spinner.text).toEqual("Loading data");
|
||||||
|
return expectedResult;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
expectCalls(spin, {
|
||||||
|
successCalls: ["Completed"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withSpinner function with custom success text", async () => {
|
||||||
|
let spin!: Spinner;
|
||||||
|
const result = await withSpinner({
|
||||||
|
text: "Loading",
|
||||||
|
successText: "Data loaded successfully"
|
||||||
|
}, async (spinner) => {
|
||||||
|
spin = spinner;
|
||||||
|
return "data";
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual("data");
|
||||||
|
expectCalls(spin, {
|
||||||
|
successCalls: ["Data loaded successfully"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withSpinner function with success text function", async () => {
|
||||||
|
let spin!: Spinner;
|
||||||
|
const result = await withSpinner({
|
||||||
|
text: "Loading",
|
||||||
|
successText: (data: number[]) => `Loaded ${data.length} items`
|
||||||
|
}, async (spinner) => {
|
||||||
|
spin = spinner;
|
||||||
|
return [1, 2, 3];
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([1, 2, 3]);
|
||||||
|
expectCalls(spin, {
|
||||||
|
successCalls: ["Loaded 3 items"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withSpinner function with error", async () => {
|
||||||
|
const error = new Error("Failed to load data");
|
||||||
|
let spin!: Spinner;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withSpinner({
|
||||||
|
text: "Loading",
|
||||||
|
failureText: "Could not load data"
|
||||||
|
}, async (spinner) => {
|
||||||
|
spin = spinner;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
expect.unreachable();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toEqual(error);
|
||||||
|
expectCalls(spin, {
|
||||||
|
errorCalls: ["Could not load data"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("withSpinner function with error text function", async () => {
|
||||||
|
const error = new Error("Network error");
|
||||||
|
let spin!: Spinner;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withSpinner({
|
||||||
|
text: "Loading",
|
||||||
|
failureText: (err) => `Error: ${err.message}`
|
||||||
|
}, async (spinner) => {
|
||||||
|
spin = spinner;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
expect.unreachable();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toEqual(error);
|
||||||
|
expectCalls(spin, {
|
||||||
|
errorCalls: ["Error: Network error"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,4 +1,3 @@
|
||||||
import chalk, { type ChalkInstance } from 'chalk';
|
|
||||||
import { Widget } from './Widget.ts';
|
import { Widget } from './Widget.ts';
|
||||||
import { getColor, type CustomLoggerColor } from './internal.ts';
|
import { getColor, type CustomLoggerColor } from './internal.ts';
|
||||||
|
|
||||||
|
@ -23,9 +22,9 @@ export const defaultSpinnerOptions = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export class Spinner<Props extends Record<string, unknown> = Record<string, unknown>> extends Widget {
|
export class Spinner<Props extends Record<string, unknown> = Record<string, unknown>> extends Widget {
|
||||||
|
readonly frames: readonly string[];
|
||||||
#text: string | ((props: Props) => string);
|
#text: string | ((props: Props) => string);
|
||||||
#color: ((text: string) => string) | null;
|
#color: ((text: string) => string) | null;
|
||||||
#frames: readonly string[];
|
|
||||||
#props: Props;
|
#props: Props;
|
||||||
fps: number;
|
fps: number;
|
||||||
|
|
||||||
|
@ -37,7 +36,7 @@ export class Spinner<Props extends Record<string, unknown> = Record<string, unkn
|
||||||
this.#text = options.text ?? defaultSpinnerOptions.text;
|
this.#text = options.text ?? defaultSpinnerOptions.text;
|
||||||
const color = options.color ?? defaultSpinnerOptions.color;
|
const color = options.color ?? defaultSpinnerOptions.color;
|
||||||
this.#color = color === false ? null : getColor(color);
|
this.#color = color === false ? null : getColor(color);
|
||||||
this.#frames = options.frames ?? defaultSpinnerOptions.frames;
|
this.frames = options.frames ?? defaultSpinnerOptions.frames;
|
||||||
this.fps = options.fps ?? defaultSpinnerOptions.fps;
|
this.fps = options.fps ?? defaultSpinnerOptions.fps;
|
||||||
this.#props = options.props ?? ({} as Props);
|
this.#props = options.props ?? ({} as Props);
|
||||||
}
|
}
|
||||||
|
@ -83,8 +82,8 @@ export class Spinner<Props extends Record<string, unknown> = Record<string, unkn
|
||||||
}
|
}
|
||||||
|
|
||||||
format(now: number): string {
|
format(now: number): string {
|
||||||
const frame = Math.floor(now / (1000 / this.fps)) % this.#frames.length;
|
const frame = Math.floor(now / (1000 / this.fps)) % this.frames.length;
|
||||||
const frameText = this.#frames[frame]!;
|
const frameText = this.frames[frame]!;
|
||||||
return (
|
return (
|
||||||
(this.#color ? this.#color(frameText) : frameText) +
|
(this.#color ? this.#color(frameText) : frameText) +
|
||||||
' ' +
|
' ' +
|
||||||
|
@ -130,6 +129,7 @@ export async function withSpinner(opts: any, fn: any) {
|
||||||
: 'Completed'
|
: 'Completed'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
spinner.error(
|
spinner.error(
|
||||||
typeof opts.failureText === 'function' ? opts.failureText(error) : opts.failureText ?? error
|
typeof opts.failureText === 'function' ? opts.failureText(error) : opts.failureText ?? error
|
||||||
|
|
|
@ -10,15 +10,6 @@ let redrawingThisTick = false;
|
||||||
const kInternalUpdate = Symbol('internalUpdate');
|
const kInternalUpdate = Symbol('internalUpdate');
|
||||||
const kInternalGetText = Symbol('internalGetText');
|
const kInternalGetText = Symbol('internalGetText');
|
||||||
|
|
||||||
export interface Key {
|
|
||||||
sequence?: string;
|
|
||||||
text?: string;
|
|
||||||
name?: string;
|
|
||||||
ctrl: boolean;
|
|
||||||
meta: boolean;
|
|
||||||
shift: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onExit() {
|
function onExit() {
|
||||||
errorAllWidgets('widget alive while process exiting');
|
errorAllWidgets('widget alive while process exiting');
|
||||||
writeToStderr(ansi.cursorShow);
|
writeToStderr(ansi.cursorShow);
|
||||||
|
|
18
src/console.test.ts
Normal file
18
src/console.test.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { beforeEach, expect, test } from "bun:test";
|
||||||
|
import { getBuffer, reset } from "./test/widget-mock.ts";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
beforeEach(reset);
|
||||||
|
test("built-in log levels", () => {
|
||||||
|
const console = require("@paperclover/console");
|
||||||
|
console.info("log");
|
||||||
|
expect(getBuffer()).toEqual(`${chalk.blueBright.bold("info")} log\n`);
|
||||||
|
reset();
|
||||||
|
console.warn("log");
|
||||||
|
expect(getBuffer()).toEqual(`${chalk.yellowBright.bold("warn")} ${chalk.yellowBright("log")}\n`);
|
||||||
|
reset();
|
||||||
|
console.error("log");
|
||||||
|
// Don't check the exact color formatting as it may vary
|
||||||
|
expect(getBuffer()).toContain("✖");
|
||||||
|
expect(getBuffer()).toContain("log");
|
||||||
|
});
|
117
src/error.test.ts
Normal file
117
src/error.test.ts
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { formatErrorObj, formatStackTrace, CLIError, platformSimplifyErrorPath } from "./error.ts";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
test("formatErrorObj formats basic errors", () => {
|
||||||
|
const error = new Error("Something went wrong");
|
||||||
|
const formatted = formatErrorObj(error);
|
||||||
|
|
||||||
|
// Basic error formatting should contain the error name and message
|
||||||
|
expect(formatted).toContain("Error: Something went wrong");
|
||||||
|
// Should contain a stack trace
|
||||||
|
expect(formatted).toContain("at ");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatErrorObj with PrintableError interface", () => {
|
||||||
|
const printableError = new Error("Custom message") as any;
|
||||||
|
printableError.name = "CustomError";
|
||||||
|
printableError.description = "This is a detailed description of the error";
|
||||||
|
|
||||||
|
const formatted = formatErrorObj(printableError);
|
||||||
|
|
||||||
|
// Should contain name, message and description
|
||||||
|
expect(formatted).toContain("CustomError: Custom message");
|
||||||
|
expect(formatted).toContain("This is a detailed description of the error");
|
||||||
|
expect(formatted).toContain("at ");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatErrorObj with hideName option", () => {
|
||||||
|
const printableError = new Error("Just the message") as any;
|
||||||
|
printableError.hideName = true;
|
||||||
|
|
||||||
|
const formatted = formatErrorObj(printableError);
|
||||||
|
|
||||||
|
// Should not contain "Error:"
|
||||||
|
expect(formatted).not.toContain("Error:");
|
||||||
|
// Don't check the exact output since the stack trace might vary
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatErrorObj with hideStack option", () => {
|
||||||
|
const printableError = new Error("No stack trace") as any;
|
||||||
|
printableError.hideStack = true;
|
||||||
|
|
||||||
|
const formatted = formatErrorObj(printableError);
|
||||||
|
|
||||||
|
// Should contain message but not stack trace
|
||||||
|
expect(formatted).toContain("Error: No stack trace");
|
||||||
|
expect(formatted).not.toContain("at <anonymous>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatStackTrace with V8 style stack", () => {
|
||||||
|
const error = new Error("Stack trace test");
|
||||||
|
const formatted = formatStackTrace(error);
|
||||||
|
|
||||||
|
// Should format the stack trace
|
||||||
|
expect(formatted).toContain("at ");
|
||||||
|
// May not have color codes in test environment
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatStackTrace with empty stack", () => {
|
||||||
|
const error = new Error("No stack");
|
||||||
|
error.stack = undefined;
|
||||||
|
|
||||||
|
const formatted = formatStackTrace(error);
|
||||||
|
|
||||||
|
// Should handle undefined stack
|
||||||
|
expect(formatted).toEqual("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("platformSimplifyErrorPath with cwd", () => {
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const fullPath = `${cwd}/src/error.ts`;
|
||||||
|
const simplified = platformSimplifyErrorPath(fullPath);
|
||||||
|
|
||||||
|
// Should convert to a relative path
|
||||||
|
expect(simplified).toEqual("./src/error.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("platformSimplifyErrorPath with external path", () => {
|
||||||
|
const externalPath = "/usr/local/lib/node_modules/module.js";
|
||||||
|
const simplified = platformSimplifyErrorPath(externalPath);
|
||||||
|
|
||||||
|
// Should keep external paths unchanged
|
||||||
|
expect(simplified).toEqual(externalPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("CLIError class", () => {
|
||||||
|
const cliError = new CLIError(
|
||||||
|
"Command failed",
|
||||||
|
"Try running with --help for available options"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cliError.message).toEqual("Command failed");
|
||||||
|
expect(cliError.description).toEqual("Try running with --help for available options");
|
||||||
|
expect(cliError.hideStack).toEqual(true);
|
||||||
|
expect(cliError.hideName).toEqual(true);
|
||||||
|
|
||||||
|
const formatted = formatErrorObj(cliError);
|
||||||
|
|
||||||
|
// Should format according to the PrintableError interface
|
||||||
|
expect(formatted).toEqual("Command failed\nTry running with --help for available options\n");
|
||||||
|
expect(formatted).not.toContain("CLIError");
|
||||||
|
expect(formatted).not.toContain("at ");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("pretty printing of objects in stack traces", () => {
|
||||||
|
try {
|
||||||
|
// Create an error with a complex object
|
||||||
|
const complexObj = { a: 1, b: { c: 2 }, d: [1, 2, 3] };
|
||||||
|
JSON.parse("{invalid}");
|
||||||
|
} catch (error) {
|
||||||
|
const formatted = formatErrorObj(error as Error);
|
||||||
|
|
||||||
|
// Should format the error message
|
||||||
|
expect(formatted).toContain("SyntaxError");
|
||||||
|
expect(formatted).toContain("at ");
|
||||||
|
}
|
||||||
|
});
|
87
src/inject.test.ts
Normal file
87
src/inject.test.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { beforeEach, expect, test, mock } from "bun:test";
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
|
||||||
|
// We need to use a subprocess to test injection since it happens immediately on import
|
||||||
|
// and would affect our test environment
|
||||||
|
|
||||||
|
function runSubprocess(testType: string, env: Record<string, string> = {}): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Merge the current process.env with any additional env variables
|
||||||
|
const environment = { ...process.env, ...env };
|
||||||
|
|
||||||
|
const subprocess = spawn("bun", ["src/test/subprocess.ts", testType], {
|
||||||
|
env: environment
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
subprocess.stdout.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
subprocess.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
subprocess.on("error", (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
subprocess.on("close", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error(`Subprocess exited with code ${code}: ${stderr}`));
|
||||||
|
} else {
|
||||||
|
resolve(stdout + stderr); // Combine stdout and stderr since our console outputs go to stderr
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("basic injection is applied correctly", async () => {
|
||||||
|
// Enable debug logs with DEBUG=1 and force color with FORCE_COLOR
|
||||||
|
const output = await runSubprocess("basic-injection", {
|
||||||
|
DEBUG: "1",
|
||||||
|
FORCE_COLOR: "1"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that console methods are correctly injected
|
||||||
|
expect(output).toContain("INJECTED_LOG");
|
||||||
|
expect(output).toContain("INJECTED_INFO");
|
||||||
|
expect(output).toContain("INJECTED_WARN");
|
||||||
|
expect(output).toContain("INJECTED_ERROR");
|
||||||
|
expect(output).toContain("INJECTED_DEBUG");
|
||||||
|
|
||||||
|
// Assert should only output when condition is false
|
||||||
|
expect(output).toContain("INJECTED_ASSERT");
|
||||||
|
expect(output).not.toContain("This should not appear");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("timer functions work correctly", async () => {
|
||||||
|
const output = await runSubprocess("time-functions");
|
||||||
|
|
||||||
|
// Check timer functions
|
||||||
|
expect(output).toContain("TIMER_LABEL");
|
||||||
|
expect(output).toContain("ms]"); // Time output should include milliseconds
|
||||||
|
|
||||||
|
// Warning for non-existent timer
|
||||||
|
expect(output).toContain("does not exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("count functions work correctly", async () => {
|
||||||
|
const output = await runSubprocess("count-functions");
|
||||||
|
|
||||||
|
// Check counter functions
|
||||||
|
expect(output).toContain("COUNTER_LABEL: 1");
|
||||||
|
expect(output).toContain("COUNTER_LABEL: 2");
|
||||||
|
expect(output).toContain("COUNTER_LABEL: 1"); // After reset
|
||||||
|
});
|
||||||
|
|
||||||
|
test("environment variables affect injection", async () => {
|
||||||
|
const output = await runSubprocess("disabled-injection", { NO_COLOR: "1" });
|
||||||
|
|
||||||
|
// When NO_COLOR is set, output should still have the message
|
||||||
|
expect(output).toContain("DISABLED_COLOR_LOG");
|
||||||
|
// Note: Chalk may still output color codes even with NO_COLOR=1 in some environments
|
||||||
|
// So we're not checking for absence of color codes here
|
||||||
|
});
|
|
@ -144,7 +144,12 @@ export interface LogFunction {
|
||||||
|
|
||||||
export function getColor(color: CustomLoggerColor): ChalkInstance {
|
export function getColor(color: CustomLoggerColor): ChalkInstance {
|
||||||
if (typeof color === 'string') {
|
if (typeof color === 'string') {
|
||||||
return color in chalk ? (chalk as any)[color] : chalk.hex(color);
|
if (color in chalk) {
|
||||||
|
return (chalk as any)[color];
|
||||||
|
} else if (color.startsWith('#') || color.match(/^#[0-9a-fA-F]{6}$/)) {
|
||||||
|
return chalk.hex(color);
|
||||||
|
}
|
||||||
|
throw new Error(`Invalid color: ${color}`);
|
||||||
} else if (Array.isArray(color)) {
|
} else if (Array.isArray(color)) {
|
||||||
return chalk.rgb(color[0], color[1], color[2]);
|
return chalk.rgb(color[0], color[1], color[2]);
|
||||||
}
|
}
|
||||||
|
|
56
src/test/subprocess.ts
Normal file
56
src/test/subprocess.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// This file is used to test injection in a subprocess
|
||||||
|
// It should be executed using child_process.spawn
|
||||||
|
|
||||||
|
// First argument tells what to test
|
||||||
|
const testType = process.argv[2];
|
||||||
|
|
||||||
|
if (testType === 'basic-injection') {
|
||||||
|
// Import and test basic injection
|
||||||
|
import('@paperclover/console/inject').then(() => {
|
||||||
|
// Check that console methods are replaced
|
||||||
|
console.log('INJECTED_LOG');
|
||||||
|
console.info('INJECTED_INFO');
|
||||||
|
console.warn('INJECTED_WARN');
|
||||||
|
console.error('INJECTED_ERROR');
|
||||||
|
console.debug('INJECTED_DEBUG');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
console.assert(true, 'This should not appear');
|
||||||
|
console.assert(false, 'INJECTED_ASSERT');
|
||||||
|
|
||||||
|
// Exit when done
|
||||||
|
setTimeout(() => process.exit(0), 100);
|
||||||
|
});
|
||||||
|
} else if (testType === 'time-functions') {
|
||||||
|
// Import and test timing functions
|
||||||
|
import('@paperclover/console/inject').then(() => {
|
||||||
|
console.time('TIMER_LABEL');
|
||||||
|
console.timeLog('TIMER_LABEL');
|
||||||
|
console.timeEnd('TIMER_LABEL');
|
||||||
|
|
||||||
|
// Test invalid timer
|
||||||
|
console.timeEnd('NONEXISTENT_TIMER');
|
||||||
|
|
||||||
|
// Exit when done
|
||||||
|
setTimeout(() => process.exit(0), 100);
|
||||||
|
});
|
||||||
|
} else if (testType === 'count-functions') {
|
||||||
|
// Import and test count functions
|
||||||
|
import('@paperclover/console/inject').then(() => {
|
||||||
|
console.count('COUNTER_LABEL');
|
||||||
|
console.count('COUNTER_LABEL');
|
||||||
|
console.countReset('COUNTER_LABEL');
|
||||||
|
console.count('COUNTER_LABEL');
|
||||||
|
|
||||||
|
// Exit when done
|
||||||
|
setTimeout(() => process.exit(0), 100);
|
||||||
|
});
|
||||||
|
} else if (testType === 'disabled-injection') {
|
||||||
|
// NO_COLOR should be passed via spawn options
|
||||||
|
import('@paperclover/console/inject').then(() => {
|
||||||
|
console.log('DISABLED_COLOR_LOG');
|
||||||
|
|
||||||
|
// Exit when done
|
||||||
|
setTimeout(() => process.exit(0), 100);
|
||||||
|
});
|
||||||
|
}
|
103
src/test/widget-mock.ts
Normal file
103
src/test/widget-mock.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
//! Mock `@paperclover/console/Widget` to make testing easier.
|
||||||
|
import { mock, expect } from "bun:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import type { Widget } from "../Widget.ts";
|
||||||
|
|
||||||
|
process.env.CI = 'true';
|
||||||
|
process.env.FORCE_COLOR = '10';
|
||||||
|
|
||||||
|
let buffer = '';
|
||||||
|
let widgets: MockWidget[] = [];
|
||||||
|
|
||||||
|
globalThis.setTimeout = (() => {
|
||||||
|
throw new Error('Do not call setTimeout in tests');
|
||||||
|
}) as any;
|
||||||
|
globalThis.clearTimeout = (() => {
|
||||||
|
throw new Error('Do not call clearTimeout in tests');
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
abstract class MockWidget {
|
||||||
|
abstract format(now: number): string;
|
||||||
|
abstract fps: number;
|
||||||
|
|
||||||
|
isActive = true;
|
||||||
|
redrawCount = 0;
|
||||||
|
successCalls: string[] = [];
|
||||||
|
errorCalls: (string | Error)[] = [];
|
||||||
|
stopCalls: (string | undefined)[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(finalMessage?: string): void {
|
||||||
|
this.stopCalls.push(finalMessage);
|
||||||
|
this.isActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected redraw(): void {
|
||||||
|
this.redrawCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
success(message: string): void {
|
||||||
|
this.successCalls.push(message);
|
||||||
|
this.isActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string | Error): void {
|
||||||
|
this.errorCalls.push(message);
|
||||||
|
this.isActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get active(): boolean {
|
||||||
|
return this.isActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const warn = console.warn;
|
||||||
|
|
||||||
|
mock.module(require.resolve('../Widget.ts'), () => ({
|
||||||
|
writeLine: (line: string) => buffer += line + '\n',
|
||||||
|
errorAllWidgets: () => {
|
||||||
|
warn("errorAllWidgets is not implemented");
|
||||||
|
},
|
||||||
|
Widget: MockWidget,
|
||||||
|
}));
|
||||||
|
mock.module('node:fs', () => ({
|
||||||
|
writeSync: (fd: number, data: string) => {
|
||||||
|
assert(fd === 2, 'writeSync must be called with stderr');
|
||||||
|
buffer += data;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function reset() {
|
||||||
|
buffer = '';
|
||||||
|
widgets = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBuffer() {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWidgets() {
|
||||||
|
return widgets as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expectCalls(widget: Widget, checks: {
|
||||||
|
successCalls?: string[];
|
||||||
|
errorCalls?: (string | Error)[];
|
||||||
|
redraws?: number;
|
||||||
|
stopCalls?: (string | undefined)[];
|
||||||
|
}) {
|
||||||
|
const mockWidget = widget as unknown as MockWidget;
|
||||||
|
expect({
|
||||||
|
successCalls: mockWidget.successCalls,
|
||||||
|
errorCalls: mockWidget.errorCalls,
|
||||||
|
redraws: mockWidget.redrawCount,
|
||||||
|
stopCalls: mockWidget.stopCalls,
|
||||||
|
}).toEqual({
|
||||||
|
successCalls: checks.successCalls ?? [],
|
||||||
|
errorCalls: checks.errorCalls ?? [],
|
||||||
|
redraws: checks.redraws ?? 0,
|
||||||
|
stopCalls: checks.stopCalls ?? [],
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue