@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.
- package/lib/browser/getting-started-contribution.d.ts +30 -2
- package/lib/browser/getting-started-contribution.d.ts.map +1 -1
- package/lib/browser/getting-started-contribution.js +120 -0
- package/lib/browser/getting-started-contribution.js.map +1 -1
- package/lib/browser/getting-started-contribution.spec.d.ts +2 -0
- package/lib/browser/getting-started-contribution.spec.d.ts.map +1 -0
- package/lib/browser/getting-started-contribution.spec.js +152 -0
- package/lib/browser/getting-started-contribution.spec.js.map +1 -0
- package/lib/browser/getting-started-frontend-module.d.ts.map +1 -1
- package/lib/browser/getting-started-frontend-module.js +8 -0
- package/lib/browser/getting-started-frontend-module.js.map +1 -1
- package/lib/browser/getting-started-widget.d.ts +19 -0
- package/lib/browser/getting-started-widget.d.ts.map +1 -1
- package/lib/browser/getting-started-widget.js +57 -2
- package/lib/browser/getting-started-widget.js.map +1 -1
- package/lib/browser/getting-started-widget.spec.d.ts +2 -0
- package/lib/browser/getting-started-widget.spec.d.ts.map +1 -0
- package/lib/browser/getting-started-widget.spec.js +71 -0
- package/lib/browser/getting-started-widget.spec.js.map +1 -0
- package/lib/browser/walkthrough-card.d.ts +8 -0
- package/lib/browser/walkthrough-card.d.ts.map +1 -0
- package/lib/browser/walkthrough-card.js +40 -0
- package/lib/browser/walkthrough-card.js.map +1 -0
- package/lib/browser/walkthrough-card.spec.d.ts +2 -0
- package/lib/browser/walkthrough-card.spec.d.ts.map +1 -0
- package/lib/browser/walkthrough-card.spec.js +188 -0
- package/lib/browser/walkthrough-card.spec.js.map +1 -0
- package/lib/browser/walkthrough-detail.d.ts +19 -0
- package/lib/browser/walkthrough-detail.d.ts.map +1 -0
- package/lib/browser/walkthrough-detail.js +210 -0
- package/lib/browser/walkthrough-detail.js.map +1 -0
- package/lib/browser/walkthrough-detail.spec.d.ts +2 -0
- package/lib/browser/walkthrough-detail.spec.d.ts.map +1 -0
- package/lib/browser/walkthrough-detail.spec.js +409 -0
- package/lib/browser/walkthrough-detail.spec.js.map +1 -0
- package/lib/browser/walkthrough-icon.d.ts +10 -0
- package/lib/browser/walkthrough-icon.d.ts.map +1 -0
- package/lib/browser/walkthrough-icon.js +37 -0
- package/lib/browser/walkthrough-icon.js.map +1 -0
- package/lib/browser/walkthrough-section.d.ts +23 -0
- package/lib/browser/walkthrough-section.d.ts.map +1 -0
- package/lib/browser/walkthrough-section.js +65 -0
- package/lib/browser/walkthrough-section.js.map +1 -0
- package/lib/browser/walkthrough-section.spec.d.ts +2 -0
- package/lib/browser/walkthrough-section.spec.d.ts.map +1 -0
- package/lib/browser/walkthrough-section.spec.js +328 -0
- package/lib/browser/walkthrough-section.spec.js.map +1 -0
- package/lib/browser/walkthrough-service.d.ts +166 -0
- package/lib/browser/walkthrough-service.d.ts.map +1 -0
- package/lib/browser/walkthrough-service.js +505 -0
- package/lib/browser/walkthrough-service.js.map +1 -0
- package/lib/browser/walkthrough-service.spec.d.ts +2 -0
- package/lib/browser/walkthrough-service.spec.d.ts.map +1 -0
- package/lib/browser/walkthrough-service.spec.js +1026 -0
- package/lib/browser/walkthrough-service.spec.js.map +1 -0
- package/lib/common/getting-started-preferences.d.ts +1 -0
- package/lib/common/getting-started-preferences.d.ts.map +1 -1
- package/lib/common/getting-started-preferences.js +5 -0
- package/lib/common/getting-started-preferences.js.map +1 -1
- package/lib/common/walkthrough-commands.d.ts +11 -0
- package/lib/common/walkthrough-commands.d.ts.map +1 -0
- package/lib/common/walkthrough-commands.js +41 -0
- package/lib/common/walkthrough-commands.js.map +1 -0
- package/lib/common/walkthrough-types.d.ts +22 -0
- package/lib/common/walkthrough-types.d.ts.map +1 -0
- package/lib/common/walkthrough-types.js +18 -0
- package/lib/common/walkthrough-types.js.map +1 -0
- package/package.json +8 -7
- package/src/browser/getting-started-contribution.spec.ts +188 -0
- package/src/browser/getting-started-contribution.ts +139 -3
- package/src/browser/getting-started-frontend-module.ts +8 -0
- package/src/browser/getting-started-widget.spec.tsx +88 -0
- package/src/browser/getting-started-widget.tsx +70 -3
- package/src/browser/style/index.css +279 -0
- package/src/browser/walkthrough-card.spec.tsx +247 -0
- package/src/browser/walkthrough-card.tsx +62 -0
- package/src/browser/walkthrough-detail.spec.tsx +675 -0
- package/src/browser/walkthrough-detail.tsx +347 -0
- package/src/browser/walkthrough-icon.tsx +42 -0
- package/src/browser/walkthrough-section.spec.tsx +484 -0
- package/src/browser/walkthrough-section.tsx +119 -0
- package/src/browser/walkthrough-service.spec.ts +1279 -0
- package/src/browser/walkthrough-service.ts +565 -0
- package/src/common/getting-started-preferences.ts +6 -0
- package/src/common/walkthrough-commands.ts +37 -0
- 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
|
+
}
|