mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
tests(ui): add test suite for StagingAreaApi
This commit is contained in:
committed by
Kent Keirsey
parent
3fb0fcbbfb
commit
c61bcd9f50
@@ -0,0 +1,193 @@
|
||||
import { merge } from 'es-toolkit';
|
||||
import type { StagingAreaAppApi } from 'features/controlLayers/components/StagingArea/state';
|
||||
import type { AutoSwitchMode } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import type { ImageDTO, S } from 'services/api/types';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
export const createMockStagingAreaApp = (): StagingAreaAppApi & {
|
||||
// Additional methods for testing
|
||||
_triggerItemsChanged: (items: S['SessionQueueItem'][]) => void;
|
||||
_triggerQueueItemStatusChanged: (data: S['QueueItemStatusChangedEvent']) => void;
|
||||
_triggerInvocationProgress: (data: S['InvocationProgressEvent']) => void;
|
||||
_setAutoSwitchMode: (mode: AutoSwitchMode) => void;
|
||||
_setImageDTO: (imageName: string, imageDTO: ImageDTO | null) => void;
|
||||
_setLoadImageDelay: (delay: number) => void;
|
||||
} => {
|
||||
const itemsChangedHandlers = new Set<(items: S['SessionQueueItem'][]) => void>();
|
||||
const queueItemStatusChangedHandlers = new Set<(data: S['QueueItemStatusChangedEvent']) => void>();
|
||||
const invocationProgressHandlers = new Set<(data: S['InvocationProgressEvent']) => void>();
|
||||
|
||||
let autoSwitchMode: AutoSwitchMode = 'switch_on_start';
|
||||
const imageDTOs = new Map<string, ImageDTO | null>();
|
||||
let loadImageDelay = 0;
|
||||
|
||||
return {
|
||||
onDiscard: vi.fn(),
|
||||
onDiscardAll: vi.fn(),
|
||||
onAccept: vi.fn(),
|
||||
onSelect: vi.fn(),
|
||||
onSelectPrev: vi.fn(),
|
||||
onSelectNext: vi.fn(),
|
||||
onSelectFirst: vi.fn(),
|
||||
onSelectLast: vi.fn(),
|
||||
getAutoSwitch: vi.fn(() => autoSwitchMode),
|
||||
onAutoSwitchChange: vi.fn(),
|
||||
getImageDTO: vi.fn((imageName: string) => {
|
||||
return Promise.resolve(imageDTOs.get(imageName) || null);
|
||||
}),
|
||||
loadImage: vi.fn(async (imageName: string) => {
|
||||
if (loadImageDelay > 0) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, loadImageDelay);
|
||||
});
|
||||
}
|
||||
// Mock HTMLImageElement for testing environment
|
||||
const mockImage = {
|
||||
src: imageName,
|
||||
width: 512,
|
||||
height: 512,
|
||||
onload: null,
|
||||
onerror: null,
|
||||
} as HTMLImageElement;
|
||||
return mockImage;
|
||||
}),
|
||||
onItemsChanged: vi.fn((handler) => {
|
||||
itemsChangedHandlers.add(handler);
|
||||
return () => itemsChangedHandlers.delete(handler);
|
||||
}),
|
||||
onQueueItemStatusChanged: vi.fn((handler) => {
|
||||
queueItemStatusChangedHandlers.add(handler);
|
||||
return () => queueItemStatusChangedHandlers.delete(handler);
|
||||
}),
|
||||
onInvocationProgress: vi.fn((handler) => {
|
||||
invocationProgressHandlers.add(handler);
|
||||
return () => invocationProgressHandlers.delete(handler);
|
||||
}),
|
||||
|
||||
// Testing helper methods
|
||||
_triggerItemsChanged: (items: S['SessionQueueItem'][]) => {
|
||||
itemsChangedHandlers.forEach((handler) => handler(items));
|
||||
},
|
||||
_triggerQueueItemStatusChanged: (data: S['QueueItemStatusChangedEvent']) => {
|
||||
queueItemStatusChangedHandlers.forEach((handler) => handler(data));
|
||||
},
|
||||
_triggerInvocationProgress: (data: S['InvocationProgressEvent']) => {
|
||||
invocationProgressHandlers.forEach((handler) => handler(data));
|
||||
},
|
||||
_setAutoSwitchMode: (mode: AutoSwitchMode) => {
|
||||
autoSwitchMode = mode;
|
||||
},
|
||||
_setImageDTO: (imageName: string, imageDTO: ImageDTO | null) => {
|
||||
imageDTOs.set(imageName, imageDTO);
|
||||
},
|
||||
_setLoadImageDelay: (delay: number) => {
|
||||
loadImageDelay = delay;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createMockQueueItem = (overrides: PartialDeep<S['SessionQueueItem']> = {}): S['SessionQueueItem'] =>
|
||||
merge(
|
||||
{
|
||||
item_id: 1,
|
||||
batch_id: 'test-batch-id',
|
||||
session_id: 'test-session',
|
||||
queue_id: 'test-queue-id',
|
||||
status: 'pending',
|
||||
priority: 0,
|
||||
origin: null,
|
||||
destination: 'test-session',
|
||||
error_type: null,
|
||||
error_message: null,
|
||||
error_traceback: null,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
started_at: null,
|
||||
completed_at: null,
|
||||
field_values: null,
|
||||
retried_from_item_id: null,
|
||||
is_api_validation_run: false,
|
||||
published_workflow_id: null,
|
||||
session: {
|
||||
id: 'test-session',
|
||||
graph: {},
|
||||
execution_graph: {},
|
||||
executed: [],
|
||||
executed_history: [],
|
||||
results: {
|
||||
'test-node-id': {
|
||||
image: {
|
||||
image_name: 'test-image.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: {},
|
||||
prepared_source_mapping: {},
|
||||
source_prepared_mapping: {
|
||||
canvas_output: ['test-node-id'],
|
||||
},
|
||||
},
|
||||
workflow: null,
|
||||
},
|
||||
overrides
|
||||
) as S['SessionQueueItem'];
|
||||
|
||||
export const createMockImageDTO = (overrides: Partial<ImageDTO> = {}): ImageDTO => ({
|
||||
image_name: 'test-image.png',
|
||||
image_url: 'http://test.com/test-image.png',
|
||||
thumbnail_url: 'http://test.com/test-image-thumb.png',
|
||||
image_origin: 'internal',
|
||||
image_category: 'general',
|
||||
width: 512,
|
||||
height: 512,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
deleted_at: null,
|
||||
is_intermediate: false,
|
||||
starred: false,
|
||||
has_workflow: false,
|
||||
session_id: 'test-session',
|
||||
node_id: 'test-node-id',
|
||||
board_id: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const createMockProgressEvent = (
|
||||
overrides: PartialDeep<S['InvocationProgressEvent']> = {}
|
||||
): S['InvocationProgressEvent'] =>
|
||||
merge(
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
queue_id: 'test-queue-id',
|
||||
item_id: 1,
|
||||
batch_id: 'test-batch-id',
|
||||
session_id: 'test-session',
|
||||
origin: null,
|
||||
destination: 'test-session',
|
||||
invocation: {},
|
||||
invocation_source_id: 'test-invocation-source-id',
|
||||
message: 'Processing...',
|
||||
percentage: 50,
|
||||
image: null,
|
||||
} as S['InvocationProgressEvent'],
|
||||
overrides
|
||||
);
|
||||
|
||||
export const createMockQueueItemStatusChangedEvent = (
|
||||
overrides: PartialDeep<S['QueueItemStatusChangedEvent']> = {}
|
||||
): S['QueueItemStatusChangedEvent'] =>
|
||||
merge(
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
queue_id: 'test-queue-id',
|
||||
item_id: 1,
|
||||
batch_id: 'test-batch-id',
|
||||
origin: null,
|
||||
destination: 'test-session',
|
||||
status: 'completed',
|
||||
error_type: null,
|
||||
error_message: null,
|
||||
} as S['QueueItemStatusChangedEvent'],
|
||||
overrides
|
||||
);
|
||||
@@ -0,0 +1,205 @@
|
||||
import type { S } from 'services/api/types';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getOutputImageName, getProgressMessage, getQueueItemElementId } from './shared';
|
||||
|
||||
describe('StagingAreaApi Utility Functions', () => {
|
||||
describe('getProgressMessage', () => {
|
||||
it('should return default message when no data provided', () => {
|
||||
expect(getProgressMessage()).toBe('Generating');
|
||||
expect(getProgressMessage(null)).toBe('Generating');
|
||||
});
|
||||
|
||||
it('should format progress message when data is provided', () => {
|
||||
const progressEvent: S['InvocationProgressEvent'] = {
|
||||
item_id: 1,
|
||||
destination: 'test-session',
|
||||
node_id: 'test-node',
|
||||
source_node_id: 'test-source-node',
|
||||
progress: 0.5,
|
||||
message: 'Processing image...',
|
||||
image: null,
|
||||
} as unknown as S['InvocationProgressEvent'];
|
||||
|
||||
const result = getProgressMessage(progressEvent);
|
||||
expect(result).toBe('Processing image...');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getQueueItemElementId', () => {
|
||||
it('should generate correct element ID for queue item', () => {
|
||||
expect(getQueueItemElementId(0)).toBe('queue-item-preview-0');
|
||||
expect(getQueueItemElementId(5)).toBe('queue-item-preview-5');
|
||||
expect(getQueueItemElementId(99)).toBe('queue-item-preview-99');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOutputImageName', () => {
|
||||
it('should extract image name from completed queue item', () => {
|
||||
const queueItem: S['SessionQueueItem'] = {
|
||||
item_id: 1,
|
||||
status: 'completed',
|
||||
priority: 0,
|
||||
destination: 'test-session',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
started_at: '2024-01-01T00:00:01Z',
|
||||
completed_at: '2024-01-01T00:01:00Z',
|
||||
error: null,
|
||||
session: {
|
||||
id: 'test-session',
|
||||
source_prepared_mapping: {
|
||||
canvas_output: ['output-node-id'],
|
||||
},
|
||||
results: {
|
||||
'output-node-id': {
|
||||
image: {
|
||||
image_name: 'test-output.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as S['SessionQueueItem'];
|
||||
|
||||
expect(getOutputImageName(queueItem)).toBe('test-output.png');
|
||||
});
|
||||
|
||||
it('should return null when no canvas output node found', () => {
|
||||
const queueItem = {
|
||||
item_id: 1,
|
||||
status: 'completed',
|
||||
priority: 0,
|
||||
destination: 'test-session',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
started_at: '2024-01-01T00:00:01Z',
|
||||
completed_at: '2024-01-01T00:01:00Z',
|
||||
error: null,
|
||||
session: {
|
||||
id: 'test-session',
|
||||
source_prepared_mapping: {
|
||||
some_other_node: ['other-node-id'],
|
||||
},
|
||||
results: {
|
||||
'other-node-id': {
|
||||
type: 'image_output',
|
||||
image: {
|
||||
image_name: 'test-output.png',
|
||||
},
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as S['SessionQueueItem'];
|
||||
|
||||
expect(getOutputImageName(queueItem)).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null when output node has no results', () => {
|
||||
const queueItem: S['SessionQueueItem'] = {
|
||||
item_id: 1,
|
||||
status: 'completed',
|
||||
priority: 0,
|
||||
destination: 'test-session',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
started_at: '2024-01-01T00:00:01Z',
|
||||
completed_at: '2024-01-01T00:01:00Z',
|
||||
error: null,
|
||||
session: {
|
||||
id: 'test-session',
|
||||
source_prepared_mapping: {
|
||||
canvas_output: ['output-node-id'],
|
||||
},
|
||||
results: {},
|
||||
},
|
||||
} as unknown as S['SessionQueueItem'];
|
||||
|
||||
expect(getOutputImageName(queueItem)).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null when results contain no image fields', () => {
|
||||
const queueItem: S['SessionQueueItem'] = {
|
||||
item_id: 1,
|
||||
status: 'completed',
|
||||
priority: 0,
|
||||
destination: 'test-session',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
started_at: '2024-01-01T00:00:01Z',
|
||||
completed_at: '2024-01-01T00:01:00Z',
|
||||
error: null,
|
||||
session: {
|
||||
id: 'test-session',
|
||||
source_prepared_mapping: {
|
||||
canvas_output: ['output-node-id'],
|
||||
},
|
||||
results: {
|
||||
'output-node-id': {
|
||||
text: 'some text output',
|
||||
number: 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as S['SessionQueueItem'];
|
||||
|
||||
expect(getOutputImageName(queueItem)).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle multiple outputs and return first image', () => {
|
||||
const queueItem: S['SessionQueueItem'] = {
|
||||
item_id: 1,
|
||||
status: 'completed',
|
||||
priority: 0,
|
||||
destination: 'test-session',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
started_at: '2024-01-01T00:00:01Z',
|
||||
completed_at: '2024-01-01T00:01:00Z',
|
||||
error: null,
|
||||
session: {
|
||||
id: 'test-session',
|
||||
source_prepared_mapping: {
|
||||
canvas_output: ['output-node-id'],
|
||||
},
|
||||
results: {
|
||||
'output-node-id': {
|
||||
text: 'some text',
|
||||
first_image: {
|
||||
image_name: 'first-image.png',
|
||||
},
|
||||
second_image: {
|
||||
image_name: 'second-image.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as S['SessionQueueItem'];
|
||||
|
||||
const result = getOutputImageName(queueItem);
|
||||
expect(result).toBe('first-image.png');
|
||||
});
|
||||
|
||||
it('should handle empty session mapping', () => {
|
||||
const queueItem: S['SessionQueueItem'] = {
|
||||
item_id: 1,
|
||||
status: 'completed',
|
||||
priority: 0,
|
||||
destination: 'test-session',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
started_at: '2024-01-01T00:00:01Z',
|
||||
completed_at: '2024-01-01T00:01:00Z',
|
||||
error: null,
|
||||
session: {
|
||||
id: 'test-session',
|
||||
source_prepared_mapping: {},
|
||||
results: {},
|
||||
},
|
||||
} as unknown as S['SessionQueueItem'];
|
||||
|
||||
expect(getOutputImageName(queueItem)).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,781 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
createMockImageDTO,
|
||||
createMockProgressEvent,
|
||||
createMockQueueItem,
|
||||
createMockQueueItemStatusChangedEvent,
|
||||
createMockStagingAreaApp,
|
||||
} from './__mocks__/mockStagingAreaApp';
|
||||
import { StagingAreaApi } from './state';
|
||||
|
||||
describe('StagingAreaApi', () => {
|
||||
let api: StagingAreaApi;
|
||||
let mockApp: ReturnType<typeof createMockStagingAreaApp>;
|
||||
const sessionId = 'test-session';
|
||||
|
||||
beforeEach(() => {
|
||||
mockApp = createMockStagingAreaApp();
|
||||
api = new StagingAreaApi(sessionId, mockApp);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
api.cleanup();
|
||||
});
|
||||
|
||||
describe('Constructor and Setup', () => {
|
||||
it('should initialize with correct session ID', () => {
|
||||
expect(api.sessionId).toBe(sessionId);
|
||||
});
|
||||
|
||||
it('should set up event subscriptions', () => {
|
||||
expect(mockApp.onItemsChanged).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(mockApp.onQueueItemStatusChanged).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(mockApp.onInvocationProgress).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
it('should initialize atoms with default values', () => {
|
||||
expect(api.$lastStartedItemId.get()).toBe(null);
|
||||
expect(api.$lastCompletedItemId.get()).toBe(null);
|
||||
expect(api.$items.get()).toEqual([]);
|
||||
expect(api.$progressData.get()).toEqual({});
|
||||
expect(api.$selectedItemId.get()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed Values', () => {
|
||||
it('should compute item count correctly', () => {
|
||||
expect(api.$itemCount.get()).toBe(0);
|
||||
|
||||
const items = [createMockQueueItem({ item_id: 1 })];
|
||||
api.$items.set(items);
|
||||
expect(api.$itemCount.get()).toBe(1);
|
||||
});
|
||||
|
||||
it('should compute hasItems correctly', () => {
|
||||
expect(api.$hasItems.get()).toBe(false);
|
||||
|
||||
const items = [createMockQueueItem({ item_id: 1 })];
|
||||
api.$items.set(items);
|
||||
expect(api.$hasItems.get()).toBe(true);
|
||||
});
|
||||
|
||||
it('should compute isPending correctly', () => {
|
||||
expect(api.$isPending.get()).toBe(false);
|
||||
|
||||
const items = [
|
||||
createMockQueueItem({ item_id: 1, status: 'pending' }),
|
||||
createMockQueueItem({ item_id: 2, status: 'completed' }),
|
||||
];
|
||||
api.$items.set(items);
|
||||
expect(api.$isPending.get()).toBe(true);
|
||||
});
|
||||
|
||||
it('should compute selectedItem correctly', () => {
|
||||
expect(api.$selectedItem.get()).toBe(null);
|
||||
|
||||
const items = [createMockQueueItem({ item_id: 1 }), createMockQueueItem({ item_id: 2 })];
|
||||
api.$items.set(items);
|
||||
api.$selectedItemId.set(1);
|
||||
|
||||
const selectedItem = api.$selectedItem.get();
|
||||
expect(selectedItem).not.toBe(null);
|
||||
expect(selectedItem?.item.item_id).toBe(1);
|
||||
expect(selectedItem?.index).toBe(0);
|
||||
});
|
||||
|
||||
it('should compute selectedItemImageDTO correctly', () => {
|
||||
const items = [createMockQueueItem({ item_id: 1 })];
|
||||
const imageDTO = createMockImageDTO();
|
||||
|
||||
api.$items.set(items);
|
||||
api.$selectedItemId.set(1);
|
||||
api.$progressData.setKey(1, {
|
||||
itemId: 1,
|
||||
progressEvent: null,
|
||||
progressImage: null,
|
||||
imageDTO,
|
||||
});
|
||||
|
||||
expect(api.$selectedItemImageDTO.get()).toBe(imageDTO);
|
||||
});
|
||||
|
||||
it('should compute selectedItemIndex correctly', () => {
|
||||
const items = [createMockQueueItem({ item_id: 1 }), createMockQueueItem({ item_id: 2 })];
|
||||
api.$items.set(items);
|
||||
api.$selectedItemId.set(2);
|
||||
|
||||
expect(api.$selectedItemIndex.get()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Selection Methods', () => {
|
||||
beforeEach(() => {
|
||||
const items = [
|
||||
createMockQueueItem({ item_id: 1 }),
|
||||
createMockQueueItem({ item_id: 2 }),
|
||||
createMockQueueItem({ item_id: 3 }),
|
||||
];
|
||||
api.$items.set(items);
|
||||
});
|
||||
|
||||
it('should select item by ID', () => {
|
||||
api.select(2);
|
||||
expect(api.$selectedItemId.get()).toBe(2);
|
||||
expect(mockApp.onSelect).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('should select next item', () => {
|
||||
api.$selectedItemId.set(1);
|
||||
api.selectNext();
|
||||
expect(api.$selectedItemId.get()).toBe(2);
|
||||
expect(mockApp.onSelectNext).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should wrap to first item when selecting next from last', () => {
|
||||
api.$selectedItemId.set(3);
|
||||
api.selectNext();
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
});
|
||||
|
||||
it('should select previous item', () => {
|
||||
api.$selectedItemId.set(2);
|
||||
api.selectPrev();
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
expect(mockApp.onSelectPrev).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should wrap to last item when selecting previous from first', () => {
|
||||
api.$selectedItemId.set(1);
|
||||
api.selectPrev();
|
||||
expect(api.$selectedItemId.get()).toBe(3);
|
||||
});
|
||||
|
||||
it('should select first item', () => {
|
||||
api.selectFirst();
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
expect(mockApp.onSelectFirst).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should select last item', () => {
|
||||
api.selectLast();
|
||||
expect(api.$selectedItemId.get()).toBe(3);
|
||||
expect(mockApp.onSelectLast).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing when no items exist', () => {
|
||||
api.$items.set([]);
|
||||
api.selectNext();
|
||||
api.selectPrev();
|
||||
api.selectFirst();
|
||||
api.selectLast();
|
||||
|
||||
expect(api.$selectedItemId.get()).toBe(null);
|
||||
});
|
||||
|
||||
it('should do nothing when no item is selected', () => {
|
||||
api.$selectedItemId.set(null);
|
||||
api.selectNext();
|
||||
api.selectPrev();
|
||||
|
||||
expect(api.$selectedItemId.get()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Discard Methods', () => {
|
||||
beforeEach(() => {
|
||||
const items = [
|
||||
createMockQueueItem({ item_id: 1 }),
|
||||
createMockQueueItem({ item_id: 2 }),
|
||||
createMockQueueItem({ item_id: 3 }),
|
||||
];
|
||||
api.$items.set(items);
|
||||
});
|
||||
|
||||
it('should discard selected item and select next', () => {
|
||||
api.$selectedItemId.set(2);
|
||||
const selectedItem = api.$selectedItem.get();
|
||||
|
||||
api.discardSelected();
|
||||
|
||||
expect(api.$selectedItemId.get()).toBe(3);
|
||||
expect(mockApp.onDiscard).toHaveBeenCalledWith(selectedItem?.item);
|
||||
});
|
||||
|
||||
it('should discard selected item and clamp to last valid index', () => {
|
||||
api.$selectedItemId.set(3);
|
||||
const selectedItem = api.$selectedItem.get();
|
||||
|
||||
api.discardSelected();
|
||||
|
||||
// The logic clamps to the next index, so when discarding last item (index 2),
|
||||
// it tries to select index 3 which clamps to index 2 (item 3)
|
||||
expect(api.$selectedItemId.get()).toBe(3);
|
||||
expect(mockApp.onDiscard).toHaveBeenCalledWith(selectedItem?.item);
|
||||
});
|
||||
|
||||
it('should set selectedItemId to null when discarding last item', () => {
|
||||
api.$items.set([createMockQueueItem({ item_id: 1 })]);
|
||||
api.$selectedItemId.set(1);
|
||||
|
||||
api.discardSelected();
|
||||
|
||||
// When there's only one item, after clamping we get the same item, so it stays selected
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
});
|
||||
|
||||
it('should do nothing when no item is selected', () => {
|
||||
api.$selectedItemId.set(null);
|
||||
api.discardSelected();
|
||||
|
||||
expect(mockApp.onDiscard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should discard all items', () => {
|
||||
api.$selectedItemId.set(2);
|
||||
api.discardAll();
|
||||
|
||||
expect(api.$selectedItemId.get()).toBe(null);
|
||||
expect(mockApp.onDiscardAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should compute discardSelectedIsEnabled correctly', () => {
|
||||
expect(api.$discardSelectedIsEnabled.get()).toBe(false);
|
||||
|
||||
api.$selectedItemId.set(1);
|
||||
expect(api.$discardSelectedIsEnabled.get()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accept Methods', () => {
|
||||
beforeEach(() => {
|
||||
const items = [createMockQueueItem({ item_id: 1 })];
|
||||
api.$items.set(items);
|
||||
api.$selectedItemId.set(1);
|
||||
});
|
||||
|
||||
it('should accept selected item when image is available', () => {
|
||||
const imageDTO = createMockImageDTO();
|
||||
api.$progressData.setKey(1, {
|
||||
itemId: 1,
|
||||
progressEvent: null,
|
||||
progressImage: null,
|
||||
imageDTO,
|
||||
});
|
||||
|
||||
const selectedItem = api.$selectedItem.get();
|
||||
api.acceptSelected();
|
||||
|
||||
expect(mockApp.onAccept).toHaveBeenCalledWith(selectedItem?.item, imageDTO);
|
||||
});
|
||||
|
||||
it('should do nothing when no image is available', () => {
|
||||
api.$progressData.setKey(1, {
|
||||
itemId: 1,
|
||||
progressEvent: null,
|
||||
progressImage: null,
|
||||
imageDTO: null,
|
||||
});
|
||||
|
||||
api.acceptSelected();
|
||||
|
||||
expect(mockApp.onAccept).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing when no item is selected', () => {
|
||||
api.$selectedItemId.set(null);
|
||||
api.acceptSelected();
|
||||
|
||||
expect(mockApp.onAccept).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should compute acceptSelectedIsEnabled correctly', () => {
|
||||
expect(api.$acceptSelectedIsEnabled.get()).toBe(false);
|
||||
|
||||
const imageDTO = createMockImageDTO();
|
||||
api.$progressData.setKey(1, {
|
||||
itemId: 1,
|
||||
progressEvent: null,
|
||||
progressImage: null,
|
||||
imageDTO,
|
||||
});
|
||||
|
||||
expect(api.$acceptSelectedIsEnabled.get()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Progress Event Handling', () => {
|
||||
it('should handle invocation progress events', () => {
|
||||
const progressEvent = createMockProgressEvent({
|
||||
item_id: 1,
|
||||
destination: sessionId,
|
||||
});
|
||||
|
||||
api.onInvocationProgressEvent(progressEvent);
|
||||
|
||||
const progressData = api.$progressData.get();
|
||||
expect(progressData[1]?.progressEvent).toBe(progressEvent);
|
||||
});
|
||||
|
||||
it('should ignore events for different sessions', () => {
|
||||
const progressEvent = createMockProgressEvent({
|
||||
item_id: 1,
|
||||
destination: 'different-session',
|
||||
});
|
||||
|
||||
api.onInvocationProgressEvent(progressEvent);
|
||||
|
||||
const progressData = api.$progressData.get();
|
||||
expect(progressData[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should update existing progress data', () => {
|
||||
api.$progressData.setKey(1, {
|
||||
itemId: 1,
|
||||
progressEvent: null,
|
||||
progressImage: null,
|
||||
imageDTO: createMockImageDTO(),
|
||||
});
|
||||
|
||||
const progressEvent = createMockProgressEvent({
|
||||
item_id: 1,
|
||||
destination: sessionId,
|
||||
});
|
||||
|
||||
api.onInvocationProgressEvent(progressEvent);
|
||||
|
||||
const progressData = api.$progressData.get();
|
||||
expect(progressData[1]?.progressEvent).toBe(progressEvent);
|
||||
expect(progressData[1]?.imageDTO).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Queue Item Status Change Handling', () => {
|
||||
it('should handle completed status and set last completed item', () => {
|
||||
const statusEvent = createMockQueueItemStatusChangedEvent({
|
||||
item_id: 1,
|
||||
destination: sessionId,
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
api.onQueueItemStatusChangedEvent(statusEvent);
|
||||
|
||||
expect(api.$lastCompletedItemId.get()).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle in_progress status with switch_on_start', () => {
|
||||
mockApp._setAutoSwitchMode('switch_on_start');
|
||||
|
||||
const statusEvent = createMockQueueItemStatusChangedEvent({
|
||||
item_id: 1,
|
||||
destination: sessionId,
|
||||
status: 'in_progress',
|
||||
});
|
||||
|
||||
api.onQueueItemStatusChangedEvent(statusEvent);
|
||||
|
||||
expect(api.$lastStartedItemId.get()).toBe(1);
|
||||
});
|
||||
|
||||
it('should ignore events for different sessions', () => {
|
||||
const statusEvent = createMockQueueItemStatusChangedEvent({
|
||||
item_id: 1,
|
||||
destination: 'different-session',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
api.onQueueItemStatusChangedEvent(statusEvent);
|
||||
|
||||
expect(api.$lastCompletedItemId.get()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Items Changed Event Handling', () => {
|
||||
it('should update items and auto-select first item', async () => {
|
||||
const items = [createMockQueueItem({ item_id: 1 }), createMockQueueItem({ item_id: 2 })];
|
||||
|
||||
await api.onItemsChangedEvent(items);
|
||||
|
||||
expect(api.$items.get()).toBe(items);
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
});
|
||||
|
||||
it('should clear selection when no items', async () => {
|
||||
api.$selectedItemId.set(1);
|
||||
|
||||
await api.onItemsChangedEvent([]);
|
||||
|
||||
expect(api.$selectedItemId.get()).toBe(null);
|
||||
});
|
||||
|
||||
it('should not change selection if item already selected', async () => {
|
||||
api.$selectedItemId.set(2);
|
||||
|
||||
const items = [createMockQueueItem({ item_id: 1 }), createMockQueueItem({ item_id: 2 })];
|
||||
|
||||
await api.onItemsChangedEvent(items);
|
||||
|
||||
expect(api.$selectedItemId.get()).toBe(2);
|
||||
});
|
||||
|
||||
it('should load images for completed items', async () => {
|
||||
const imageDTO = createMockImageDTO({ image_name: 'test-image.png' });
|
||||
mockApp._setImageDTO('test-image.png', imageDTO);
|
||||
|
||||
const items = [
|
||||
createMockQueueItem({
|
||||
item_id: 1,
|
||||
status: 'completed',
|
||||
}),
|
||||
];
|
||||
|
||||
await api.onItemsChangedEvent(items);
|
||||
|
||||
const progressData = api.$progressData.get();
|
||||
expect(progressData[1]?.imageDTO).toBe(imageDTO);
|
||||
});
|
||||
|
||||
it('should handle auto-switch on completion', async () => {
|
||||
mockApp._setAutoSwitchMode('switch_on_finish');
|
||||
api.$lastCompletedItemId.set(1);
|
||||
|
||||
const imageDTO = createMockImageDTO({ image_name: 'test-image.png' });
|
||||
mockApp._setImageDTO('test-image.png', imageDTO);
|
||||
|
||||
const items = [
|
||||
createMockQueueItem({
|
||||
item_id: 1,
|
||||
status: 'completed',
|
||||
}),
|
||||
];
|
||||
|
||||
await api.onItemsChangedEvent(items);
|
||||
|
||||
// Wait for async image loading - the loadImage promise needs to complete
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 50);
|
||||
});
|
||||
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
// The lastCompletedItemId should be reset after the loadImage promise resolves
|
||||
expect(api.$lastCompletedItemId.get()).toBe(null);
|
||||
});
|
||||
|
||||
it('should clean up progress data for removed items', async () => {
|
||||
api.$progressData.setKey(999, {
|
||||
itemId: 999,
|
||||
progressEvent: null,
|
||||
progressImage: null,
|
||||
imageDTO: null,
|
||||
});
|
||||
|
||||
const items = [createMockQueueItem({ item_id: 1 })];
|
||||
|
||||
await api.onItemsChangedEvent(items);
|
||||
|
||||
const progressData = api.$progressData.get();
|
||||
expect(progressData[999]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should clear progress data for canceled/failed items', async () => {
|
||||
api.$progressData.setKey(1, {
|
||||
itemId: 1,
|
||||
progressEvent: createMockProgressEvent({ item_id: 1 }),
|
||||
progressImage: null,
|
||||
imageDTO: createMockImageDTO(),
|
||||
});
|
||||
|
||||
const items = [createMockQueueItem({ item_id: 1, status: 'canceled' })];
|
||||
|
||||
await api.onItemsChangedEvent(items);
|
||||
|
||||
const progressData = api.$progressData.get();
|
||||
expect(progressData[1]?.progressEvent).toBe(null);
|
||||
expect(progressData[1]?.progressImage).toBe(null);
|
||||
expect(progressData[1]?.imageDTO).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Auto Switch', () => {
|
||||
it('should set auto switch mode', () => {
|
||||
api.setAutoSwitch('switch_on_finish');
|
||||
expect(mockApp.onAutoSwitchChange).toHaveBeenCalledWith('switch_on_finish');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Utility Methods', () => {
|
||||
it('should build isSelected computed correctly', () => {
|
||||
const isSelected = api.buildIsSelectedComputed(1);
|
||||
expect(isSelected.get()).toBe(false);
|
||||
|
||||
api.$selectedItemId.set(1);
|
||||
expect(isSelected.get()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cleanup', () => {
|
||||
it('should reset all state on cleanup', () => {
|
||||
api.$selectedItemId.set(1);
|
||||
api.$items.set([createMockQueueItem({ item_id: 1 })]);
|
||||
api.$lastStartedItemId.set(1);
|
||||
api.$lastCompletedItemId.set(1);
|
||||
api.$progressData.setKey(1, {
|
||||
itemId: 1,
|
||||
progressEvent: null,
|
||||
progressImage: null,
|
||||
imageDTO: null,
|
||||
});
|
||||
|
||||
api.cleanup();
|
||||
|
||||
expect(api.$selectedItemId.get()).toBe(null);
|
||||
expect(api.$items.get()).toEqual([]);
|
||||
expect(api.$lastStartedItemId.get()).toBe(null);
|
||||
expect(api.$lastCompletedItemId.get()).toBe(null);
|
||||
expect(api.$progressData.get()).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
describe('Selection with Empty or Single Item Lists', () => {
|
||||
it('should handle selection operations with single item', () => {
|
||||
const items = [createMockQueueItem({ item_id: 1 })];
|
||||
api.$items.set(items);
|
||||
api.$selectedItemId.set(1);
|
||||
|
||||
// Navigation should wrap around to the same item
|
||||
api.selectNext();
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
|
||||
api.selectPrev();
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle selection operations with empty list', () => {
|
||||
api.$items.set([]);
|
||||
|
||||
api.selectFirst();
|
||||
api.selectLast();
|
||||
api.selectNext();
|
||||
api.selectPrev();
|
||||
|
||||
expect(api.$selectedItemId.get()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Progress Data Edge Cases', () => {
|
||||
it('should handle progress updates with image data', () => {
|
||||
const progressEvent = createMockProgressEvent({
|
||||
item_id: 1,
|
||||
destination: sessionId,
|
||||
image: { width: 512, height: 512, dataURL: 'foo' },
|
||||
});
|
||||
|
||||
api.onInvocationProgressEvent(progressEvent);
|
||||
|
||||
const progressData = api.$progressData.get();
|
||||
expect(progressData[1]?.progressImage).toBe(progressEvent.image);
|
||||
expect(progressData[1]?.progressEvent).toBe(progressEvent);
|
||||
});
|
||||
|
||||
it('should preserve imageDTO when updating progress', () => {
|
||||
const imageDTO = createMockImageDTO();
|
||||
api.$progressData.setKey(1, {
|
||||
itemId: 1,
|
||||
progressEvent: null,
|
||||
progressImage: null,
|
||||
imageDTO,
|
||||
});
|
||||
|
||||
const progressEvent = createMockProgressEvent({
|
||||
item_id: 1,
|
||||
destination: sessionId,
|
||||
});
|
||||
|
||||
api.onInvocationProgressEvent(progressEvent);
|
||||
|
||||
const progressData = api.$progressData.get();
|
||||
expect(progressData[1]?.imageDTO).toBe(imageDTO);
|
||||
expect(progressData[1]?.progressEvent).toBe(progressEvent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Auto-Switch Edge Cases', () => {
|
||||
it('should handle auto-switch when item is not in current items list', async () => {
|
||||
mockApp._setAutoSwitchMode('switch_on_start');
|
||||
api.$lastStartedItemId.set(999); // Non-existent item
|
||||
|
||||
const items = [createMockQueueItem({ item_id: 1 })];
|
||||
await api.onItemsChangedEvent(items);
|
||||
|
||||
// Should not switch to non-existent item
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
expect(api.$lastStartedItemId.get()).toBe(999);
|
||||
});
|
||||
|
||||
it('should handle auto-switch on finish when image loading fails', async () => {
|
||||
mockApp._setAutoSwitchMode('switch_on_finish');
|
||||
api.$lastCompletedItemId.set(1);
|
||||
|
||||
// Mock image loading failure
|
||||
mockApp._setImageDTO('test-image.png', null);
|
||||
|
||||
const items = [
|
||||
createMockQueueItem({
|
||||
item_id: 1,
|
||||
status: 'completed',
|
||||
session: {
|
||||
id: sessionId,
|
||||
source_prepared_mapping: { canvas_output: ['test-node-id'] },
|
||||
results: {
|
||||
'test-node-id': {
|
||||
type: 'image_output',
|
||||
image: { image_name: 'test-image.png' },
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
await api.onItemsChangedEvent(items);
|
||||
|
||||
// Should not switch when image loading fails
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
expect(api.$lastCompletedItemId.get()).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle auto-switch on finish with slow image loading', async () => {
|
||||
mockApp._setAutoSwitchMode('switch_on_finish');
|
||||
api.$lastCompletedItemId.set(1);
|
||||
|
||||
const imageDTO = createMockImageDTO({ image_name: 'test-image.png' });
|
||||
mockApp._setImageDTO('test-image.png', imageDTO);
|
||||
mockApp._setLoadImageDelay(50); // Add delay to image loading
|
||||
|
||||
const items = [
|
||||
createMockQueueItem({
|
||||
item_id: 1,
|
||||
status: 'completed',
|
||||
session: {
|
||||
id: sessionId,
|
||||
source_prepared_mapping: { canvas_output: ['test-node-id'] },
|
||||
results: { 'test-node-id': { image: { image_name: 'test-image.png' } } },
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
await api.onItemsChangedEvent(items);
|
||||
|
||||
// Should switch after image loads - wait for both the delay and promise resolution
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 150);
|
||||
});
|
||||
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
// The lastCompletedItemId should be reset after the loadImage promise resolves
|
||||
expect(api.$lastCompletedItemId.get()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Concurrent Operations', () => {
|
||||
it('should handle rapid item changes', async () => {
|
||||
const items1 = [createMockQueueItem({ item_id: 1 })];
|
||||
const items2 = [createMockQueueItem({ item_id: 2 })];
|
||||
|
||||
// Fire multiple events rapidly
|
||||
const promise1 = api.onItemsChangedEvent(items1);
|
||||
const promise2 = api.onItemsChangedEvent(items2);
|
||||
|
||||
await Promise.all([promise1, promise2]);
|
||||
|
||||
// Should end up with the last set of items
|
||||
expect(api.$items.get()).toBe(items2);
|
||||
// The selectedItemId retains the old value (1) but $selectedItem will be null
|
||||
// because item 1 is no longer in the items list
|
||||
expect(api.$selectedItemId.get()).toBe(1);
|
||||
expect(api.$selectedItem.get()).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle multiple progress events for same item', () => {
|
||||
const event1 = createMockProgressEvent({
|
||||
item_id: 1,
|
||||
destination: sessionId,
|
||||
percentage: 0.3,
|
||||
});
|
||||
const event2 = createMockProgressEvent({
|
||||
item_id: 1,
|
||||
destination: sessionId,
|
||||
percentage: 0.7,
|
||||
});
|
||||
|
||||
api.onInvocationProgressEvent(event1);
|
||||
api.onInvocationProgressEvent(event2);
|
||||
|
||||
const progressData = api.$progressData.get();
|
||||
expect(progressData[1]?.progressEvent).toBe(event2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Memory Management', () => {
|
||||
it('should clean up progress data for large number of items', async () => {
|
||||
// Create progress data for many items
|
||||
for (let i = 1; i <= 1000; i++) {
|
||||
api.$progressData.setKey(i, {
|
||||
itemId: i,
|
||||
progressEvent: null,
|
||||
progressImage: null,
|
||||
imageDTO: null,
|
||||
});
|
||||
}
|
||||
|
||||
// Only keep a few items
|
||||
const items = [createMockQueueItem({ item_id: 1 }), createMockQueueItem({ item_id: 2 })];
|
||||
|
||||
await api.onItemsChangedEvent(items);
|
||||
|
||||
const progressData = api.$progressData.get();
|
||||
const progressDataKeys = Object.keys(progressData);
|
||||
|
||||
// Should only have progress data for current items
|
||||
expect(progressDataKeys.length).toBeLessThanOrEqual(2);
|
||||
expect(progressData[1]).toBeDefined();
|
||||
expect(progressData[2]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event Subscription Management', () => {
|
||||
it('should handle multiple subscriptions and unsubscriptions', () => {
|
||||
const api2 = new StagingAreaApi(sessionId, mockApp);
|
||||
const api3 = new StagingAreaApi(sessionId, mockApp);
|
||||
|
||||
// All should be subscribed
|
||||
expect(mockApp.onItemsChanged).toHaveBeenCalledTimes(3);
|
||||
|
||||
api2.cleanup();
|
||||
api3.cleanup();
|
||||
|
||||
// Should not affect original api
|
||||
expect(api.$items.get()).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle events after cleanup', () => {
|
||||
api.cleanup();
|
||||
|
||||
// These should not crash
|
||||
const progressEvent = createMockProgressEvent({
|
||||
item_id: 1,
|
||||
destination: sessionId,
|
||||
});
|
||||
|
||||
api.onInvocationProgressEvent(progressEvent);
|
||||
|
||||
// State should remain clean - but the event handler still works
|
||||
// so it will add progress data even after cleanup
|
||||
const progressData = api.$progressData.get();
|
||||
expect(progressData[1]).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user