Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/imageCarousel/test/browser/imageCarousel.contribution.test.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import { URI } from '../../../../../base/common/uri.js';
8
import { VSBuffer } from '../../../../../base/common/buffer.js';
9
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
10
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
11
import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js';
12
import { NullFilesConfigurationService, createFileStat } from '../../../../test/common/workbenchTestServices.js';
13
import { IExplorerService } from '../../../files/browser/files.js';
14
import { ExplorerItem } from '../../../files/common/explorerModel.js';
15
import { IFileService, IFileStat, IFileContent } from '../../../../../platform/files/common/files.js';
16
import { IEditorService, MODAL_GROUP } from '../../../../services/editor/common/editorService.js';
17
import { ImageCarouselEditorInput } from '../../browser/imageCarouselEditorInput.js';
18
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
19
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
20
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
21
22
// Importing the contribution registers the actions
23
import '../../browser/imageCarousel.contribution.js';
24
25
function createExplorerItem(
26
path: string,
27
isFolder: boolean,
28
fileService: IFileService,
29
configService: TestConfigurationService,
30
parent?: ExplorerItem,
31
): ExplorerItem {
32
return new ExplorerItem(
33
URI.file(path),
34
fileService,
35
configService,
36
NullFilesConfigurationService,
37
parent,
38
isFolder,
39
);
40
}
41
42
suite('OpenImagesInCarouselFromExplorerAction', () => {
43
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
44
45
let instantiationService: TestInstantiationService;
46
let configService: TestConfigurationService;
47
let openedInputs: { input: ImageCarouselEditorInput; group: typeof MODAL_GROUP }[];
48
let infoMessages: string[];
49
let errorMessages: string[];
50
51
setup(() => {
52
openedInputs = [];
53
infoMessages = [];
54
errorMessages = [];
55
configService = new TestConfigurationService();
56
instantiationService = workbenchInstantiationService(undefined, disposables);
57
});
58
59
function stubFileService(resolveMap: Map<string, IFileStat>, fileContents: Map<string, VSBuffer>): void {
60
instantiationService.stub(IFileService, 'resolve', async (resource: URI) => {
61
const stat = resolveMap.get(resource.path);
62
if (!stat) {
63
throw new Error(`File not found: ${resource.path}`);
64
}
65
return stat;
66
});
67
68
instantiationService.stub(IFileService, 'readFile', async (resource: URI) => {
69
const content = fileContents.get(resource.path);
70
if (!content) {
71
throw new Error(`Cannot read: ${resource.path}`);
72
}
73
return { resource, value: content } as IFileContent;
74
});
75
}
76
77
function stubExplorerService(items: ExplorerItem[]): void {
78
instantiationService.stub(IExplorerService, {
79
getContext: () => items,
80
});
81
}
82
83
function stubEditorService(): void {
84
instantiationService.stub(IEditorService, 'openEditor', async (input: unknown, _options: unknown, group: unknown) => {
85
if (input instanceof ImageCarouselEditorInput) {
86
openedInputs.push({ input, group: group as typeof MODAL_GROUP });
87
disposables.add(input);
88
}
89
return undefined;
90
});
91
}
92
93
function stubNotificationService(): void {
94
instantiationService.stub(INotificationService, 'info', (message: string) => {
95
infoMessages.push(message);
96
});
97
instantiationService.stub(INotificationService, 'error', (message: string) => {
98
errorMessages.push(message);
99
});
100
}
101
102
test('single image file opens carousel with sibling images', async () => {
103
const fileService = instantiationService.get(IFileService);
104
const parent = createExplorerItem('/workspace/images', true, fileService, configService);
105
const imageItem = createExplorerItem('/workspace/images/photo.png', false, fileService, configService, parent);
106
107
const pngData = VSBuffer.fromString('fake-png');
108
const jpgData = VSBuffer.fromString('fake-jpg');
109
const txtData = VSBuffer.fromString('text file');
110
111
const resolveMap = new Map<string, IFileStat>();
112
resolveMap.set('/workspace/images', createFileStat(
113
URI.file('/workspace/images'), false, false, true, false, [
114
{ resource: URI.file('/workspace/images/photo.png'), isFile: true },
115
{ resource: URI.file('/workspace/images/other.jpg'), isFile: true },
116
{ resource: URI.file('/workspace/images/readme.txt'), isFile: true },
117
{ resource: URI.file('/workspace/images/subfolder'), isDirectory: true, isFile: false },
118
]
119
));
120
121
const fileContents = new Map<string, VSBuffer>();
122
fileContents.set('/workspace/images/photo.png', pngData);
123
fileContents.set('/workspace/images/other.jpg', jpgData);
124
fileContents.set('/workspace/images/readme.txt', txtData);
125
126
stubFileService(resolveMap, fileContents);
127
stubExplorerService([imageItem]);
128
stubEditorService();
129
130
const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js');
131
const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel');
132
assert.ok(command, 'Command should be registered');
133
134
await instantiationService.invokeFunction(command.handler);
135
136
assert.strictEqual(openedInputs.length, 1, 'Should open one editor');
137
const input = openedInputs[0].input;
138
assert.strictEqual(input.collection.sections.length, 1);
139
140
const images = input.collection.sections[0].images;
141
assert.strictEqual(images.length, 2, 'Should include 2 image siblings (png + jpg), not txt');
142
// Images are sorted by basename: other.jpg before photo.png
143
assert.strictEqual(images[0].name, 'other.jpg');
144
assert.strictEqual(images[1].name, 'photo.png');
145
146
// Start index should be the selected image (photo.png = index 1 after sorting)
147
assert.strictEqual(input.startIndex, 1);
148
});
149
150
test('folder opens carousel with all contained images', async () => {
151
const fileService = instantiationService.get(IFileService);
152
const folderItem = createExplorerItem('/workspace/images', true, fileService, configService);
153
154
const gifData = VSBuffer.fromString('fake-gif');
155
const webpData = VSBuffer.fromString('fake-webp');
156
157
const resolveMap = new Map<string, IFileStat>();
158
resolveMap.set('/workspace/images', createFileStat(
159
URI.file('/workspace/images'), false, false, true, false, [
160
{ resource: URI.file('/workspace/images/anim.gif'), isFile: true },
161
{ resource: URI.file('/workspace/images/photo.webp'), isFile: true },
162
{ resource: URI.file('/workspace/images/script.js'), isFile: true },
163
]
164
));
165
166
const fileContents = new Map<string, VSBuffer>();
167
fileContents.set('/workspace/images/anim.gif', gifData);
168
fileContents.set('/workspace/images/photo.webp', webpData);
169
170
stubFileService(resolveMap, fileContents);
171
stubExplorerService([folderItem]);
172
stubEditorService();
173
174
const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js');
175
const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel');
176
assert.ok(command);
177
178
await instantiationService.invokeFunction(command.handler);
179
180
assert.strictEqual(openedInputs.length, 1);
181
const images = openedInputs[0].input.collection.sections[0].images;
182
assert.strictEqual(images.length, 2, 'Should include 2 images (gif + webp), not js');
183
assert.strictEqual(images[0].name, 'anim.gif');
184
assert.strictEqual(images[1].name, 'photo.webp');
185
});
186
187
test('multiple selected images open in carousel', async () => {
188
const fileService = instantiationService.get(IFileService);
189
const img1 = createExplorerItem('/workspace/a.png', false, fileService, configService);
190
const img2 = createExplorerItem('/workspace/b.svg', false, fileService, configService);
191
const txtFile = createExplorerItem('/workspace/notes.txt', false, fileService, configService);
192
193
const pngData = VSBuffer.fromString('fake-png');
194
const svgData = VSBuffer.fromString('<svg></svg>');
195
196
const resolveMap = new Map<string, IFileStat>();
197
198
const fileContents = new Map<string, VSBuffer>();
199
fileContents.set('/workspace/a.png', pngData);
200
fileContents.set('/workspace/b.svg', svgData);
201
202
stubFileService(resolveMap, fileContents);
203
stubExplorerService([img1, img2, txtFile]);
204
stubEditorService();
205
206
const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js');
207
const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel');
208
assert.ok(command);
209
210
await instantiationService.invokeFunction(command.handler);
211
212
assert.strictEqual(openedInputs.length, 1);
213
const images = openedInputs[0].input.collection.sections[0].images;
214
assert.strictEqual(images.length, 2, 'Should include only image files');
215
assert.strictEqual(images[0].name, 'a.png');
216
assert.strictEqual(images[1].name, 'b.svg');
217
});
218
219
test('empty selection with resource argument opens carousel from that folder', async () => {
220
const pngData = VSBuffer.fromString('fake-png');
221
const jpgData = VSBuffer.fromString('fake-jpg');
222
223
const folderUri = URI.file('/workspace/photos');
224
const resolveMap = new Map<string, IFileStat>();
225
resolveMap.set('/workspace/photos', createFileStat(
226
folderUri, false, false, true, false, [
227
{ resource: URI.file('/workspace/photos/sunset.png'), isFile: true },
228
{ resource: URI.file('/workspace/photos/mountain.jpg'), isFile: true },
229
{ resource: URI.file('/workspace/photos/notes.txt'), isFile: true },
230
]
231
));
232
233
const fileContents = new Map<string, VSBuffer>();
234
fileContents.set('/workspace/photos/sunset.png', pngData);
235
fileContents.set('/workspace/photos/mountain.jpg', jpgData);
236
237
stubFileService(resolveMap, fileContents);
238
stubExplorerService([]);
239
stubEditorService();
240
241
const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js');
242
const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel');
243
assert.ok(command);
244
245
// Pass the folder URI as the resource argument (as explorer does for empty-space click)
246
await instantiationService.invokeFunction(command.handler, folderUri);
247
248
assert.strictEqual(openedInputs.length, 1, 'Should open carousel using resource argument fallback');
249
const images = openedInputs[0].input.collection.sections[0].images;
250
assert.strictEqual(images.length, 2, 'Should include 2 images from the folder');
251
});
252
253
test('empty selection without resource falls back to first workspace folder', async () => {
254
const pngData = VSBuffer.fromString('fake-png');
255
256
// Derive the workspace root from IWorkspaceContextService so the test
257
// works on all platforms (the path differs on Windows vs Unix).
258
const contextService = instantiationService.get(IWorkspaceContextService);
259
const wsRoot = contextService.getWorkspace().folders[0].uri;
260
const logoUri = URI.joinPath(wsRoot, 'logo.png');
261
const readmeUri = URI.joinPath(wsRoot, 'readme.md');
262
263
const resolveMap = new Map<string, IFileStat>();
264
resolveMap.set(wsRoot.path, createFileStat(
265
wsRoot, false, false, true, false, [
266
{ resource: logoUri, isFile: true },
267
{ resource: readmeUri, isFile: true },
268
]
269
));
270
271
const fileContents = new Map<string, VSBuffer>();
272
fileContents.set(logoUri.path, pngData);
273
274
stubFileService(resolveMap, fileContents);
275
stubExplorerService([]);
276
stubEditorService();
277
278
const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js');
279
const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel');
280
assert.ok(command);
281
282
// No resource argument — should fall back to workspace root
283
await instantiationService.invokeFunction(command.handler);
284
285
assert.strictEqual(openedInputs.length, 1, 'Should open carousel using workspace root fallback');
286
const images = openedInputs[0].input.collection.sections[0].images;
287
assert.strictEqual(images.length, 1, 'Should include image from workspace root');
288
assert.strictEqual(images[0].name, 'logo.png');
289
});
290
291
test('empty selection with no images shows notification', async () => {
292
const folderUri = URI.file('/workspace/docs');
293
const resolveMap = new Map<string, IFileStat>();
294
resolveMap.set('/workspace/docs', createFileStat(
295
folderUri, false, false, true, false, [
296
{ resource: URI.file('/workspace/docs/readme.md'), isFile: true },
297
]
298
));
299
300
stubFileService(resolveMap, new Map());
301
stubExplorerService([]);
302
stubEditorService();
303
stubNotificationService();
304
305
const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js');
306
const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel');
307
assert.ok(command);
308
309
await instantiationService.invokeFunction(command.handler, folderUri);
310
311
assert.strictEqual(openedInputs.length, 0, 'Should not open carousel when folder has no images');
312
assert.strictEqual(infoMessages.length, 1, 'Should show notification');
313
});
314
315
test('folder with no images shows notification', async () => {
316
const fileService = instantiationService.get(IFileService);
317
const folderItem = createExplorerItem('/workspace/docs', true, fileService, configService);
318
319
const resolveMap = new Map<string, IFileStat>();
320
resolveMap.set('/workspace/docs', createFileStat(
321
URI.file('/workspace/docs'), false, false, true, false, [
322
{ resource: URI.file('/workspace/docs/readme.md'), isFile: true },
323
{ resource: URI.file('/workspace/docs/notes.txt'), isFile: true },
324
]
325
));
326
327
stubFileService(resolveMap, new Map());
328
stubExplorerService([folderItem]);
329
stubEditorService();
330
stubNotificationService();
331
332
const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js');
333
const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel');
334
assert.ok(command);
335
336
await instantiationService.invokeFunction(command.handler);
337
338
assert.strictEqual(openedInputs.length, 0, 'Should not open carousel when folder has no images');
339
assert.strictEqual(infoMessages.length, 1, 'Should show notification about no images');
340
});
341
342
test('folder read error shows error notification', async () => {
343
const fileService = instantiationService.get(IFileService);
344
const folderItem = createExplorerItem('/workspace/restricted', true, fileService, configService);
345
346
// resolve throws to simulate a permission error
347
const resolveMap = new Map<string, IFileStat>();
348
stubFileService(resolveMap, new Map());
349
stubExplorerService([folderItem]);
350
stubEditorService();
351
stubNotificationService();
352
353
const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js');
354
const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel');
355
assert.ok(command);
356
357
await instantiationService.invokeFunction(command.handler);
358
359
assert.strictEqual(openedInputs.length, 0, 'Should not open carousel on folder read error');
360
assert.strictEqual(errorMessages.length, 1, 'Should show error notification');
361
assert.strictEqual(infoMessages.length, 0, 'Should not show info notification');
362
});
363
364
test('images with URIs are passed lazily without reading file contents', async () => {
365
const folderUri = URI.file('/workspace/broken');
366
367
const resolveMap = new Map<string, IFileStat>();
368
resolveMap.set('/workspace/broken', createFileStat(
369
folderUri, false, false, true, false, [
370
{ resource: URI.file('/workspace/broken/corrupt.png'), isFile: true },
371
{ resource: URI.file('/workspace/broken/missing.jpg'), isFile: true },
372
]
373
));
374
375
// No file contents — with lazy loading, no readFile should be called at action time
376
let readFileCallCount = 0;
377
stubFileService(resolveMap, new Map());
378
instantiationService.stub(IFileService, 'readFile', async () => {
379
readFileCallCount++;
380
throw new Error('readFile should not be called');
381
});
382
stubExplorerService([]);
383
stubEditorService();
384
stubNotificationService();
385
386
const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js');
387
const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel');
388
assert.ok(command);
389
390
await instantiationService.invokeFunction(command.handler, folderUri);
391
392
assert.strictEqual(readFileCallCount, 0, 'readFile should not be called during action');
393
assert.strictEqual(openedInputs.length, 1, 'Should open carousel with lazy image entries');
394
const images = openedInputs[0].input.collection.sections[0].images;
395
assert.strictEqual(images.length, 2, 'Should include 2 lazy image entries');
396
assert.strictEqual(images[0].data, undefined, 'Image data should not be loaded eagerly');
397
assert.ok(images[0].uri, 'Image should have a URI for lazy loading');
398
});
399
400
test('folder includes video files alongside images', async () => {
401
const fileService = instantiationService.get(IFileService);
402
const folderItem = createExplorerItem('/workspace/media', true, fileService, configService);
403
404
const resolveMap = new Map<string, IFileStat>();
405
resolveMap.set('/workspace/media', createFileStat(
406
URI.file('/workspace/media'), false, false, true, false, [
407
{ resource: URI.file('/workspace/media/clip.mp4'), isFile: true },
408
{ resource: URI.file('/workspace/media/photo.png'), isFile: true },
409
{ resource: URI.file('/workspace/media/demo.webm'), isFile: true },
410
{ resource: URI.file('/workspace/media/intro.mov'), isFile: true },
411
{ resource: URI.file('/workspace/media/readme.txt'), isFile: true },
412
]
413
));
414
415
stubFileService(resolveMap, new Map());
416
stubExplorerService([folderItem]);
417
stubEditorService();
418
419
const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js');
420
const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel');
421
assert.ok(command);
422
423
await instantiationService.invokeFunction(command.handler);
424
425
assert.strictEqual(openedInputs.length, 1);
426
const images = openedInputs[0].input.collection.sections[0].images;
427
assert.strictEqual(images.length, 4, 'Should include mp4 + webm + mov + png, not txt');
428
assert.strictEqual(images[0].name, 'clip.mp4');
429
assert.strictEqual(images[1].name, 'demo.webm');
430
assert.strictEqual(images[2].name, 'intro.mov');
431
assert.strictEqual(images[3].name, 'photo.png');
432
});
433
434
test('single video file opens carousel with sibling media', async () => {
435
const fileService = instantiationService.get(IFileService);
436
const parent = createExplorerItem('/workspace/media', true, fileService, configService);
437
const videoItem = createExplorerItem('/workspace/media/clip.mp4', false, fileService, configService, parent);
438
439
const resolveMap = new Map<string, IFileStat>();
440
resolveMap.set('/workspace/media', createFileStat(
441
URI.file('/workspace/media'), false, false, true, false, [
442
{ resource: URI.file('/workspace/media/clip.mp4'), isFile: true },
443
{ resource: URI.file('/workspace/media/photo.png'), isFile: true },
444
{ resource: URI.file('/workspace/media/notes.txt'), isFile: true },
445
]
446
));
447
448
stubFileService(resolveMap, new Map());
449
stubExplorerService([videoItem]);
450
stubEditorService();
451
452
const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js');
453
const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel');
454
assert.ok(command);
455
456
await instantiationService.invokeFunction(command.handler);
457
458
assert.strictEqual(openedInputs.length, 1);
459
const input = openedInputs[0].input;
460
const images = input.collection.sections[0].images;
461
assert.strictEqual(images.length, 2, 'Should include mp4 + png siblings');
462
assert.strictEqual(images[0].name, 'clip.mp4');
463
assert.strictEqual(images[1].name, 'photo.png');
464
assert.strictEqual(input.startIndex, 0, 'Start index should point to the selected video');
465
});
466
});
467
468