Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/tui/__tests__/helpers/wait-for-ink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Ink redraws, and React re-attaches Ink's input listener, on their own
// schedule. A fixed sleep between keys races both on a busy machine, so tests
// wait for each key's visible effect instead.

// Two setImmediate turns let React run the passive effect that re-attaches the
// input listener after a redraw, so the next key isn't dropped
export async function letReactCatchUp(): Promise<void> {
for (let turn = 0; turn < 2; turn++) {
await new Promise((resolve) => {
setImmediate(resolve);
});
}
}

// Retries the assertions until they pass, then lets React catch up. Throws the
// last failure once the timeout passes.
export async function waitFor(assertions: () => void, timeoutMs = 3000): Promise<void> {
const deadline = Date.now() + timeoutMs;
for (;;) {
await new Promise((resolve) => {
setTimeout(resolve, 10);
});
try {
assertions();
await letReactCatchUp();
return;
} catch (error) {
if (Date.now() >= deadline) {
throw error;
}
}
}
}
98 changes: 54 additions & 44 deletions src/tui/components/__tests__/GlobalOverridesMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from 'vitest';

import { DEFAULT_SETTINGS } from '../../../types/Settings';
import { waitFor } from '../../__tests__/helpers/wait-for-ink';
import { GlobalOverridesMenu } from '../GlobalOverridesMenu';

class MockTtyStream extends PassThrough {
Expand Down Expand Up @@ -58,12 +59,6 @@ function createMockStdout(): CapturedWriteStream {
});
}

function flushInk() {
return new Promise((resolve) => {
setTimeout(resolve, 25);
});
}

describe('GlobalOverridesMenu', () => {
afterEach(() => {
vi.restoreAllMocks();
Expand Down Expand Up @@ -93,9 +88,10 @@ describe('GlobalOverridesMenu', () => {
);

try {
await flushInk();
expect(stdout.getOutput()).toContain('Minimalist Mode:');
expect(stdout.getOutput()).toContain('✗ Disabled');
await waitFor(() => {
expect(stdout.getOutput()).toContain('Minimalist Mode:');
expect(stdout.getOutput()).toContain('✗ Disabled');
});
} finally {
instance.unmount();
instance.cleanup();
Expand Down Expand Up @@ -127,17 +123,19 @@ describe('GlobalOverridesMenu', () => {
);

try {
await flushInk();
await waitFor(() => {
expect(stdout.getOutput()).toContain('Minimalist Mode:');
});
stdout.clearOutput();
stdin.write('n');
await flushInk();

const numberRows = stripAnsi(stdout.getOutput())
const numberRows = () => stripAnsi(stdout.getOutput())
.split('\n')
.filter(line => /(?:token|speed|percent|memory|cost): precise/.test(line));
const colonColumns = new Set(numberRows.map(line => line.indexOf(':')));
await waitFor(() => {
expect(numberRows()).toHaveLength(5);
});

expect(numberRows).toHaveLength(5);
const colonColumns = new Set(numberRows().map(line => line.indexOf(':')));
expect(colonColumns.size).toBe(1);
} finally {
instance.unmount();
Expand Down Expand Up @@ -172,11 +170,13 @@ describe('GlobalOverridesMenu', () => {
);

try {
await flushInk();
await waitFor(() => {
expect(stdout.getOutput()).toContain('Minimalist Mode:');
});
stdin.write('m');
await flushInk();

expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ minimalistMode: true }));
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ minimalistMode: true }));
});
} finally {
instance.unmount();
instance.cleanup();
Expand Down Expand Up @@ -210,11 +210,13 @@ describe('GlobalOverridesMenu', () => {
);

try {
await flushInk();
await waitFor(() => {
expect(stdout.getOutput()).toContain('Minimalist Mode:');
});
stdin.write('m');
await flushInk();

expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ minimalistMode: false }));
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ minimalistMode: false }));
});
} finally {
instance.unmount();
instance.cleanup();
Expand Down Expand Up @@ -248,9 +250,10 @@ describe('GlobalOverridesMenu', () => {
);

try {
await flushInk();
expect(stdout.getOutput()).toContain('Padding Side:');
expect(stdout.getOutput()).toContain('Both');
await waitFor(() => {
expect(stdout.getOutput()).toContain('Padding Side:');
expect(stdout.getOutput()).toContain('Both');
});
} finally {
instance.unmount();
instance.cleanup();
Expand Down Expand Up @@ -288,11 +291,13 @@ describe('GlobalOverridesMenu', () => {
);

try {
await flushInk();
await waitFor(() => {
expect(stdout.getOutput()).toContain('Minimalist Mode:');
});
stdin.write('d');
await flushInk();

expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ defaultPaddingSide: expected }));
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ defaultPaddingSide: expected }));
});
} finally {
instance.unmount();
instance.cleanup();
Expand Down Expand Up @@ -326,10 +331,10 @@ describe('GlobalOverridesMenu', () => {
);

try {
await flushInk();
const output = stdout.getOutput();
expect(output).toContain('Override FG Color:');
expect(output).toContain('(f) cycle, (g) gradient, (x) clear');
await waitFor(() => {
expect(stdout.getOutput()).toContain('Override FG Color:');
expect(stdout.getOutput()).toContain('(f) cycle, (g) gradient, (x) clear');
});
} finally {
instance.unmount();
instance.cleanup();
Expand Down Expand Up @@ -363,15 +368,18 @@ describe('GlobalOverridesMenu', () => {
);

try {
await flushInk();
await waitFor(() => {
expect(stdout.getOutput()).toContain('Minimalist Mode:');
});
stdin.write('g');
await flushInk();
expect(stdout.getOutput()).toContain('Select Gradient - Override FG Color');
await waitFor(() => {
expect(stdout.getOutput()).toContain('Select Gradient - Override FG Color');
});

stdin.write('\r');
await flushInk();

expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ overrideForegroundColor: 'gradient:atlas' }));
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ overrideForegroundColor: 'gradient:atlas' }));
});
} finally {
instance.unmount();
instance.cleanup();
Expand Down Expand Up @@ -405,11 +413,13 @@ describe('GlobalOverridesMenu', () => {
);

try {
await flushInk();
await waitFor(() => {
expect(stdout.getOutput()).toContain('Minimalist Mode:');
});
stdin.write('x');
await flushInk();

expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ overrideForegroundColor: undefined }));
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ overrideForegroundColor: undefined }));
});
} finally {
instance.unmount();
instance.cleanup();
Expand Down
20 changes: 9 additions & 11 deletions src/tui/components/__tests__/ImportPreviewDialog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
DEFAULT_SETTINGS,
type Settings
} from '../../../types/Settings';
import { waitFor } from '../../__tests__/helpers/wait-for-ink';
import {
ImportPreviewDialog,
getImportPreviewKeys,
Expand Down Expand Up @@ -63,10 +64,6 @@ function createMockStdout(): CapturedWriteStream {
});
}

function flushInk() {
return new Promise(resolve => setTimeout(resolve, 25));
}

describe('ImportPreviewDialog helpers', () => {
it('includes optional settings that exist only in the imported config', () => {
const current: Settings = { ...DEFAULT_SETTINGS };
Expand Down Expand Up @@ -129,16 +126,17 @@ describe('ImportPreviewDialog helpers', () => {
});

try {
await flushInk();
expect(stdout.getOutput()).toContain('flexMode: full-minus-40 → full');
await waitFor(() => {
expect(stdout.getOutput()).toContain('flexMode: full-minus-40 → full');
});

stdout.clearOutput();
stdin.write('\u001B[B');
await flushInk();

const output = stdout.getOutput();
const lastFlexModeRow = output.slice(output.lastIndexOf('flexMode:')).split('\n')[0];
expect(lastFlexModeRow).toBe('flexMode: full-minus-40');
await waitFor(() => {
const output = stdout.getOutput();
const lastFlexModeRow = output.slice(output.lastIndexOf('flexMode:')).split('\n')[0];
expect(lastFlexModeRow).toBe('flexMode: full-minus-40');
});
} finally {
instance.unmount();
instance.cleanup();
Expand Down
Loading
Loading