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
138 changes: 138 additions & 0 deletions test/helpers/fakeBackend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { writeFile } from 'node:fs/promises';

import { vi } from 'vitest';

import type { RendererBackend } from '../../src/renderer/backend.js';
import type {
ReplayInput,
ReplayState,
ScreenshotResult,
SemanticSnapshot,
} from '../../src/renderer/types.js';

type MockFn = ReturnType<typeof vi.fn>;

const PNG_HEADER = Buffer.from([0x89, 0x50, 0x4e, 0x47]);

export interface FakeBackendOptions {
rendererBackend?: string;
bootImplementation?: () => Promise<void>;
resultOverrides?: Partial<ScreenshotResult>;
writePng?: boolean;
fail?: Error;
onScreenshot?: (
outputPath: string,
options?: { showCursor?: boolean },
) => void;
}

export type FakeRendererBackend = RendererBackend & {
bootMock: MockFn;
replayToMock: MockFn;
snapshotMock: MockFn;
screenshotMock: MockFn;
getVisibleTextMock: MockFn;
disposeMock: MockFn;
setBooted: (value: boolean) => void;
};

export function createFakeBackend(
options: FakeBackendOptions = {},
): FakeRendererBackend {
const rendererBackend = options.rendererBackend ?? 'fake-renderer';
const writePng = options.writePng ?? true;
let booted = false;

const bootMock = vi.fn((): Promise<void> => {
if (options.bootImplementation !== undefined) {
return options.bootImplementation();
}
booted = true;
return Promise.resolve();
});

const replayToMock = vi.fn(
(input: ReplayInput): Promise<ReplayState> =>
Promise.resolve({
lastSeq: input.targetSeq,
cols: input.initialCols,
rows: input.initialRows,
cursorRow: 0,
cursorCol: 0,
}),
);

const snapshotMock = vi.fn(
(): Promise<SemanticSnapshot> =>
Promise.resolve({
sessionId: 'session-01',
capturedAtSeq: 0,
cols: 80,
rows: 24,
cursorRow: 0,
cursorCol: 0,
isAltScreen: false,
visibleLines: [],
}),
);

const screenshotMock = vi.fn(
async (
outputPath: string,
screenshotOptions?: { showCursor?: boolean },
): Promise<ScreenshotResult> => {
options.onScreenshot?.(outputPath, screenshotOptions);
if (options.fail !== undefined) {
throw options.fail;
}
if (writePng) {
await writeFile(outputPath, PNG_HEADER);
}
return {
sessionId: 'session-01',
capturedAtSeq: 5,
profileName: 'reference-dark',
cols: 80,
rows: 24,
artifactPath: outputPath,
pngSizeBytes: 4,
cursorVisible: screenshotOptions?.showCursor === true,
rendererBackend,
pixelWidth: 800,
pixelHeight: 600,
sha256: 'a'.repeat(64),
renderProfileHash: 'b'.repeat(64),
...options.resultOverrides,
};
},
);

const getVisibleTextMock = vi.fn((): Promise<string> => Promise.resolve(''));

const disposeMock = vi.fn((): Promise<void> => {
booted = false;
return Promise.resolve();
});

return {
rendererBackend,
get isBooted() {
return booted;
},
setBooted(value: boolean) {
booted = value;
},
boot: bootMock,
bootMock,
replayTo: replayToMock,
replayToMock,
snapshot: snapshotMock,
snapshotMock,
screenshot: screenshotMock,
screenshotMock,
getVisibleText: getVisibleTextMock,
getVisibleTextMock,
dispose: disposeMock,
disposeMock,
};
}
96 changes: 4 additions & 92 deletions test/unit/host/renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,12 @@ import type { RendererBackend } from '../../../src/renderer/backend.js';
import type {
RenderProfileConfig,
ReplayInput,
ReplayState,
ScreenshotResult,
SemanticSnapshot,
} from '../../../src/renderer/types.js';

type MockFn = ReturnType<typeof vi.fn>;

type FakeRendererBackend = RendererBackend & {
bootMock: MockFn;
replayToMock: MockFn;
snapshotMock: MockFn;
screenshotMock: MockFn;
getVisibleTextMock: MockFn;
disposeMock: MockFn;
setBooted: (value: boolean) => void;
};
import {
createFakeBackend,
type FakeRendererBackend,
} from '../../helpers/fakeBackend.js';

function createProfile(name = 'default'): RenderProfileConfig {
return {
Expand Down Expand Up @@ -57,84 +47,6 @@ function createReplayInput(overrides: Partial<ReplayInput> = {}): ReplayInput {
};
}

function createFakeBackend(
options: {
bootImplementation?: () => Promise<void>;
} = {},
): FakeRendererBackend {
let booted = false;
const bootMock = vi.fn((): Promise<void> => {
if (options.bootImplementation !== undefined) {
return options.bootImplementation();
}

booted = true;
return Promise.resolve();
});
const replayToMock = vi.fn(
(input: ReplayInput): Promise<ReplayState> =>
Promise.resolve({
lastSeq: input.targetSeq,
cols: input.initialCols,
rows: input.initialRows,
cursorRow: 0,
cursorCol: 0,
}),
);
const snapshotMock = vi.fn(
(): Promise<SemanticSnapshot> =>
Promise.resolve({
sessionId: 'session-01',
capturedAtSeq: 0,
cols: 80,
rows: 24,
cursorRow: 0,
cursorCol: 0,
isAltScreen: false,
visibleLines: [],
}),
);
const screenshotMock = vi.fn(
(outputPath: string): Promise<ScreenshotResult> =>
Promise.resolve({
sessionId: 'session-01',
capturedAtSeq: 0,
profileName: 'default',
cols: 80,
rows: 24,
artifactPath: outputPath,
pngSizeBytes: 1,
}),
);
const getVisibleTextMock = vi.fn((): Promise<string> => Promise.resolve(''));
const disposeMock = vi.fn((): Promise<void> => {
booted = false;
return Promise.resolve();
});

return {
rendererBackend: 'fake-renderer',
boot: bootMock,
bootMock,
replayTo: replayToMock,
replayToMock,
snapshot: snapshotMock,
snapshotMock,
screenshot: screenshotMock,
screenshotMock,
getVisibleText: getVisibleTextMock,
getVisibleTextMock,
dispose: disposeMock,
disposeMock,
get isBooted() {
return booted;
},
setBooted(value: boolean) {
booted = value;
},
};
}

function createDeferred<T>(): {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
Expand Down
99 changes: 24 additions & 75 deletions test/unit/renderer/libghosttyVtBackend.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { describe, expect, it, vi } from 'vitest';

import type { RendererBackend } from '../../../src/renderer/backend.js';
import { LibghosttyVtBackend } from '../../../src/renderer/libghosttyVt/backend.js';
import type { LibghosttyVtNativeModule } from '../../../src/renderer/libghosttyVt/backend.js';
import type {
RenderProfileConfig,
ReplayInput,
ReplayState,
ScreenshotResult,
SemanticSnapshot,
} from '../../../src/renderer/types.js';
import { createLogger } from '../../../src/util/logger.js';

import { createFakeBackend } from '../../helpers/fakeBackend.js';

function createProfile(): RenderProfileConfig {
return {
name: 'reference-dark',
Expand Down Expand Up @@ -126,75 +124,6 @@ function createNativeFixture(options: { visibleText?: string } = {}) {
};
}

function createFallbackBackend(): RendererBackend & {
bootMock: ReturnType<typeof vi.fn>;
replayToMock: ReturnType<typeof vi.fn>;
screenshotMock: ReturnType<typeof vi.fn>;
disposeMock: ReturnType<typeof vi.fn>;
} {
let booted = false;
const bootMock = vi.fn(() => {
booted = true;
return Promise.resolve();
});
const replayToMock = vi.fn(
(input: ReplayInput): Promise<ReplayState> =>
Promise.resolve({
lastSeq: input.targetSeq,
cols: input.initialCols,
rows: input.initialRows,
cursorRow: 0,
cursorCol: 0,
}),
);
const screenshotMock = vi.fn(
(outputPath: string): Promise<ScreenshotResult> =>
Promise.resolve({
sessionId: 'session-01',
capturedAtSeq: 2,
profileName: 'reference-dark',
cols: 12,
rows: 5,
artifactPath: outputPath,
pngSizeBytes: 123,
rendererBackend: 'ghostty-web',
}),
);
const disposeMock = vi.fn(() => {
booted = false;
return Promise.resolve();
});

return {
rendererBackend: 'ghostty-web',
get isBooted() {
return booted;
},
boot: bootMock,
bootMock,
replayTo: replayToMock,
replayToMock,
snapshot: vi.fn(
(): Promise<SemanticSnapshot> =>
Promise.resolve({
sessionId: 'session-01',
capturedAtSeq: 2,
cols: 12,
rows: 5,
cursorRow: 0,
cursorCol: 0,
isAltScreen: false,
visibleLines: [],
}),
),
screenshot: screenshotMock,
screenshotMock,
getVisibleText: vi.fn().mockResolvedValue(''),
dispose: disposeMock,
disposeMock,
};
}

function createBackend(
fixture = createNativeFixture(),
options: Partial<ConstructorParameters<typeof LibghosttyVtBackend>[2]> = {},
Expand Down Expand Up @@ -357,7 +286,17 @@ describe('LibghosttyVtBackend', () => {

it('uses ghostty-web fallback for screenshots and preserves fallback metadata', async () => {
const fixture = createNativeFixture();
const fallback = createFallbackBackend();
const fallback = createFakeBackend({
rendererBackend: 'ghostty-web',
writePng: false,
resultOverrides: {
capturedAtSeq: 2,
cols: 12,
rows: 5,
pngSizeBytes: 123,
rendererBackend: 'ghostty-web',
},
});
const backend = createBackend(fixture, {
fallbackFactory: () => fallback,
});
Expand All @@ -383,7 +322,17 @@ describe('LibghosttyVtBackend', () => {

it('disposes native and fallback resources idempotently', async () => {
const fixture = createNativeFixture();
const fallback = createFallbackBackend();
const fallback = createFakeBackend({
rendererBackend: 'ghostty-web',
writePng: false,
resultOverrides: {
capturedAtSeq: 2,
cols: 12,
rows: 5,
pngSizeBytes: 123,
rendererBackend: 'ghostty-web',
},
});
const backend = createBackend(fixture, {
fallbackFactory: () => fallback,
});
Expand Down
Loading
Loading