@theia/getting-started 1.75.0-next.21 → 1.75.0-next.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/lib/browser/getting-started-contribution.d.ts +30 -2
  2. package/lib/browser/getting-started-contribution.d.ts.map +1 -1
  3. package/lib/browser/getting-started-contribution.js +120 -0
  4. package/lib/browser/getting-started-contribution.js.map +1 -1
  5. package/lib/browser/getting-started-contribution.spec.d.ts +2 -0
  6. package/lib/browser/getting-started-contribution.spec.d.ts.map +1 -0
  7. package/lib/browser/getting-started-contribution.spec.js +152 -0
  8. package/lib/browser/getting-started-contribution.spec.js.map +1 -0
  9. package/lib/browser/getting-started-frontend-module.d.ts.map +1 -1
  10. package/lib/browser/getting-started-frontend-module.js +8 -0
  11. package/lib/browser/getting-started-frontend-module.js.map +1 -1
  12. package/lib/browser/getting-started-widget.d.ts +19 -0
  13. package/lib/browser/getting-started-widget.d.ts.map +1 -1
  14. package/lib/browser/getting-started-widget.js +57 -2
  15. package/lib/browser/getting-started-widget.js.map +1 -1
  16. package/lib/browser/getting-started-widget.spec.d.ts +2 -0
  17. package/lib/browser/getting-started-widget.spec.d.ts.map +1 -0
  18. package/lib/browser/getting-started-widget.spec.js +71 -0
  19. package/lib/browser/getting-started-widget.spec.js.map +1 -0
  20. package/lib/browser/walkthrough-card.d.ts +8 -0
  21. package/lib/browser/walkthrough-card.d.ts.map +1 -0
  22. package/lib/browser/walkthrough-card.js +40 -0
  23. package/lib/browser/walkthrough-card.js.map +1 -0
  24. package/lib/browser/walkthrough-card.spec.d.ts +2 -0
  25. package/lib/browser/walkthrough-card.spec.d.ts.map +1 -0
  26. package/lib/browser/walkthrough-card.spec.js +188 -0
  27. package/lib/browser/walkthrough-card.spec.js.map +1 -0
  28. package/lib/browser/walkthrough-detail.d.ts +19 -0
  29. package/lib/browser/walkthrough-detail.d.ts.map +1 -0
  30. package/lib/browser/walkthrough-detail.js +210 -0
  31. package/lib/browser/walkthrough-detail.js.map +1 -0
  32. package/lib/browser/walkthrough-detail.spec.d.ts +2 -0
  33. package/lib/browser/walkthrough-detail.spec.d.ts.map +1 -0
  34. package/lib/browser/walkthrough-detail.spec.js +409 -0
  35. package/lib/browser/walkthrough-detail.spec.js.map +1 -0
  36. package/lib/browser/walkthrough-icon.d.ts +10 -0
  37. package/lib/browser/walkthrough-icon.d.ts.map +1 -0
  38. package/lib/browser/walkthrough-icon.js +37 -0
  39. package/lib/browser/walkthrough-icon.js.map +1 -0
  40. package/lib/browser/walkthrough-section.d.ts +23 -0
  41. package/lib/browser/walkthrough-section.d.ts.map +1 -0
  42. package/lib/browser/walkthrough-section.js +65 -0
  43. package/lib/browser/walkthrough-section.js.map +1 -0
  44. package/lib/browser/walkthrough-section.spec.d.ts +2 -0
  45. package/lib/browser/walkthrough-section.spec.d.ts.map +1 -0
  46. package/lib/browser/walkthrough-section.spec.js +328 -0
  47. package/lib/browser/walkthrough-section.spec.js.map +1 -0
  48. package/lib/browser/walkthrough-service.d.ts +166 -0
  49. package/lib/browser/walkthrough-service.d.ts.map +1 -0
  50. package/lib/browser/walkthrough-service.js +505 -0
  51. package/lib/browser/walkthrough-service.js.map +1 -0
  52. package/lib/browser/walkthrough-service.spec.d.ts +2 -0
  53. package/lib/browser/walkthrough-service.spec.d.ts.map +1 -0
  54. package/lib/browser/walkthrough-service.spec.js +1026 -0
  55. package/lib/browser/walkthrough-service.spec.js.map +1 -0
  56. package/lib/common/getting-started-preferences.d.ts +1 -0
  57. package/lib/common/getting-started-preferences.d.ts.map +1 -1
  58. package/lib/common/getting-started-preferences.js +5 -0
  59. package/lib/common/getting-started-preferences.js.map +1 -1
  60. package/lib/common/walkthrough-commands.d.ts +11 -0
  61. package/lib/common/walkthrough-commands.d.ts.map +1 -0
  62. package/lib/common/walkthrough-commands.js +41 -0
  63. package/lib/common/walkthrough-commands.js.map +1 -0
  64. package/lib/common/walkthrough-types.d.ts +22 -0
  65. package/lib/common/walkthrough-types.d.ts.map +1 -0
  66. package/lib/common/walkthrough-types.js +18 -0
  67. package/lib/common/walkthrough-types.js.map +1 -0
  68. package/package.json +8 -7
  69. package/src/browser/getting-started-contribution.spec.ts +188 -0
  70. package/src/browser/getting-started-contribution.ts +139 -3
  71. package/src/browser/getting-started-frontend-module.ts +8 -0
  72. package/src/browser/getting-started-widget.spec.tsx +88 -0
  73. package/src/browser/getting-started-widget.tsx +70 -3
  74. package/src/browser/style/index.css +279 -0
  75. package/src/browser/walkthrough-card.spec.tsx +247 -0
  76. package/src/browser/walkthrough-card.tsx +62 -0
  77. package/src/browser/walkthrough-detail.spec.tsx +675 -0
  78. package/src/browser/walkthrough-detail.tsx +347 -0
  79. package/src/browser/walkthrough-icon.tsx +42 -0
  80. package/src/browser/walkthrough-section.spec.tsx +484 -0
  81. package/src/browser/walkthrough-section.tsx +119 -0
  82. package/src/browser/walkthrough-service.spec.ts +1279 -0
  83. package/src/browser/walkthrough-service.ts +565 -0
  84. package/src/common/getting-started-preferences.ts +6 -0
  85. package/src/common/walkthrough-commands.ts +37 -0
  86. package/src/common/walkthrough-types.ts +39 -0
@@ -0,0 +1,484 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import * as assert from 'assert';
18
+ import * as React from 'react';
19
+ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
20
+
21
+ let disableJSDOM = enableJSDOM();
22
+
23
+ import { createRoot, Root } from 'react-dom/client';
24
+ import { Emitter } from '@theia/core/lib/common/event';
25
+ import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
26
+ import { ThemeService } from '@theia/core/lib/browser/theming';
27
+ import { Disposable } from '@theia/core/lib/common/disposable';
28
+ import { ILogger } from '@theia/core/lib/common/logger';
29
+ import { ThemeType } from '@theia/core/lib/common/theme';
30
+ import { Walkthrough, WalkthroughStep } from '../common/walkthrough-types';
31
+ import { WalkthroughService } from './walkthrough-service';
32
+ import { WALKTHROUGH_LIST_LIMIT, WalkthroughSection } from './walkthrough-section';
33
+
34
+ function createMockStep(overrides?: Partial<WalkthroughStep>): WalkthroughStep {
35
+ return {
36
+ id: 'step-1',
37
+ title: 'Test Step',
38
+ description: 'A test step description',
39
+ isComplete: false,
40
+ ...overrides
41
+ };
42
+ }
43
+
44
+ function createMockWalkthrough(overrides?: Partial<Walkthrough>): Walkthrough {
45
+ return {
46
+ id: 'test-walkthrough',
47
+ title: 'Test Walkthrough',
48
+ description: 'A test walkthrough description',
49
+ steps: [
50
+ createMockStep({ id: 'step-1', title: 'First Step' }),
51
+ createMockStep({ id: 'step-2', title: 'Second Step' })
52
+ ],
53
+ pluginId: 'test.plugin',
54
+ ...overrides
55
+ };
56
+ }
57
+
58
+ interface MockWalkthroughService extends Pick<WalkthroughService,
59
+ 'getWalkthroughs' | 'getWalkthrough' | 'onDidChangeWalkthroughs' | 'onDidChangeSelection' |
60
+ 'selectedWalkthrough' | 'selectedStep' | 'selectWalkthrough' | 'selectStep' | 'clearSelection' |
61
+ 'markStepComplete' | 'markStepIncomplete' | 'markAllStepsComplete' | 'resetProgress' | 'getStepProgress' | 'handleLinkClick'
62
+ > {
63
+ changeEmitter: Emitter<void>;
64
+ selectionEmitter: Emitter<void>;
65
+ }
66
+
67
+ /**
68
+ * Mirrors the selection handling of the real service: the selection is owned by the service, not by the component.
69
+ */
70
+ function createMockWalkthroughService(walkthroughs: Walkthrough[] = []): MockWalkthroughService {
71
+ const changeEmitter = new Emitter<void>();
72
+ const selectionEmitter = new Emitter<void>();
73
+ let selectedWalkthroughId: string | undefined;
74
+ let selectedStepId: string | undefined;
75
+ return {
76
+ changeEmitter,
77
+ selectionEmitter,
78
+ getWalkthroughs: () => [...walkthroughs],
79
+ getWalkthrough: (id: string) => walkthroughs.find(w => w.id === id),
80
+ onDidChangeWalkthroughs: changeEmitter.event,
81
+ onDidChangeSelection: selectionEmitter.event,
82
+ get selectedWalkthrough(): Walkthrough | undefined {
83
+ return walkthroughs.find(w => w.id === selectedWalkthroughId);
84
+ },
85
+ get selectedStep(): WalkthroughStep | undefined {
86
+ return this.selectedWalkthrough?.steps.find(s => s.id === selectedStepId);
87
+ },
88
+ selectWalkthrough: (id: string) => {
89
+ const walkthrough = walkthroughs.find(w => w.id === id);
90
+ if (walkthrough) {
91
+ selectedWalkthroughId = id;
92
+ selectedStepId = (walkthrough.steps.find(s => !s.isComplete) ?? walkthrough.steps[0])?.id;
93
+ selectionEmitter.fire();
94
+ }
95
+ },
96
+ selectStep: (stepId: string) => {
97
+ selectedStepId = stepId;
98
+ selectionEmitter.fire();
99
+ },
100
+ clearSelection: () => {
101
+ selectedWalkthroughId = undefined;
102
+ selectedStepId = undefined;
103
+ selectionEmitter.fire();
104
+ },
105
+ markStepComplete: () => Promise.resolve(),
106
+ markStepIncomplete: () => Promise.resolve(),
107
+ markAllStepsComplete: () => Promise.resolve(),
108
+ resetProgress: () => Promise.resolve(),
109
+ getStepProgress: () => ({ completed: 0, total: 0 }),
110
+ handleLinkClick: () => Promise.resolve()
111
+ };
112
+ }
113
+
114
+ function createMockMarkdownRenderer(): MarkdownRenderer {
115
+ return {
116
+ render: () => {
117
+ const div = document.createElement('div');
118
+ div.textContent = 'rendered markdown';
119
+ return { element: div, dispose: () => { } };
120
+ }
121
+ };
122
+ }
123
+
124
+ function createMockThemeService(themeType: ThemeType = 'dark'): ThemeService {
125
+ return {
126
+ getCurrentTheme: () => ({ id: 'test', label: 'Test', type: themeType }),
127
+ onDidColorThemeChange: () => Disposable.NULL
128
+ } as unknown as ThemeService;
129
+ }
130
+
131
+ function createMockLogger(): ILogger {
132
+ return { warn: () => { }, error: () => { }, info: () => { }, debug: () => { } } as unknown as ILogger;
133
+ }
134
+
135
+ describe('WalkthroughSection', () => {
136
+ let container: HTMLElement;
137
+ let root: Root;
138
+ let mockRenderer: MarkdownRenderer;
139
+
140
+ before(() => disableJSDOM = enableJSDOM());
141
+ after(() => disableJSDOM());
142
+
143
+ beforeEach(() => {
144
+ container = document.createElement('div');
145
+ document.body.appendChild(container);
146
+ root = createRoot(container);
147
+ mockRenderer = createMockMarkdownRenderer();
148
+ });
149
+
150
+ afterEach(() => {
151
+ root.unmount();
152
+ document.body.removeChild(container);
153
+ });
154
+
155
+ it('should render nothing when getWalkthroughs returns empty array', done => {
156
+ const mockService = createMockWalkthroughService([]);
157
+
158
+ root.render(
159
+ <WalkthroughSection
160
+ walkthroughService={mockService as unknown as WalkthroughService}
161
+ markdownRenderer={mockRenderer}
162
+ themeService={createMockThemeService()}
163
+ logger={createMockLogger()}
164
+ />
165
+ );
166
+
167
+ setTimeout(() => {
168
+ const cards = container.querySelectorAll('.gs-walkthrough-card');
169
+ assert.strictEqual(cards.length, 0, 'Should have no walkthrough cards');
170
+
171
+ const section = container.querySelector('.gs-section');
172
+ // eslint-disable-next-line no-null/no-null
173
+ assert.strictEqual(section, null, 'Should not render the section');
174
+ done();
175
+ }, 50);
176
+ });
177
+
178
+ it('should render walkthrough cards when walkthroughs exist', done => {
179
+ const walkthroughs = [
180
+ createMockWalkthrough({ id: 'wt1', title: 'Walkthrough One' }),
181
+ createMockWalkthrough({ id: 'wt2', title: 'Walkthrough Two' })
182
+ ];
183
+ const mockService = createMockWalkthroughService(walkthroughs);
184
+
185
+ root.render(
186
+ <WalkthroughSection
187
+ walkthroughService={mockService as unknown as WalkthroughService}
188
+ markdownRenderer={mockRenderer}
189
+ themeService={createMockThemeService()}
190
+ logger={createMockLogger()}
191
+ />
192
+ );
193
+
194
+ setTimeout(() => {
195
+ const cards = container.querySelectorAll('.gs-walkthrough-card');
196
+ assert.strictEqual(cards.length, 2, 'Should render 2 walkthrough cards');
197
+
198
+ const titles = container.querySelectorAll('.gs-walkthrough-card-title');
199
+ assert.ok(titles[0]?.textContent?.includes('Walkthrough One'), 'First card should show correct title');
200
+ assert.ok(titles[1]?.textContent?.includes('Walkthrough Two'), 'Second card should show correct title');
201
+ done();
202
+ }, 50);
203
+ });
204
+
205
+ it('should transition from card list to detail view when a card is clicked', done => {
206
+ const walkthroughs = [
207
+ createMockWalkthrough({ id: 'wt1', title: 'Click Me' })
208
+ ];
209
+ const mockService = createMockWalkthroughService(walkthroughs);
210
+
211
+ root.render(
212
+ <WalkthroughSection
213
+ walkthroughService={mockService as unknown as WalkthroughService}
214
+ markdownRenderer={mockRenderer}
215
+ themeService={createMockThemeService()}
216
+ logger={createMockLogger()}
217
+ />
218
+ );
219
+
220
+ setTimeout(() => {
221
+ const card = container.querySelector('.gs-walkthrough-card') as HTMLElement;
222
+ assert.ok(card, 'Card should exist');
223
+ card.click();
224
+
225
+ setTimeout(() => {
226
+ const detailView = container.querySelector('.gs-walkthrough-detail');
227
+ assert.ok(detailView, 'Detail view should be visible after clicking card');
228
+
229
+ const detailTitle = container.querySelector('.gs-walkthrough-detail-title');
230
+ assert.ok(detailTitle?.textContent?.includes('Click Me'), 'Detail view should show walkthrough title');
231
+
232
+ const cardList = container.querySelector('.gs-walkthrough-cards');
233
+ // eslint-disable-next-line no-null/no-null
234
+ assert.strictEqual(cardList, null, 'Card list should not be visible in detail view');
235
+ done();
236
+ }, 50);
237
+ }, 50);
238
+ });
239
+
240
+ it('should return to card list when back button is clicked in detail view', done => {
241
+ const walkthroughs = [
242
+ createMockWalkthrough({ id: 'wt1', title: 'Test WT' })
243
+ ];
244
+ const mockService = createMockWalkthroughService(walkthroughs);
245
+
246
+ root.render(
247
+ <WalkthroughSection
248
+ walkthroughService={mockService as unknown as WalkthroughService}
249
+ markdownRenderer={mockRenderer}
250
+ themeService={createMockThemeService()}
251
+ logger={createMockLogger()}
252
+ />
253
+ );
254
+
255
+ setTimeout(() => {
256
+ // Click on card to enter detail view
257
+ const card = container.querySelector('.gs-walkthrough-card') as HTMLElement;
258
+ card.click();
259
+
260
+ setTimeout(() => {
261
+ assert.ok(container.querySelector('.gs-walkthrough-detail'), 'Should be in detail view');
262
+
263
+ // Click back
264
+ const backLink = container.querySelector('.gs-walkthrough-back-link') as HTMLElement;
265
+ assert.ok(backLink, 'Back link should exist');
266
+ backLink.click();
267
+
268
+ setTimeout(() => {
269
+ const cards = container.querySelectorAll('.gs-walkthrough-card');
270
+ assert.strictEqual(cards.length, 1, 'Card list should be visible again');
271
+
272
+ const detail = container.querySelector('.gs-walkthrough-detail');
273
+ // eslint-disable-next-line no-null/no-null
274
+ assert.strictEqual(detail, null, 'Detail view should not be visible');
275
+ done();
276
+ }, 50);
277
+ }, 50);
278
+ }, 50);
279
+ });
280
+
281
+ it('should re-render when onDidChangeWalkthroughs fires', done => {
282
+ const walkthroughs = [
283
+ createMockWalkthrough({ id: 'wt1', title: 'Original' })
284
+ ];
285
+ const mockService = createMockWalkthroughService(walkthroughs);
286
+
287
+ root.render(
288
+ <WalkthroughSection
289
+ walkthroughService={mockService as unknown as WalkthroughService}
290
+ markdownRenderer={mockRenderer}
291
+ themeService={createMockThemeService()}
292
+ logger={createMockLogger()}
293
+ />
294
+ );
295
+
296
+ setTimeout(() => {
297
+ let cards = container.querySelectorAll('.gs-walkthrough-card');
298
+ assert.strictEqual(cards.length, 1, 'Should have 1 card initially');
299
+
300
+ // Add a second walkthrough and fire change event
301
+ walkthroughs.push(createMockWalkthrough({ id: 'wt2', title: 'New One' }));
302
+ mockService.changeEmitter.fire();
303
+
304
+ setTimeout(() => {
305
+ cards = container.querySelectorAll('.gs-walkthrough-card');
306
+ assert.strictEqual(cards.length, 2, 'Should have 2 cards after change event');
307
+ done();
308
+ }, 50);
309
+ }, 50);
310
+ });
311
+
312
+ it('should show the detail view when the service selects a valid walkthrough id', done => {
313
+ const walkthroughs = [
314
+ createMockWalkthrough({ id: 'wt1', title: 'First' }),
315
+ createMockWalkthrough({ id: 'wt2', title: 'Second' })
316
+ ];
317
+ const mockService = createMockWalkthroughService(walkthroughs);
318
+
319
+ root.render(
320
+ <WalkthroughSection
321
+ walkthroughService={mockService as unknown as WalkthroughService}
322
+ markdownRenderer={mockRenderer}
323
+ themeService={createMockThemeService()}
324
+ logger={createMockLogger()}
325
+ />
326
+ );
327
+
328
+ setTimeout(() => {
329
+ // Select the second walkthrough through the service, as `walkthrough.open` does
330
+ mockService.selectWalkthrough('wt2');
331
+
332
+ setTimeout(() => {
333
+ const detailView = container.querySelector('.gs-walkthrough-detail');
334
+ assert.ok(detailView, 'Detail view should be visible');
335
+
336
+ const detailTitle = container.querySelector('.gs-walkthrough-detail-title');
337
+ assert.ok(detailTitle?.textContent?.includes('Second'), 'Should show the selected walkthrough');
338
+ done();
339
+ }, 50);
340
+ }, 50);
341
+ });
342
+
343
+ it('should not navigate to detail view when an unknown walkthrough id is selected', done => {
344
+ const walkthroughs = [
345
+ createMockWalkthrough({ id: 'wt1', title: 'Only One' })
346
+ ];
347
+ const mockService = createMockWalkthroughService(walkthroughs);
348
+
349
+ root.render(
350
+ <WalkthroughSection
351
+ walkthroughService={mockService as unknown as WalkthroughService}
352
+ markdownRenderer={mockRenderer}
353
+ themeService={createMockThemeService()}
354
+ logger={createMockLogger()}
355
+ />
356
+ );
357
+
358
+ setTimeout(() => {
359
+ mockService.selectWalkthrough('nonexistent');
360
+
361
+ setTimeout(() => {
362
+ const detailView = container.querySelector('.gs-walkthrough-detail');
363
+ // eslint-disable-next-line no-null/no-null
364
+ assert.strictEqual(detailView, null, 'Should not show detail view for invalid ID');
365
+
366
+ const cards = container.querySelectorAll('.gs-walkthrough-card');
367
+ assert.strictEqual(cards.length, 1, 'Should still show cards');
368
+ done();
369
+ }, 50);
370
+ }, 50);
371
+ });
372
+
373
+ it('should properly clean up event subscriptions on unmount', done => {
374
+ const walkthroughs = [
375
+ createMockWalkthrough({ id: 'wt1' })
376
+ ];
377
+ const mockService = createMockWalkthroughService(walkthroughs);
378
+
379
+ root.render(
380
+ <WalkthroughSection
381
+ walkthroughService={mockService as unknown as WalkthroughService}
382
+ markdownRenderer={mockRenderer}
383
+ themeService={createMockThemeService()}
384
+ logger={createMockLogger()}
385
+ />
386
+ );
387
+
388
+ setTimeout(() => {
389
+ // Unmount the component
390
+ root.unmount();
391
+
392
+ // Verify emitters can still fire without error (listeners were cleaned up)
393
+ assert.doesNotThrow(() => {
394
+ mockService.changeEmitter.fire();
395
+ mockService.selectionEmitter.fire();
396
+ }, 'Should not throw after unmount');
397
+
398
+ // Re-create root for afterEach cleanup
399
+ root = createRoot(container);
400
+ done();
401
+ }, 50);
402
+ });
403
+ it('should list at most WALKTHROUGH_LIST_LIMIT walkthroughs and delegate More... to the picker', done => {
404
+ const walkthroughs = [1, 2, 3, 4, 5].map(i => createMockWalkthrough({ id: `wt${i}`, title: `WT ${i}` }));
405
+ const mockService = createMockWalkthroughService(walkthroughs);
406
+ let showAllCalled = false;
407
+
408
+ root.render(
409
+ <WalkthroughSection
410
+ walkthroughService={mockService as unknown as WalkthroughService}
411
+ markdownRenderer={mockRenderer}
412
+ themeService={createMockThemeService()}
413
+ logger={createMockLogger()}
414
+ onShowAll={() => { showAllCalled = true; }}
415
+ />
416
+ );
417
+
418
+ setTimeout(() => {
419
+ assert.strictEqual(container.querySelectorAll('.gs-walkthrough-card').length, WALKTHROUGH_LIST_LIMIT, 'Should list only the first few cards');
420
+
421
+ const more = container.querySelector('.gs-walkthrough-more') as HTMLElement;
422
+ assert.ok(more, 'More link should exist');
423
+ more.click();
424
+
425
+ setTimeout(() => {
426
+ assert.strictEqual(showAllCalled, true, 'More... should offer all walkthroughs for selection');
427
+ assert.strictEqual(container.querySelectorAll('.gs-walkthrough-card').length, WALKTHROUGH_LIST_LIMIT, 'The list itself should not expand');
428
+ done();
429
+ }, 50);
430
+ }, 50);
431
+ });
432
+
433
+ it('should not list a walkthrough whose steps are all complete', done => {
434
+ const walkthroughs = [
435
+ createMockWalkthrough({ id: 'pending', title: 'Pending' }),
436
+ createMockWalkthrough({
437
+ id: 'done',
438
+ title: 'Done',
439
+ steps: [createMockStep({ id: 's1', isComplete: true })]
440
+ })
441
+ ];
442
+ const mockService = createMockWalkthroughService(walkthroughs);
443
+
444
+ root.render(
445
+ <WalkthroughSection
446
+ walkthroughService={mockService as unknown as WalkthroughService}
447
+ markdownRenderer={mockRenderer}
448
+ themeService={createMockThemeService()}
449
+ logger={createMockLogger()}
450
+ onShowAll={() => { }}
451
+ />
452
+ );
453
+
454
+ setTimeout(() => {
455
+ const titles = Array.from(container.querySelectorAll('.gs-walkthrough-card-title')).map(e => e.textContent);
456
+ assert.deepStrictEqual(titles, ['Pending'], 'Only the unfinished walkthrough should be listed');
457
+ // The completed one is still reachable, so the link has to stay.
458
+ assert.ok(container.querySelector('.gs-walkthrough-more'), 'More link should be offered');
459
+ done();
460
+ }, 50);
461
+ });
462
+
463
+ it('should not offer More... when every walkthrough is listed', done => {
464
+ const walkthroughs = [1, 2].map(i => createMockWalkthrough({ id: `wt${i}`, title: `WT ${i}` }));
465
+ const mockService = createMockWalkthroughService(walkthroughs);
466
+
467
+ root.render(
468
+ <WalkthroughSection
469
+ walkthroughService={mockService as unknown as WalkthroughService}
470
+ markdownRenderer={mockRenderer}
471
+ themeService={createMockThemeService()}
472
+ logger={createMockLogger()}
473
+ onShowAll={() => { }}
474
+ />
475
+ );
476
+
477
+ setTimeout(() => {
478
+ assert.strictEqual(container.querySelectorAll('.gs-walkthrough-card').length, 2, 'Should list both cards');
479
+ // eslint-disable-next-line no-null/no-null
480
+ assert.strictEqual(container.querySelector('.gs-walkthrough-more'), null, 'More link should not exist');
481
+ done();
482
+ }, 50);
483
+ });
484
+ });
@@ -0,0 +1,119 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { codicon } from '@theia/core/lib/browser';
18
+ import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
19
+ import { ThemeService } from '@theia/core/lib/browser/theming';
20
+ import { ILogger } from '@theia/core/lib/common/logger';
21
+ import { DisposableCollection } from '@theia/core/lib/common/disposable';
22
+ import { nls } from '@theia/core/lib/common/nls';
23
+ import * as React from '@theia/core/shared/react';
24
+ import { Walkthrough } from '../common/walkthrough-types';
25
+ import { WalkthroughService } from './walkthrough-service';
26
+ import { WalkthroughCard } from './walkthrough-card';
27
+ import { WalkthroughDetail } from './walkthrough-detail';
28
+
29
+ export interface WalkthroughSectionProps {
30
+ walkthroughService: WalkthroughService;
31
+ markdownRenderer: MarkdownRenderer;
32
+ themeService: ThemeService;
33
+ logger: ILogger;
34
+ /** Offers all walkthroughs for selection, completed ones included. */
35
+ onShowAll?: () => void;
36
+ }
37
+
38
+ /** How many walkthroughs are listed on the welcome page. */
39
+ export const WALKTHROUGH_LIST_LIMIT = 2;
40
+
41
+ /**
42
+ * Renders the list of contributed walkthroughs, or the currently selected walkthrough.
43
+ *
44
+ * The selection is owned by the {@link WalkthroughService} rather than by this component, so that the
45
+ * welcome view can give the selected walkthrough the whole view instead of a single section.
46
+ */
47
+ export function WalkthroughSection(props: WalkthroughSectionProps): React.ReactElement {
48
+ const { walkthroughService } = props;
49
+ const [, forceUpdate] = React.useReducer((version: number) => version + 1, 0);
50
+
51
+ React.useEffect(() => {
52
+ const toDispose = new DisposableCollection(
53
+ walkthroughService.onDidChangeWalkthroughs(() => forceUpdate()),
54
+ walkthroughService.onDidChangeSelection(() => forceUpdate())
55
+ );
56
+ return () => toDispose.dispose();
57
+ }, [walkthroughService]);
58
+
59
+ const selectedWalkthrough = walkthroughService.selectedWalkthrough;
60
+ if (selectedWalkthrough) {
61
+ return (
62
+ <WalkthroughDetail
63
+ walkthrough={selectedWalkthrough}
64
+ onStepSelect={step => walkthroughService.selectStep(step.id)}
65
+ onBack={() => walkthroughService.clearSelection()}
66
+ selectedStep={walkthroughService.selectedStep}
67
+ markdownRenderer={props.markdownRenderer}
68
+ onLinkClick={url => walkthroughService.handleLinkClick(url)}
69
+ onToggleStepDone={step => step.isComplete
70
+ ? walkthroughService.markStepIncomplete(selectedWalkthrough.id, step.id)
71
+ : walkthroughService.markStepComplete(selectedWalkthrough.id, step.id)}
72
+ onMarkAllStepsDone={() => walkthroughService.markAllStepsComplete(selectedWalkthrough.id)}
73
+ themeService={props.themeService}
74
+ logger={props.logger}
75
+ />
76
+ );
77
+ }
78
+
79
+ const walkthroughs = walkthroughService.getWalkthroughs();
80
+ if (walkthroughs.length === 0) {
81
+ return <React.Fragment />;
82
+ }
83
+
84
+ // A finished walkthrough has nothing left to offer here; it stays reachable through `onShowAll`.
85
+ const pending = walkthroughs.filter(walkthrough => walkthrough.steps.some(step => !step.isComplete));
86
+ const listed = pending.slice(0, WALKTHROUGH_LIST_LIMIT);
87
+ return (
88
+ <div className='gs-section'>
89
+ <h3 className='gs-section-header'>
90
+ <i className={codicon('compass')}></i>
91
+ {nls.localizeByDefault('Walkthroughs')}
92
+ </h3>
93
+ <div className='gs-walkthrough-cards'>
94
+ {listed.map(walkthrough => (
95
+ <WalkthroughCard
96
+ key={walkthrough.id}
97
+ walkthrough={walkthrough}
98
+ onSelect={(selected: Walkthrough) => walkthroughService.selectWalkthrough(selected.id)}
99
+ />
100
+ ))}
101
+ </div>
102
+ {props.onShowAll && walkthroughs.length > listed.length && (
103
+ <a
104
+ role='button'
105
+ tabIndex={0}
106
+ className='gs-walkthrough-more'
107
+ onClick={props.onShowAll}
108
+ onKeyDown={(e: React.KeyboardEvent) => {
109
+ if (e.key === 'Enter') {
110
+ props.onShowAll?.();
111
+ }
112
+ }}
113
+ >
114
+ {nls.localizeByDefault('More...')}
115
+ </a>
116
+ )}
117
+ </div>
118
+ );
119
+ }