@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,675 @@
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 { MarkdownRenderer, MarkdownRenderOptions } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
25
+ import { ThemeService } from '@theia/core/lib/browser/theming';
26
+ import { Disposable } from '@theia/core/lib/common/disposable';
27
+ import { ILogger } from '@theia/core/lib/common/logger';
28
+ import { ThemeType } from '@theia/core/lib/common/theme';
29
+ import { Walkthrough, WalkthroughStep } from '../common/walkthrough-types';
30
+ import { WalkthroughDetail } from './walkthrough-detail';
31
+
32
+ function createMockStep(overrides?: Partial<WalkthroughStep>): WalkthroughStep {
33
+ return {
34
+ id: 'step-1',
35
+ title: 'Test Step',
36
+ description: 'A test step description',
37
+ isComplete: false,
38
+ ...overrides
39
+ };
40
+ }
41
+
42
+ function createMockWalkthrough(overrides?: Partial<Walkthrough>): Walkthrough {
43
+ return {
44
+ id: 'test-walkthrough',
45
+ title: 'Test Walkthrough',
46
+ description: 'A test walkthrough description',
47
+ steps: [
48
+ createMockStep({ id: 'step-1', title: 'First Step' }),
49
+ createMockStep({ id: 'step-2', title: 'Second Step' }),
50
+ createMockStep({ id: 'step-3', title: 'Third Step' })
51
+ ],
52
+ pluginId: 'test.plugin',
53
+ ...overrides
54
+ };
55
+ }
56
+
57
+ function createMockMarkdownRenderer(): MarkdownRenderer {
58
+ return {
59
+ render: (markdown: { value: string }) => {
60
+ const div = document.createElement('div');
61
+ div.textContent = markdown?.value ?? 'rendered markdown';
62
+ return { element: div, dispose: () => { } };
63
+ }
64
+ };
65
+ }
66
+
67
+ function createMockThemeService(themeType: ThemeType = 'dark'): ThemeService {
68
+ return {
69
+ getCurrentTheme: () => ({ id: 'test', label: 'Test', type: themeType }),
70
+ onDidColorThemeChange: () => Disposable.NULL
71
+ } as unknown as ThemeService;
72
+ }
73
+
74
+ function createMockLogger(): ILogger {
75
+ return { warn: () => { }, error: () => { }, info: () => { }, debug: () => { } } as unknown as ILogger;
76
+ }
77
+
78
+ describe('WalkthroughDetail', () => {
79
+ let container: HTMLElement;
80
+ let root: Root;
81
+ let mockRenderer: MarkdownRenderer;
82
+
83
+ before(() => disableJSDOM = enableJSDOM());
84
+ after(() => disableJSDOM());
85
+
86
+ beforeEach(() => {
87
+ container = document.createElement('div');
88
+ document.body.appendChild(container);
89
+ root = createRoot(container);
90
+ mockRenderer = createMockMarkdownRenderer();
91
+ });
92
+
93
+ afterEach(() => {
94
+ root.unmount();
95
+ document.body.removeChild(container);
96
+ });
97
+
98
+ it('should render walkthrough title and back button', done => {
99
+ const walkthrough = createMockWalkthrough({ title: 'Detail Walkthrough' });
100
+
101
+ root.render(
102
+ <WalkthroughDetail
103
+ logger={createMockLogger()}
104
+ themeService={createMockThemeService()}
105
+ walkthrough={walkthrough}
106
+ onStepSelect={() => { }}
107
+ onBack={() => { }}
108
+ markdownRenderer={mockRenderer}
109
+ />
110
+ );
111
+
112
+ setTimeout(() => {
113
+ const title = container.querySelector('.gs-walkthrough-detail-title');
114
+ assert.ok(title, 'Title element should exist');
115
+ assert.ok(title?.textContent?.includes('Detail Walkthrough'), 'Should contain walkthrough title');
116
+
117
+ const backLink = container.querySelector('.gs-walkthrough-back-link');
118
+ assert.ok(backLink, 'Back link should exist');
119
+ // The label is a separate element so that it, and not the arrow icon, can carry the hover underline.
120
+ assert.ok(backLink?.querySelector('.gs-walkthrough-back-label'), 'Back label should be its own element');
121
+ done();
122
+ }, 50);
123
+ });
124
+
125
+ it('should render all steps in the step list', done => {
126
+ const walkthrough = createMockWalkthrough({
127
+ steps: [
128
+ createMockStep({ id: 's1', title: 'Step A' }),
129
+ createMockStep({ id: 's2', title: 'Step B' }),
130
+ createMockStep({ id: 's3', title: 'Step C' })
131
+ ]
132
+ });
133
+
134
+ root.render(
135
+ <WalkthroughDetail
136
+ logger={createMockLogger()}
137
+ themeService={createMockThemeService()}
138
+ walkthrough={walkthrough}
139
+ onStepSelect={() => { }}
140
+ onBack={() => { }}
141
+ markdownRenderer={mockRenderer}
142
+ />
143
+ );
144
+
145
+ setTimeout(() => {
146
+ const stepItems = container.querySelectorAll('.gs-walkthrough-step-item');
147
+ assert.strictEqual(stepItems.length, 3, 'Should render 3 step items');
148
+
149
+ const stepTitles = container.querySelectorAll('.gs-walkthrough-step-title');
150
+ assert.ok(stepTitles[0]?.textContent?.includes('Step A'), 'First step should be Step A');
151
+ assert.ok(stepTitles[1]?.textContent?.includes('Step B'), 'Second step should be Step B');
152
+ assert.ok(stepTitles[2]?.textContent?.includes('Step C'), 'Third step should be Step C');
153
+ done();
154
+ }, 50);
155
+ });
156
+
157
+ it('should highlight the selected step', done => {
158
+ const selectedStep = createMockStep({ id: 's2', title: 'Selected Step' });
159
+ const walkthrough = createMockWalkthrough({
160
+ steps: [
161
+ createMockStep({ id: 's1', title: 'Step A' }),
162
+ selectedStep,
163
+ createMockStep({ id: 's3', title: 'Step C' })
164
+ ]
165
+ });
166
+
167
+ root.render(
168
+ <WalkthroughDetail
169
+ logger={createMockLogger()}
170
+ themeService={createMockThemeService()}
171
+ walkthrough={walkthrough}
172
+ onStepSelect={() => { }}
173
+ onBack={() => { }}
174
+ selectedStep={selectedStep}
175
+ markdownRenderer={mockRenderer}
176
+ />
177
+ );
178
+
179
+ setTimeout(() => {
180
+ const stepItems = container.querySelectorAll('.gs-walkthrough-step-item');
181
+ assert.ok(!stepItems[0].classList.contains('selected'), 'First step should not be selected');
182
+ assert.ok(stepItems[1].classList.contains('selected'), 'Second step should be selected');
183
+ assert.ok(!stepItems[2].classList.contains('selected'), 'Third step should not be selected');
184
+ done();
185
+ }, 50);
186
+ });
187
+
188
+ it('should call onBack when back button is clicked', done => {
189
+ const walkthrough = createMockWalkthrough();
190
+ let backCalled = false;
191
+
192
+ root.render(
193
+ <WalkthroughDetail
194
+ logger={createMockLogger()}
195
+ themeService={createMockThemeService()}
196
+ walkthrough={walkthrough}
197
+ onStepSelect={() => { }}
198
+ onBack={() => { backCalled = true; }}
199
+ markdownRenderer={mockRenderer}
200
+ />
201
+ );
202
+
203
+ setTimeout(() => {
204
+ const backLink = container.querySelector('.gs-walkthrough-back-link') as HTMLElement;
205
+ assert.ok(backLink, 'Back link should exist');
206
+ backLink.click();
207
+
208
+ setTimeout(() => {
209
+ assert.ok(backCalled, 'onBack should have been called');
210
+ done();
211
+ }, 50);
212
+ }, 50);
213
+ });
214
+
215
+ it('should call onStepSelect when a step is clicked', done => {
216
+ const steps = [
217
+ createMockStep({ id: 's1', title: 'Step A' }),
218
+ createMockStep({ id: 's2', title: 'Step B' })
219
+ ];
220
+ const walkthrough = createMockWalkthrough({ steps });
221
+ let selectedStep: WalkthroughStep | undefined;
222
+
223
+ root.render(
224
+ <WalkthroughDetail
225
+ logger={createMockLogger()}
226
+ themeService={createMockThemeService()}
227
+ walkthrough={walkthrough}
228
+ onStepSelect={step => { selectedStep = step; }}
229
+ onBack={() => { }}
230
+ markdownRenderer={mockRenderer}
231
+ />
232
+ );
233
+
234
+ setTimeout(() => {
235
+ const stepItems = container.querySelectorAll('.gs-walkthrough-step-item');
236
+ (stepItems[1] as HTMLElement).click();
237
+
238
+ setTimeout(() => {
239
+ assert.ok(selectedStep, 'onStepSelect should have been called');
240
+ assert.strictEqual(selectedStep?.id, 's2', 'Should select the clicked step');
241
+ done();
242
+ }, 50);
243
+ }, 50);
244
+ });
245
+
246
+ it('should render step content for the selected step', done => {
247
+ const selectedStep = createMockStep({
248
+ id: 's1',
249
+ title: 'Content Step',
250
+ description: 'Step with content'
251
+ });
252
+ const walkthrough = createMockWalkthrough({ steps: [selectedStep] });
253
+
254
+ root.render(
255
+ <WalkthroughDetail
256
+ logger={createMockLogger()}
257
+ themeService={createMockThemeService()}
258
+ walkthrough={walkthrough}
259
+ onStepSelect={() => { }}
260
+ onBack={() => { }}
261
+ selectedStep={selectedStep}
262
+ markdownRenderer={mockRenderer}
263
+ />
264
+ );
265
+
266
+ setTimeout(() => {
267
+ const stepContent = container.querySelector('.gs-walkthrough-step-content');
268
+ assert.ok(stepContent, 'Step content section should exist');
269
+
270
+ const stepDetail = container.querySelector('.gs-walkthrough-step-detail');
271
+ assert.ok(stepDetail, 'Step detail should exist');
272
+ assert.ok(stepDetail?.textContent?.includes('Content Step'), 'Should show step title');
273
+
274
+ const descriptionContainer = container.querySelector('.gs-walkthrough-step-description');
275
+ assert.ok(descriptionContainer, 'Description container should exist');
276
+ assert.ok(descriptionContainer?.textContent?.includes('Step with content'), 'Should show step description via markdown renderer');
277
+ done();
278
+ }, 50);
279
+ });
280
+
281
+ it('should not render step content when no step is selected', done => {
282
+ const walkthrough = createMockWalkthrough();
283
+
284
+ root.render(
285
+ <WalkthroughDetail
286
+ logger={createMockLogger()}
287
+ themeService={createMockThemeService()}
288
+ walkthrough={walkthrough}
289
+ onStepSelect={() => { }}
290
+ onBack={() => { }}
291
+ markdownRenderer={mockRenderer}
292
+ />
293
+ );
294
+
295
+ setTimeout(() => {
296
+ const stepContent = container.querySelector('.gs-walkthrough-step-content');
297
+ // eslint-disable-next-line no-null/no-null
298
+ assert.strictEqual(stepContent, null, 'Step content should not be rendered');
299
+ done();
300
+ }, 50);
301
+ });
302
+
303
+ it('should show completion icon for completed steps', done => {
304
+ const walkthrough = createMockWalkthrough({
305
+ steps: [
306
+ createMockStep({ id: 's1', isComplete: true }),
307
+ createMockStep({ id: 's2', isComplete: false })
308
+ ]
309
+ });
310
+
311
+ root.render(
312
+ <WalkthroughDetail
313
+ logger={createMockLogger()}
314
+ themeService={createMockThemeService()}
315
+ walkthrough={walkthrough}
316
+ onStepSelect={() => { }}
317
+ onBack={() => { }}
318
+ markdownRenderer={mockRenderer}
319
+ />
320
+ );
321
+
322
+ setTimeout(() => {
323
+ const stepItems = container.querySelectorAll('.gs-walkthrough-step-item');
324
+ assert.ok(stepItems[0].classList.contains('completed'), 'Completed step should have completed class');
325
+ assert.ok(!stepItems[1].classList.contains('completed'), 'Incomplete step should not have completed class');
326
+
327
+ const icons = container.querySelectorAll('.gs-walkthrough-step-icon');
328
+ assert.ok(icons[0].classList.contains('codicon-pass-filled'), 'Completed step should have pass-filled icon');
329
+ assert.ok(icons[1].classList.contains('codicon-circle-large-outline'), 'Incomplete step should have circle-outline icon');
330
+ done();
331
+ }, 50);
332
+ });
333
+
334
+ it('should render image media for selected step', done => {
335
+ const selectedStep = createMockStep({
336
+ id: 's1',
337
+ media: { image: '/path/to/image.png', altText: 'test image' }
338
+ });
339
+ const walkthrough = createMockWalkthrough({ steps: [selectedStep] });
340
+
341
+ root.render(
342
+ <WalkthroughDetail
343
+ logger={createMockLogger()}
344
+ themeService={createMockThemeService()}
345
+ walkthrough={walkthrough}
346
+ onStepSelect={() => { }}
347
+ onBack={() => { }}
348
+ selectedStep={selectedStep}
349
+ markdownRenderer={mockRenderer}
350
+ />
351
+ );
352
+
353
+ setTimeout(() => {
354
+ const img = container.querySelector('.gs-walkthrough-media-image') as HTMLImageElement;
355
+ assert.ok(img, 'Image element should exist');
356
+ assert.strictEqual(img.getAttribute('src'), '/path/to/image.png', 'Image src should be set');
357
+ assert.strictEqual(img.getAttribute('alt'), 'test image', 'Image alt should be set');
358
+ done();
359
+ }, 50);
360
+ });
361
+
362
+ it('should render SVG media for selected step', done => {
363
+ const selectedStep = createMockStep({
364
+ id: 's1',
365
+ media: { svg: '/path/to/graphic.svg' }
366
+ });
367
+ const walkthrough = createMockWalkthrough({ steps: [selectedStep] });
368
+
369
+ root.render(
370
+ <WalkthroughDetail
371
+ logger={createMockLogger()}
372
+ themeService={createMockThemeService()}
373
+ walkthrough={walkthrough}
374
+ onStepSelect={() => { }}
375
+ onBack={() => { }}
376
+ selectedStep={selectedStep}
377
+ markdownRenderer={mockRenderer}
378
+ />
379
+ );
380
+
381
+ setTimeout(() => {
382
+ const img = container.querySelector('.gs-walkthrough-media-image') as HTMLImageElement;
383
+ assert.ok(img, 'SVG image element should exist');
384
+ assert.strictEqual(img.getAttribute('src'), '/path/to/graphic.svg', 'SVG src should be set');
385
+ done();
386
+ }, 50);
387
+ });
388
+
389
+ it('should turn single newlines in a description into Markdown hard breaks', done => {
390
+ const renderedMarkdown: string[] = [];
391
+ const recordingRenderer: MarkdownRenderer = {
392
+ render: (markdown: { value: string }) => {
393
+ renderedMarkdown.push(markdown.value);
394
+ return { element: document.createElement('div'), dispose: () => { } };
395
+ }
396
+ };
397
+ const selectedStep = createMockStep({
398
+ id: 's1',
399
+ description: 'First line\nSecond line\n\nNew paragraph'
400
+ });
401
+ const walkthrough = createMockWalkthrough({ steps: [selectedStep] });
402
+
403
+ root.render(
404
+ <WalkthroughDetail
405
+ logger={createMockLogger()}
406
+ themeService={createMockThemeService()}
407
+ walkthrough={walkthrough}
408
+ onStepSelect={() => { }}
409
+ onBack={() => { }}
410
+ selectedStep={selectedStep}
411
+ markdownRenderer={recordingRenderer}
412
+ />
413
+ );
414
+
415
+ setTimeout(() => {
416
+ const description = renderedMarkdown.find(value => value.includes('First line'));
417
+ assert.ok(description, 'The description should have been rendered');
418
+ assert.ok(description?.includes('First line \nSecond line'), 'A single newline should become a hard break');
419
+ assert.ok(description?.includes('Second line\n\nNew paragraph'), 'A blank line should stay a paragraph break');
420
+ done();
421
+ }, 50);
422
+ });
423
+ it('should mark a step done when its step icon is clicked, without selecting the step', done => {
424
+ const step = createMockStep({ id: 's1', isComplete: false });
425
+ const walkthrough = createMockWalkthrough({ steps: [step] });
426
+ let markedStep: WalkthroughStep | undefined;
427
+ let selectedStep: WalkthroughStep | undefined;
428
+
429
+ root.render(
430
+ <WalkthroughDetail
431
+ logger={createMockLogger()}
432
+ themeService={createMockThemeService()}
433
+ walkthrough={walkthrough}
434
+ onStepSelect={s => { selectedStep = s; }}
435
+ onBack={() => { }}
436
+ markdownRenderer={mockRenderer}
437
+ onToggleStepDone={s => { markedStep = s; }}
438
+ />
439
+ );
440
+
441
+ setTimeout(() => {
442
+ const icon = container.querySelector('.gs-walkthrough-step-icon') as HTMLElement;
443
+ assert.ok(icon, 'Step icon should exist');
444
+ icon.click();
445
+
446
+ setTimeout(() => {
447
+ assert.strictEqual(markedStep?.id, 's1', 'onToggleStepDone should receive the step');
448
+ assert.strictEqual(selectedStep, undefined, 'Clicking the icon should not select the step');
449
+ done();
450
+ }, 50);
451
+ }, 50);
452
+ });
453
+
454
+ it('should toggle a completed step back to pending when its step icon is clicked', done => {
455
+ const step = createMockStep({ id: 's1', isComplete: true });
456
+ const walkthrough = createMockWalkthrough({ steps: [step] });
457
+ let toggledStep: WalkthroughStep | undefined;
458
+
459
+ root.render(
460
+ <WalkthroughDetail
461
+ logger={createMockLogger()}
462
+ themeService={createMockThemeService()}
463
+ walkthrough={walkthrough}
464
+ onStepSelect={() => { }}
465
+ onBack={() => { }}
466
+ markdownRenderer={mockRenderer}
467
+ onToggleStepDone={s => { toggledStep = s; }}
468
+ />
469
+ );
470
+
471
+ setTimeout(() => {
472
+ const icon = container.querySelector('.gs-walkthrough-step-icon') as HTMLElement;
473
+ assert.strictEqual(icon.getAttribute('aria-checked'), 'true', 'A completed step should read as checked');
474
+ icon.click();
475
+
476
+ setTimeout(() => {
477
+ assert.strictEqual(toggledStep?.id, 's1', 'onToggleStepDone should receive the completed step');
478
+ done();
479
+ }, 50);
480
+ }, 50);
481
+ });
482
+
483
+ it('should offer a Mark All Done action below the step list', done => {
484
+ const walkthrough = createMockWalkthrough({
485
+ steps: [createMockStep({ id: 's1' }), createMockStep({ id: 's2' })]
486
+ });
487
+ let markedAll = false;
488
+
489
+ root.render(
490
+ <WalkthroughDetail
491
+ logger={createMockLogger()}
492
+ themeService={createMockThemeService()}
493
+ walkthrough={walkthrough}
494
+ onStepSelect={() => { }}
495
+ onBack={() => { }}
496
+ markdownRenderer={mockRenderer}
497
+ onMarkAllStepsDone={() => { markedAll = true; }}
498
+ />
499
+ );
500
+
501
+ setTimeout(() => {
502
+ const button = container.querySelector('.gs-walkthrough-mark-all-done') as HTMLButtonElement;
503
+ assert.ok(button, 'Mark All Done button should exist');
504
+ assert.ok(button.closest('.gs-walkthrough-steps'), 'It should sit with the step list');
505
+ assert.ok(!button.disabled, 'It should be enabled while steps are pending');
506
+ button.click();
507
+
508
+ setTimeout(() => {
509
+ assert.strictEqual(markedAll, true, 'onMarkAllStepsDone should have been called');
510
+ done();
511
+ }, 50);
512
+ }, 50);
513
+ });
514
+
515
+ it('should disable Mark All Done once every step is complete', done => {
516
+ const walkthrough = createMockWalkthrough({
517
+ steps: [createMockStep({ id: 's1', isComplete: true }), createMockStep({ id: 's2', isComplete: true })]
518
+ });
519
+
520
+ root.render(
521
+ <WalkthroughDetail
522
+ logger={createMockLogger()}
523
+ themeService={createMockThemeService()}
524
+ walkthrough={walkthrough}
525
+ onStepSelect={() => { }}
526
+ onBack={() => { }}
527
+ markdownRenderer={mockRenderer}
528
+ onMarkAllStepsDone={() => { }}
529
+ />
530
+ );
531
+
532
+ setTimeout(() => {
533
+ const button = container.querySelector('.gs-walkthrough-mark-all-done') as HTMLButtonElement;
534
+ assert.ok(button, 'Mark All Done button should exist');
535
+ assert.ok(button.disabled, 'It should be disabled when nothing is left to complete');
536
+ done();
537
+ }, 50);
538
+ });
539
+
540
+ it('should keep `command:` links as plain inline links', done => {
541
+ const selectedStep = createMockStep({ id: 's1', description: '[Do it](command:my.command)' });
542
+ const walkthrough = createMockWalkthrough({ steps: [selectedStep] });
543
+ const linkRenderer: MarkdownRenderer = {
544
+ render: () => {
545
+ const div = document.createElement('div');
546
+ const anchor = document.createElement('a');
547
+ anchor.setAttribute('data-href', 'command:my.command');
548
+ anchor.textContent = 'Do it';
549
+ div.appendChild(anchor);
550
+ return { element: div, dispose: () => { } };
551
+ }
552
+ };
553
+
554
+ root.render(
555
+ <WalkthroughDetail
556
+ logger={createMockLogger()}
557
+ themeService={createMockThemeService()}
558
+ walkthrough={walkthrough}
559
+ onStepSelect={() => { }}
560
+ onBack={() => { }}
561
+ selectedStep={selectedStep}
562
+ markdownRenderer={linkRenderer}
563
+ />
564
+ );
565
+
566
+ setTimeout(() => {
567
+ const anchor = container.querySelector('.gs-walkthrough-step-description a') as HTMLElement;
568
+ assert.ok(anchor, 'The command link should be rendered');
569
+ assert.ok(!anchor.classList.contains('theia-button'), 'A command link should not be styled as a button');
570
+ done();
571
+ }, 50);
572
+ });
573
+
574
+ it('should pick the image variant matching the current theme and resolve the plugin resource', done => {
575
+ const selectedStep = createMockStep({
576
+ id: 's1',
577
+ media: {
578
+ image: {
579
+ dark: 'hostedPlugin/pub_name/dark.png',
580
+ light: 'hostedPlugin/pub_name/light.png',
581
+ hc: 'hostedPlugin/pub_name/hc.png',
582
+ hcLight: 'hostedPlugin/pub_name/hc-light.png'
583
+ },
584
+ altText: 'themed'
585
+ }
586
+ });
587
+ const walkthrough = createMockWalkthrough({ steps: [selectedStep] });
588
+
589
+ root.render(
590
+ <WalkthroughDetail
591
+ logger={createMockLogger()}
592
+ themeService={createMockThemeService('light')}
593
+ walkthrough={walkthrough}
594
+ onStepSelect={() => { }}
595
+ onBack={() => { }}
596
+ selectedStep={selectedStep}
597
+ markdownRenderer={mockRenderer}
598
+ />
599
+ );
600
+
601
+ setTimeout(() => {
602
+ const img = container.querySelector('.gs-walkthrough-media-image') as HTMLImageElement;
603
+ assert.ok(img, 'Image element should exist');
604
+ const src = img.getAttribute('src')!;
605
+ assert.ok(src.includes('light.png'), `Should use the light variant, was '${src}'`);
606
+ assert.ok(!src.startsWith('hostedPlugin/'), `Should resolve against the backend endpoint, was '${src}'`);
607
+ done();
608
+ }, 50);
609
+ });
610
+ it('should not render completion actions inside the step content', done => {
611
+ const selectedStep = createMockStep({ id: 's1', isComplete: false });
612
+ const walkthrough = createMockWalkthrough({ steps: [selectedStep] });
613
+
614
+ root.render(
615
+ <WalkthroughDetail
616
+ logger={createMockLogger()}
617
+ themeService={createMockThemeService()}
618
+ walkthrough={walkthrough}
619
+ onStepSelect={() => { }}
620
+ onBack={() => { }}
621
+ selectedStep={selectedStep}
622
+ markdownRenderer={mockRenderer}
623
+ onToggleStepDone={() => { }}
624
+ onMarkAllStepsDone={() => { }}
625
+ />
626
+ );
627
+
628
+ setTimeout(() => {
629
+ const content = container.querySelector('.gs-walkthrough-step-content')!;
630
+ // eslint-disable-next-line no-null/no-null
631
+ assert.strictEqual(content.querySelector('button'), null, 'The step content should carry no buttons');
632
+ // eslint-disable-next-line no-null/no-null
633
+ assert.strictEqual(content.querySelector('.gs-walkthrough-step-done'), null, 'The step content should carry no done indicator');
634
+ done();
635
+ }, 50);
636
+ });
637
+ it('should render markdown media as trusted and with an action handler, so that its links work', done => {
638
+ const originalFetch = global.fetch;
639
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
640
+ (global as any).fetch = () => Promise.resolve({ ok: true, text: () => Promise.resolve('[Run](command:my.command)') });
641
+
642
+ const captured: { value: string, isTrusted?: boolean, options?: MarkdownRenderOptions }[] = [];
643
+ const recordingRenderer: MarkdownRenderer = {
644
+ render: (markdown: { value: string, isTrusted?: boolean }, options?: MarkdownRenderOptions) => {
645
+ captured.push({ ...markdown, options });
646
+ return { element: document.createElement('div'), dispose: () => { } };
647
+ }
648
+ };
649
+ const selectedStep = createMockStep({ id: 's1', media: { markdown: 'content.md' } });
650
+ const walkthrough = createMockWalkthrough({ steps: [selectedStep] });
651
+
652
+ root.render(
653
+ <WalkthroughDetail
654
+ logger={createMockLogger()}
655
+ themeService={createMockThemeService()}
656
+ walkthrough={walkthrough}
657
+ onStepSelect={() => { }}
658
+ onBack={() => { }}
659
+ selectedStep={selectedStep}
660
+ markdownRenderer={recordingRenderer}
661
+ onLinkClick={() => { }}
662
+ />
663
+ );
664
+
665
+ setTimeout(() => {
666
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
667
+ (global as any).fetch = originalFetch;
668
+ const media = captured.find(entry => entry.value.includes('command:my.command'));
669
+ assert.ok(media, 'The media content should have been rendered');
670
+ assert.strictEqual(media?.isTrusted, true, 'Media has to be trusted, otherwise command links are stripped');
671
+ assert.ok(media?.options?.actionHandler, 'Media needs an action handler, otherwise its links are inert');
672
+ done();
673
+ }, 50);
674
+ });
675
+ });