mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-03 10:55:47 +00:00
591 lines
No EOL
21 KiB
TypeScript
591 lines
No EOL
21 KiB
TypeScript
import { imageToolHandler, buildSwiftCliArgs, ImageToolInput } from '../../../src/tools/image';
|
|
import { executeSwiftCli, readImageAsBase64 } from '../../../src/utils/peekaboo-cli';
|
|
import { mockSwiftCli } from '../../mocks/peekaboo-cli.mock';
|
|
import { pino } from 'pino';
|
|
import { SavedFile, ImageCaptureData } from '../../../src/types';
|
|
|
|
// Mock the Swift CLI utility
|
|
jest.mock('../../../src/utils/peekaboo-cli', () => ({
|
|
executeSwiftCli: jest.fn(),
|
|
readImageAsBase64: jest.fn()
|
|
}));
|
|
|
|
const mockExecuteSwiftCli = executeSwiftCli as jest.MockedFunction<typeof executeSwiftCli>;
|
|
const mockReadImageAsBase64 = readImageAsBase64 as jest.MockedFunction<typeof readImageAsBase64>;
|
|
|
|
// Create a mock logger for tests
|
|
const mockLogger = pino({ level: 'silent' });
|
|
const mockContext = { logger: mockLogger };
|
|
|
|
describe('Image Tool', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('imageToolHandler', () => {
|
|
it('should capture screen with minimal parameters', async () => {
|
|
const mockResponse = mockSwiftCli.captureImage('screen');
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.content[0].type).toBe('text');
|
|
expect(result.content[0].text).toContain('Captured');
|
|
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
|
expect.arrayContaining(['image', '--mode', 'screen']),
|
|
mockLogger
|
|
);
|
|
});
|
|
|
|
it('should capture window with app parameter', async () => {
|
|
const mockResponse = mockSwiftCli.captureImage('window');
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
app: 'Safari',
|
|
mode: 'window',
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.content[0].type).toBe('text');
|
|
expect(result.content[0].text).toContain('Captured');
|
|
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
|
expect.arrayContaining(['image', '--app', 'Safari', '--mode', 'window']),
|
|
mockLogger
|
|
);
|
|
});
|
|
|
|
it('should handle specific format and options', async () => {
|
|
const mockResponse = mockSwiftCli.captureImage('screen');
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'jpg',
|
|
return_data: true,
|
|
capture_focus: 'foreground',
|
|
path: '/tmp/custom'
|
|
}, mockContext);
|
|
|
|
expect(result.content[0].type).toBe('text');
|
|
expect(result.content[0].text).toContain('Captured');
|
|
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
|
expect.arrayContaining(['image', '--path', '/tmp/custom', '--mode', 'screen', '--format', 'jpg', '--capture-focus', 'foreground']),
|
|
mockLogger
|
|
);
|
|
});
|
|
|
|
it('should handle Swift CLI errors', async () => {
|
|
const mockResponse = {
|
|
success: false,
|
|
error: {
|
|
message: 'Permission denied',
|
|
code: 'PERMISSION_DENIED'
|
|
}
|
|
};
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.content[0].text).toContain('Image capture failed');
|
|
expect(result.isError).toBe(true);
|
|
});
|
|
|
|
it('should handle unexpected errors', async () => {
|
|
mockExecuteSwiftCli.mockRejectedValue(new Error('Unexpected error'));
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.content[0].text).toContain('Unexpected error');
|
|
expect(result.isError).toBe(true);
|
|
});
|
|
|
|
it('should return image data when return_data is true and readImageAsBase64 succeeds', async () => {
|
|
const mockSavedFile: SavedFile = { path: '/tmp/test.png', mime_type: 'image/png', item_label: 'Screen 1' };
|
|
const mockCaptureData: ImageCaptureData = { saved_files: [mockSavedFile] };
|
|
const mockCliResponse = { success: true, data: mockCaptureData, messages: ['Captured one file'] };
|
|
mockExecuteSwiftCli.mockResolvedValue(mockCliResponse);
|
|
mockReadImageAsBase64.mockResolvedValue('base64imagedata');
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: true,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.isError).toBeUndefined(); // Should not be an error response
|
|
expect(result.content).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ type: 'text', text: expect.stringContaining('Captured 1 image') }),
|
|
expect.objectContaining({ type: 'text', text: 'Messages: Captured one file' }),
|
|
expect.objectContaining({
|
|
type: 'image',
|
|
data: 'base64imagedata',
|
|
mimeType: 'image/png',
|
|
metadata: expect.objectContaining({ source_path: '/tmp/test.png' })
|
|
})
|
|
]));
|
|
expect(mockReadImageAsBase64).toHaveBeenCalledWith('/tmp/test.png');
|
|
expect(result.saved_files).toEqual([mockSavedFile]);
|
|
});
|
|
|
|
it('should include messages from Swift CLI in the output', async () => {
|
|
const mockCliResponse = {
|
|
success: true,
|
|
data: { saved_files: [{ path: '/tmp/msg.png', mime_type: 'image/png' }] },
|
|
messages: ['Test message 1', 'Another message']
|
|
};
|
|
mockExecuteSwiftCli.mockResolvedValue(mockCliResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.content).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ type: 'text', text: expect.stringContaining('Messages: Test message 1; Another message') })
|
|
]));
|
|
});
|
|
|
|
it('should handle error from readImageAsBase64 and still return summary', async () => {
|
|
const mockSavedFile: SavedFile = { path: '/tmp/fail.png', mime_type: 'image/png' };
|
|
const mockCaptureData: ImageCaptureData = { saved_files: [mockSavedFile] };
|
|
const mockCliResponse = { success: true, data: mockCaptureData };
|
|
mockExecuteSwiftCli.mockResolvedValue(mockCliResponse);
|
|
mockReadImageAsBase64.mockRejectedValue(new Error('Read failed'));
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: true,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(result.content).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ type: 'text', text: expect.stringContaining('Captured 1 image') }),
|
|
expect.objectContaining({ type: 'text', text: 'Warning: Could not read image data from /tmp/fail.png' })
|
|
]));
|
|
expect(result.saved_files).toEqual([mockSavedFile]);
|
|
});
|
|
});
|
|
|
|
describe('buildSwiftCliArgs', () => {
|
|
const defaults = { format: 'png' as const, return_data: false, capture_focus: 'background' as const };
|
|
|
|
it('should default to screen mode if no app provided and no mode specified', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults });
|
|
expect(args).toEqual(['image', '--mode', 'screen', '--format', 'png', '--capture-focus', 'background']);
|
|
});
|
|
|
|
it('should default to window mode if app is provided and no mode specified', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults, app: 'Safari' });
|
|
expect(args).toEqual(['image', '--app', 'Safari', '--mode', 'window', '--format', 'png', '--capture-focus', 'background']);
|
|
});
|
|
|
|
it('should use specified mode: screen', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults, mode: 'screen' });
|
|
expect(args).toEqual(expect.arrayContaining(['--mode', 'screen']));
|
|
});
|
|
|
|
it('should use specified mode: window with app', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults, app: 'Terminal', mode: 'window' });
|
|
expect(args).toEqual(expect.arrayContaining(['--app', 'Terminal', '--mode', 'window']));
|
|
});
|
|
|
|
it('should use specified mode: multi with app', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults, app: 'Finder', mode: 'multi' });
|
|
expect(args).toEqual(expect.arrayContaining(['--app', 'Finder', '--mode', 'multi']));
|
|
});
|
|
|
|
it('should include app', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults, app: 'Notes' });
|
|
expect(args).toEqual(expect.arrayContaining(['--app', 'Notes']));
|
|
});
|
|
|
|
it('should include path', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults, path: '/tmp/image.jpg' });
|
|
expect(args).toEqual(expect.arrayContaining(['--path', '/tmp/image.jpg']));
|
|
});
|
|
|
|
it('should include window_specifier by title', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults, app: 'Safari', window_specifier: { title: 'Apple' } });
|
|
expect(args).toEqual(expect.arrayContaining(['--window-title', 'Apple']));
|
|
});
|
|
|
|
it('should include window_specifier by index', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults, app: 'Safari', window_specifier: { index: 0 } });
|
|
expect(args).toEqual(expect.arrayContaining(['--window-index', '0']));
|
|
});
|
|
|
|
it('should include format (default png)', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults });
|
|
expect(args).toEqual(expect.arrayContaining(['--format', 'png']));
|
|
});
|
|
|
|
it('should include specified format jpg', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults, format: 'jpg' });
|
|
expect(args).toEqual(expect.arrayContaining(['--format', 'jpg']));
|
|
});
|
|
|
|
it('should include capture_focus (default background)', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults });
|
|
expect(args).toEqual(expect.arrayContaining(['--capture-focus', 'background']));
|
|
});
|
|
|
|
it('should include specified capture_focus foreground', () => {
|
|
const args = buildSwiftCliArgs({ ...defaults, capture_focus: 'foreground' });
|
|
expect(args).toEqual(expect.arrayContaining(['--capture-focus', 'foreground']));
|
|
});
|
|
|
|
it('should handle all options together', () => {
|
|
const input: ImageToolInput = {
|
|
...defaults, // Ensure all required fields are present
|
|
app: 'Preview',
|
|
path: '/users/test/file.tiff',
|
|
mode: 'window',
|
|
window_specifier: { index: 1 },
|
|
format: 'png',
|
|
capture_focus: 'foreground'
|
|
};
|
|
const args = buildSwiftCliArgs(input);
|
|
expect(args).toEqual([
|
|
'image',
|
|
'--app', 'Preview',
|
|
'--path', '/users/test/file.tiff',
|
|
'--mode', 'window',
|
|
'--window-index', '1',
|
|
'--format', 'png',
|
|
'--capture-focus', 'foreground'
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases and Error Handling', () => {
|
|
it('should handle window capture with window_specifier index', async () => {
|
|
const mockResponse = mockSwiftCli.captureImage('window');
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
app: 'Safari',
|
|
window_specifier: { index: 0 },
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
|
expect.arrayContaining(['--window-index', '0']),
|
|
mockLogger
|
|
);
|
|
});
|
|
|
|
it('should handle all supported image formats', async () => {
|
|
const formats: Array<'png' | 'jpg'> = ['png', 'jpg'];
|
|
|
|
for (const format of formats) {
|
|
const mockResponse = mockSwiftCli.captureImage('screen');
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
format,
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
|
expect.arrayContaining(['--format', format]),
|
|
mockLogger
|
|
);
|
|
}
|
|
});
|
|
|
|
it('should handle window capture with window_specifier title', async () => {
|
|
const mockResponse = mockSwiftCli.captureImage('window');
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
app: 'Safari',
|
|
window_specifier: { title: 'Google' },
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
|
expect.arrayContaining(['--window-title', 'Google']),
|
|
mockLogger
|
|
);
|
|
});
|
|
|
|
it('should handle very long app names', async () => {
|
|
const longAppName = 'A'.repeat(256);
|
|
const mockResponse = mockSwiftCli.captureImage('window');
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
app: longAppName,
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
|
expect.arrayContaining(['--app', longAppName]),
|
|
mockLogger
|
|
);
|
|
});
|
|
|
|
it('should handle special characters in app names', async () => {
|
|
const specialAppName = 'App with Spaces & Special-Chars (1)';
|
|
const mockResponse = mockSwiftCli.captureImage('window');
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
app: specialAppName,
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
|
expect.arrayContaining(['--app', specialAppName]),
|
|
mockLogger
|
|
);
|
|
});
|
|
|
|
it('should handle paths with spaces and special characters', async () => {
|
|
const specialPath = '/Users/Test User/Documents/Screen Captures (2024)/';
|
|
const mockResponse = mockSwiftCli.captureImage('screen');
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
path: specialPath,
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
|
expect.arrayContaining(['--path', specialPath]),
|
|
mockLogger
|
|
);
|
|
});
|
|
|
|
it('should handle empty saved_files array', async () => {
|
|
const mockResponse = {
|
|
success: true,
|
|
data: { saved_files: [] }
|
|
};
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.content[0].text).toContain('Captured 0 images');
|
|
expect(result.saved_files).toEqual([]);
|
|
});
|
|
|
|
it('should handle malformed Swift CLI response', async () => {
|
|
const mockResponse = {
|
|
success: true,
|
|
data: null // Invalid data
|
|
};
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse as any);
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Invalid response from image capture');
|
|
});
|
|
|
|
it('should handle multiple saved files', async () => {
|
|
const mockFiles: SavedFile[] = [
|
|
{ path: '/tmp/screen1.png', mime_type: 'image/png', item_label: 'Screen 1' },
|
|
{ path: '/tmp/screen2.png', mime_type: 'image/png', item_label: 'Screen 2' },
|
|
{ path: '/tmp/screen3.png', mime_type: 'image/png', item_label: 'Screen 3' }
|
|
];
|
|
const mockResponse = {
|
|
success: true,
|
|
data: { saved_files: mockFiles }
|
|
};
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.content[0].text).toContain('Captured 3 images');
|
|
expect(result.content[0].text).toContain('Screen 1');
|
|
expect(result.content[0].text).toContain('Screen 2');
|
|
expect(result.content[0].text).toContain('Screen 3');
|
|
expect(result.saved_files).toEqual(mockFiles);
|
|
});
|
|
|
|
it('should handle return_data with multiple files', async () => {
|
|
const mockFiles: SavedFile[] = [
|
|
{ path: '/tmp/img1.png', mime_type: 'image/png', item_label: 'Image 1' },
|
|
{ path: '/tmp/img2.jpg', mime_type: 'image/jpeg', item_label: 'Image 2' }
|
|
];
|
|
const mockResponse = {
|
|
success: true,
|
|
data: { saved_files: mockFiles }
|
|
};
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
mockReadImageAsBase64
|
|
.mockResolvedValueOnce('base64data1')
|
|
.mockResolvedValueOnce('base64data2');
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: true,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.content).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
type: 'image',
|
|
data: 'base64data1',
|
|
mimeType: 'image/png'
|
|
}),
|
|
expect.objectContaining({
|
|
type: 'image',
|
|
data: 'base64data2',
|
|
mimeType: 'image/jpeg'
|
|
})
|
|
]));
|
|
expect(mockReadImageAsBase64).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should handle partial failures when reading multiple images', async () => {
|
|
const mockFiles: SavedFile[] = [
|
|
{ path: '/tmp/img1.png', mime_type: 'image/png', item_label: 'Image 1' },
|
|
{ path: '/tmp/img2.jpg', mime_type: 'image/jpeg', item_label: 'Image 2' }
|
|
];
|
|
const mockResponse = {
|
|
success: true,
|
|
data: { saved_files: mockFiles }
|
|
};
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
mockReadImageAsBase64
|
|
.mockResolvedValueOnce('base64data1')
|
|
.mockRejectedValueOnce(new Error('Read failed'));
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: true,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.content).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
type: 'image',
|
|
data: 'base64data1'
|
|
}),
|
|
expect.objectContaining({
|
|
type: 'text',
|
|
text: 'Warning: Could not read image data from /tmp/img2.jpg'
|
|
})
|
|
]));
|
|
});
|
|
|
|
it('should handle Swift CLI timeout errors', async () => {
|
|
const mockResponse = {
|
|
success: false,
|
|
error: {
|
|
message: 'Command timed out after 30 seconds',
|
|
code: 'TIMEOUT'
|
|
}
|
|
};
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Command timed out');
|
|
expect(result._meta.backend_error_code).toBe('TIMEOUT');
|
|
});
|
|
|
|
it('should handle window_specifier with both title and index', async () => {
|
|
const mockResponse = mockSwiftCli.captureImage('window');
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const args = buildSwiftCliArgs({
|
|
app: 'Safari',
|
|
window_specifier: { title: 'Google', index: 1 },
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
});
|
|
|
|
// Should only include the first one (title takes precedence in implementation)
|
|
expect(args).toContain('--window-title');
|
|
expect(args).toContain('Google');
|
|
expect(args).not.toContain('--window-index');
|
|
});
|
|
|
|
it('should handle negative window index', async () => {
|
|
const args = buildSwiftCliArgs({
|
|
app: 'Safari',
|
|
window_specifier: { index: -1 },
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
});
|
|
|
|
expect(args).toContain('--window-index');
|
|
expect(args).toContain('-1');
|
|
});
|
|
|
|
it('should handle very large window index', async () => {
|
|
const largeWindowIndex = 999999;
|
|
const mockResponse = mockSwiftCli.captureImage('window');
|
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
|
|
|
const result = await imageToolHandler({
|
|
app: 'Safari',
|
|
window_specifier: { index: largeWindowIndex },
|
|
format: 'png',
|
|
return_data: false,
|
|
capture_focus: 'background'
|
|
}, mockContext);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
|
expect.arrayContaining(['--window-index', largeWindowIndex.toString()]),
|
|
mockLogger
|
|
);
|
|
});
|
|
});
|
|
});
|