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

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,1026 @@
1
+ "use strict";
2
+ // *****************************************************************************
3
+ // Copyright (C) 2026 EclipseSource and others.
4
+ //
5
+ // This program and the accompanying materials are made available under the
6
+ // terms of the Eclipse Public License v. 2.0 which is available at
7
+ // http://www.eclipse.org/legal/epl-2.0.
8
+ //
9
+ // This Source Code may also be made available under the following Secondary
10
+ // Licenses when the conditions for such availability set forth in the Eclipse
11
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
12
+ // with the GNU Classpath Exception which is available at
13
+ // https://www.gnu.org/software/classpath/license.html.
14
+ //
15
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
16
+ // *****************************************************************************
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ /* eslint-disable @typescript-eslint/no-explicit-any */
19
+ const chai_1 = require("chai");
20
+ const walkthrough_service_1 = require("./walkthrough-service");
21
+ const event_1 = require("@theia/core/lib/common/event");
22
+ describe('WalkthroughService', () => {
23
+ let service;
24
+ let storedData;
25
+ let onDidChangePluginsEmitter;
26
+ let onDidExecuteCommandEmitter;
27
+ let onPreferenceChangedEmitter;
28
+ let onDidChangeContextKeyEmitter;
29
+ let mockPlugins;
30
+ let mockDeployedPlugins;
31
+ let mockContextKeyMatchResult;
32
+ let executedCommands;
33
+ let mockPreferences;
34
+ let onDidExpandViewEmitter;
35
+ let mockOpenerGetOpener;
36
+ let mockOpenerOpenCalls;
37
+ let reportedErrors;
38
+ let mockDisabledByTrust;
39
+ function createContribution(overrides) {
40
+ return {
41
+ id: 'test-walkthrough',
42
+ title: 'Test Walkthrough',
43
+ description: 'A walkthrough for testing',
44
+ pluginId: 'test.publisher.test-plugin',
45
+ steps: [
46
+ {
47
+ id: 'step1',
48
+ title: 'Step One',
49
+ description: 'First step',
50
+ completionEvents: ['onCommand:test.command'],
51
+ },
52
+ {
53
+ id: 'step2',
54
+ title: 'Step Two',
55
+ description: 'Second step',
56
+ completionEvents: ['onSettingChanged:editor.fontSize'],
57
+ }
58
+ ],
59
+ ...overrides
60
+ };
61
+ }
62
+ beforeEach(() => {
63
+ storedData = {};
64
+ onDidChangePluginsEmitter = new event_1.Emitter();
65
+ onDidExecuteCommandEmitter = new event_1.Emitter();
66
+ onPreferenceChangedEmitter = new event_1.Emitter();
67
+ onDidChangeContextKeyEmitter = new event_1.Emitter();
68
+ mockPlugins = [];
69
+ mockDeployedPlugins = new Map();
70
+ mockContextKeyMatchResult = false;
71
+ executedCommands = [];
72
+ mockOpenerOpenCalls = [];
73
+ reportedErrors = [];
74
+ mockDisabledByTrust = new Set();
75
+ const mockOpener = { open: (...args) => { mockOpenerOpenCalls.push(String(args[0])); } };
76
+ mockOpenerGetOpener = () => Promise.resolve(mockOpener);
77
+ onDidExpandViewEmitter = new event_1.Emitter();
78
+ mockPreferences = {
79
+ 'workbench.welcomePage.walkthroughs.openOnInstall': true
80
+ };
81
+ service = new walkthrough_service_1.WalkthroughService();
82
+ // Manually wire dependencies
83
+ service.storageService = {
84
+ getData: (key, defaultValue) => Promise.resolve(storedData[key] ?? defaultValue),
85
+ setData: (key, value) => {
86
+ storedData[key] = value;
87
+ return Promise.resolve();
88
+ }
89
+ };
90
+ service.commandRegistry = {
91
+ onDidExecuteCommand: onDidExecuteCommandEmitter.event,
92
+ executeCommand: (commandId, ...args) => {
93
+ executedCommands.push({ commandId, args });
94
+ return Promise.resolve();
95
+ }
96
+ };
97
+ service.preferenceService = {
98
+ onPreferenceChanged: onPreferenceChangedEmitter.event
99
+ };
100
+ service.contextKeyService = {
101
+ onDidChange: onDidChangeContextKeyEmitter.event,
102
+ match: () => mockContextKeyMatchResult,
103
+ // Good enough for the expressions used here: every identifier is a key.
104
+ parseKeys: (expression) => new Set(expression.match(/[A-Za-z_][A-Za-z0-9_.]*/g) ?? [])
105
+ };
106
+ service.gettingStartedPreferences = new Proxy({}, {
107
+ get: (_target, prop) => mockPreferences[prop]
108
+ });
109
+ service.viewEventSource = {
110
+ onDidExpandView: onDidExpandViewEmitter.event
111
+ };
112
+ service.openerService = {
113
+ getOpener: () => mockOpenerGetOpener()
114
+ };
115
+ service.logger = {
116
+ warn: () => { },
117
+ error: () => { },
118
+ info: () => { },
119
+ debug: () => { }
120
+ };
121
+ service.messageService = {
122
+ error: (message) => {
123
+ reportedErrors.push(message);
124
+ return Promise.resolve(undefined);
125
+ }
126
+ };
127
+ service.pluginSupport = {
128
+ get plugins() { return mockPlugins; },
129
+ getPlugin: (id) => mockDeployedPlugins.get(id),
130
+ get disabledByTrust() { return mockDisabledByTrust; },
131
+ onDidChangePlugins: onDidChangePluginsEmitter.event
132
+ };
133
+ });
134
+ afterEach(() => {
135
+ service.dispose();
136
+ onDidChangePluginsEmitter.dispose();
137
+ onDidExecuteCommandEmitter.dispose();
138
+ onPreferenceChangedEmitter.dispose();
139
+ onDidChangeContextKeyEmitter.dispose();
140
+ onDidExpandViewEmitter.dispose();
141
+ });
142
+ describe('registerWalkthrough', () => {
143
+ it('should register a walkthrough and make it retrievable', () => {
144
+ const contribution = createContribution();
145
+ service.registerWalkthrough(contribution);
146
+ const walkthroughs = service.getWalkthroughs();
147
+ (0, chai_1.expect)(walkthroughs).to.have.lengthOf(1);
148
+ (0, chai_1.expect)(walkthroughs[0].id).to.equal('test.publisher.test-plugin.test-walkthrough');
149
+ (0, chai_1.expect)(walkthroughs[0].title).to.equal('Test Walkthrough');
150
+ (0, chai_1.expect)(walkthroughs[0].steps).to.have.lengthOf(2);
151
+ });
152
+ it('should construct full ID from pluginId and walkthrough id', () => {
153
+ const contribution = createContribution({ pluginId: 'my.plugin', id: 'my-wt' });
154
+ service.registerWalkthrough(contribution);
155
+ (0, chai_1.expect)(service.getWalkthrough('my.plugin.my-wt')).to.not.be.undefined;
156
+ });
157
+ it('should fire onDidChangeWalkthroughs event', () => {
158
+ let fired = false;
159
+ service.onDidChangeWalkthroughs(() => { fired = true; });
160
+ service.registerWalkthrough(createContribution());
161
+ (0, chai_1.expect)(fired).to.be.true;
162
+ });
163
+ it('should initialize steps as not complete', () => {
164
+ service.registerWalkthrough(createContribution());
165
+ const walkthrough = service.getWalkthroughs()[0];
166
+ for (const step of walkthrough.steps) {
167
+ (0, chai_1.expect)(step.isComplete).to.be.false;
168
+ }
169
+ });
170
+ });
171
+ describe('markStepComplete', () => {
172
+ beforeEach(() => {
173
+ service.registerWalkthrough(createContribution());
174
+ });
175
+ it('should mark a step as complete', async () => {
176
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
177
+ await service.markStepComplete(wtId, 'step1');
178
+ const walkthrough = service.getWalkthrough(wtId);
179
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.true;
180
+ (0, chai_1.expect)(walkthrough.steps[1].isComplete).to.be.false;
181
+ });
182
+ it('should persist progress to storage', async () => {
183
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
184
+ await service.markStepComplete(wtId, 'step1');
185
+ const stored = storedData['walkthrough-progress'];
186
+ (0, chai_1.expect)(stored.completedSteps[wtId]).to.include('step1');
187
+ });
188
+ it('should fire onDidChangeWalkthroughs on completion', async () => {
189
+ let fireCount = 0;
190
+ service.onDidChangeWalkthroughs(() => { fireCount++; });
191
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
192
+ // fireCount starts at 0 after registering above event
193
+ await service.markStepComplete(wtId, 'step1');
194
+ (0, chai_1.expect)(fireCount).to.equal(1);
195
+ });
196
+ it('should not fire event if step is already complete', async () => {
197
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
198
+ await service.markStepComplete(wtId, 'step1');
199
+ let fired = false;
200
+ service.onDidChangeWalkthroughs(() => { fired = true; });
201
+ await service.markStepComplete(wtId, 'step1');
202
+ (0, chai_1.expect)(fired).to.be.false;
203
+ });
204
+ it('should no-op for unknown walkthrough', async () => {
205
+ await service.markStepComplete('nonexistent', 'step1');
206
+ // should not throw
207
+ });
208
+ it('should no-op for unknown step', async () => {
209
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
210
+ await service.markStepComplete(wtId, 'nonexistent-step');
211
+ // should not throw
212
+ });
213
+ });
214
+ describe('markStepIncomplete', () => {
215
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
216
+ beforeEach(() => {
217
+ service.registerWalkthrough(createContribution());
218
+ });
219
+ it('should take the completion mark off a step again', async () => {
220
+ await service.markStepComplete(wtId, 'step1');
221
+ (0, chai_1.expect)(service.getWalkthrough(wtId).steps[0].isComplete).to.be.true;
222
+ await service.markStepIncomplete(wtId, 'step1');
223
+ (0, chai_1.expect)(service.getWalkthrough(wtId).steps[0].isComplete).to.be.false;
224
+ });
225
+ it('should remove the step from the persisted progress', async () => {
226
+ await service.markStepComplete(wtId, 'step1');
227
+ await service.markStepComplete(wtId, 'step2');
228
+ await service.markStepIncomplete(wtId, 'step1');
229
+ (0, chai_1.expect)(storedData['walkthrough-progress'].completedSteps[wtId]).to.deep.equal(['step2']);
230
+ });
231
+ it('should no-op for a step that is not complete', async () => {
232
+ let fired = false;
233
+ service.onDidChangeWalkthroughs(() => { fired = true; });
234
+ await service.markStepIncomplete(wtId, 'step1');
235
+ (0, chai_1.expect)(fired).to.be.false;
236
+ });
237
+ it('should no-op for an unknown step', async () => {
238
+ let fired = false;
239
+ service.onDidChangeWalkthroughs(() => { fired = true; });
240
+ await service.markStepIncomplete(wtId, 'nonexistent');
241
+ (0, chai_1.expect)(fired).to.be.false;
242
+ });
243
+ it('should let the progress go back down', async () => {
244
+ await service.markAllStepsComplete(wtId);
245
+ (0, chai_1.expect)(service.getStepProgress(wtId)).to.deep.equal({ completed: 2, total: 2 });
246
+ await service.markStepIncomplete(wtId, 'step1');
247
+ (0, chai_1.expect)(service.getStepProgress(wtId)).to.deep.equal({ completed: 1, total: 2 });
248
+ });
249
+ });
250
+ describe('markAllStepsComplete', () => {
251
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
252
+ beforeEach(() => {
253
+ service.registerWalkthrough(createContribution());
254
+ });
255
+ it('should complete every step', async () => {
256
+ await service.markAllStepsComplete(wtId);
257
+ const walkthrough = service.getWalkthrough(wtId);
258
+ for (const step of walkthrough.steps) {
259
+ (0, chai_1.expect)(step.isComplete).to.be.true;
260
+ }
261
+ });
262
+ it('should persist the progress once', async () => {
263
+ await service.markAllStepsComplete(wtId);
264
+ const stored = storedData['walkthrough-progress'];
265
+ (0, chai_1.expect)(stored.completedSteps[wtId]).to.deep.equal(['step1', 'step2']);
266
+ });
267
+ it('should no-op when everything is already complete', async () => {
268
+ await service.markAllStepsComplete(wtId);
269
+ let fired = false;
270
+ service.onDidChangeWalkthroughs(() => { fired = true; });
271
+ await service.markAllStepsComplete(wtId);
272
+ (0, chai_1.expect)(fired).to.be.false;
273
+ });
274
+ it('should no-op for an unknown walkthrough', async () => {
275
+ let fired = false;
276
+ service.onDidChangeWalkthroughs(() => { fired = true; });
277
+ await service.markAllStepsComplete('nonexistent');
278
+ (0, chai_1.expect)(fired).to.be.false;
279
+ });
280
+ });
281
+ describe('resetProgress', () => {
282
+ beforeEach(async () => {
283
+ service.registerWalkthrough(createContribution());
284
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
285
+ await service.markStepComplete(wtId, 'step1');
286
+ await service.markStepComplete(wtId, 'step2');
287
+ });
288
+ it('should reset all steps to incomplete', async () => {
289
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
290
+ await service.resetProgress(wtId);
291
+ const walkthrough = service.getWalkthrough(wtId);
292
+ for (const step of walkthrough.steps) {
293
+ (0, chai_1.expect)(step.isComplete).to.be.false;
294
+ }
295
+ });
296
+ it('should remove progress from storage', async () => {
297
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
298
+ await service.resetProgress(wtId);
299
+ const stored = storedData['walkthrough-progress'];
300
+ (0, chai_1.expect)(stored.completedSteps[wtId]).to.be.undefined;
301
+ });
302
+ it('should no-op for unknown walkthrough', async () => {
303
+ await service.resetProgress('nonexistent');
304
+ // should not throw
305
+ });
306
+ });
307
+ describe('getStepProgress', () => {
308
+ beforeEach(() => {
309
+ service.registerWalkthrough(createContribution());
310
+ });
311
+ it('should return correct progress', () => {
312
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
313
+ const progress = service.getStepProgress(wtId);
314
+ (0, chai_1.expect)(progress.completed).to.equal(0);
315
+ (0, chai_1.expect)(progress.total).to.equal(2);
316
+ });
317
+ it('should update after marking steps complete', async () => {
318
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
319
+ await service.markStepComplete(wtId, 'step1');
320
+ const progress = service.getStepProgress(wtId);
321
+ (0, chai_1.expect)(progress.completed).to.equal(1);
322
+ (0, chai_1.expect)(progress.total).to.equal(2);
323
+ });
324
+ it('should return zero for unknown walkthrough', () => {
325
+ const progress = service.getStepProgress('nonexistent');
326
+ (0, chai_1.expect)(progress.completed).to.equal(0);
327
+ (0, chai_1.expect)(progress.total).to.equal(0);
328
+ });
329
+ });
330
+ describe('completion event handling', () => {
331
+ beforeEach(() => {
332
+ service.registerWalkthrough(createContribution());
333
+ // Wire up the event handlers that @postConstruct would normally set up
334
+ service.toDispose.push(service.commandRegistry.onDidExecuteCommand((e) => {
335
+ service.handleCompletionEvent(`onCommand:${e.commandId}`);
336
+ }));
337
+ service.toDispose.push(service.preferenceService.onPreferenceChanged((e) => {
338
+ service.handleCompletionEvent(`onSettingChanged:${e.preferenceName}`);
339
+ }));
340
+ });
341
+ it('should complete step on matching command execution', async () => {
342
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
343
+ onDidExecuteCommandEmitter.fire({ commandId: 'test.command' });
344
+ // Give the async markStepComplete a tick to complete
345
+ await new Promise(resolve => setTimeout(resolve, 10));
346
+ const walkthrough = service.getWalkthrough(wtId);
347
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.true;
348
+ (0, chai_1.expect)(walkthrough.steps[1].isComplete).to.be.false;
349
+ });
350
+ it('should complete step on matching preference change', async () => {
351
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
352
+ onPreferenceChangedEmitter.fire({ preferenceName: 'editor.fontSize' });
353
+ await new Promise(resolve => setTimeout(resolve, 10));
354
+ const walkthrough = service.getWalkthrough(wtId);
355
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.false;
356
+ (0, chai_1.expect)(walkthrough.steps[1].isComplete).to.be.true;
357
+ });
358
+ it('should not complete step on non-matching event', async () => {
359
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
360
+ onDidExecuteCommandEmitter.fire({ commandId: 'unrelated.command' });
361
+ await new Promise(resolve => setTimeout(resolve, 10));
362
+ const walkthrough = service.getWalkthrough(wtId);
363
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.false;
364
+ (0, chai_1.expect)(walkthrough.steps[1].isComplete).to.be.false;
365
+ });
366
+ });
367
+ describe('progress persistence', () => {
368
+ it('should restore completed steps from storage on registration', async () => {
369
+ storedData['walkthrough-progress'] = {
370
+ completedSteps: {
371
+ 'test.publisher.test-plugin.test-walkthrough': ['step1']
372
+ }
373
+ };
374
+ // Simulate what loadProgress does
375
+ await service.loadProgress();
376
+ service.registerWalkthrough(createContribution());
377
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
378
+ const walkthrough = service.getWalkthrough(wtId);
379
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.true;
380
+ (0, chai_1.expect)(walkthrough.steps[1].isComplete).to.be.false;
381
+ });
382
+ it('should not register a walkthrough before the persisted progress was read', async () => {
383
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
384
+ const pluginId = 'test.publisher.test-plugin';
385
+ storedData['walkthrough-progress'] = { completedSteps: { [wtId]: ['step1'] } };
386
+ // Plugins are deployed while the storage read is still pending.
387
+ let releaseStorage = () => { };
388
+ const pendingRead = new Promise(resolve => { releaseStorage = resolve; });
389
+ service.storageService = {
390
+ getData: async (key, defaultValue) => {
391
+ await pendingRead;
392
+ return storedData[key] ?? defaultValue;
393
+ },
394
+ setData: () => Promise.resolve()
395
+ };
396
+ mockPlugins = [{ model: { id: pluginId, publisher: 'test.publisher', name: 'test-plugin' } }];
397
+ mockDeployedPlugins.set(pluginId, { contributes: { walkthroughs: [createContribution()] } });
398
+ service.init();
399
+ onDidChangePluginsEmitter.fire();
400
+ (0, chai_1.expect)(service.getWalkthroughs(), 'nothing may be registered yet').to.be.empty;
401
+ releaseStorage();
402
+ await new Promise(resolve => setTimeout(resolve, 10));
403
+ (0, chai_1.expect)(service.getWalkthrough(wtId).steps[0].isComplete, 'the saved progress has to be applied').to.be.true;
404
+ });
405
+ it('should handle missing storage data gracefully', async () => {
406
+ await service.loadProgress();
407
+ service.registerWalkthrough(createContribution());
408
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
409
+ const walkthrough = service.getWalkthrough(wtId);
410
+ for (const step of walkthrough.steps) {
411
+ (0, chai_1.expect)(step.isComplete).to.be.false;
412
+ }
413
+ });
414
+ });
415
+ describe('extension icon', () => {
416
+ it('should carry the icon of the contributing extension', () => {
417
+ service.registerWalkthrough(createContribution({ pluginIcon: 'hostedPlugin/pub_name/icon.png' }));
418
+ const walkthrough = service.getWalkthrough('test.publisher.test-plugin.test-walkthrough');
419
+ (0, chai_1.expect)(walkthrough.pluginIcon).to.equal('hostedPlugin/pub_name/icon.png');
420
+ });
421
+ it('should leave the icon undefined when the extension declares none', () => {
422
+ service.registerWalkthrough(createContribution());
423
+ const walkthrough = service.getWalkthrough('test.publisher.test-plugin.test-walkthrough');
424
+ (0, chai_1.expect)(walkthrough.pluginIcon).to.be.undefined;
425
+ });
426
+ });
427
+ describe('selection', () => {
428
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
429
+ it('should have no selection initially', () => {
430
+ (0, chai_1.expect)(service.selectedWalkthrough).to.be.undefined;
431
+ (0, chai_1.expect)(service.selectedStep).to.be.undefined;
432
+ });
433
+ it('should retain the selected walkthrough and fire onDidChangeSelection', () => {
434
+ service.registerWalkthrough(createContribution());
435
+ let fired = 0;
436
+ service.onDidChangeSelection(() => { fired++; });
437
+ service.selectWalkthrough(wtId);
438
+ (0, chai_1.expect)(fired).to.equal(1);
439
+ (0, chai_1.expect)(service.selectedWalkthrough?.id).to.equal(wtId);
440
+ });
441
+ it('should preselect the first pending step', () => {
442
+ service.registerWalkthrough(createContribution());
443
+ service.selectWalkthrough(wtId);
444
+ (0, chai_1.expect)(service.selectedStep?.id).to.equal('step1');
445
+ });
446
+ it('should preselect the first step that is not yet complete', async () => {
447
+ service.registerWalkthrough(createContribution());
448
+ await service.markStepComplete(wtId, 'step1');
449
+ service.selectWalkthrough(wtId);
450
+ (0, chai_1.expect)(service.selectedStep?.id).to.equal('step2');
451
+ });
452
+ it('should accept the VS Code `publisher.name#walkthroughId` form', () => {
453
+ service.registerWalkthrough(createContribution());
454
+ service.selectWalkthrough('test.publisher.test-plugin#test-walkthrough');
455
+ (0, chai_1.expect)(service.selectedWalkthrough?.id).to.equal(wtId);
456
+ });
457
+ it('should not select an unknown walkthrough', () => {
458
+ let fired = false;
459
+ service.onDidChangeSelection(() => { fired = true; });
460
+ service.selectWalkthrough('nonexistent');
461
+ (0, chai_1.expect)(fired).to.be.false;
462
+ (0, chai_1.expect)(service.selectedWalkthrough).to.be.undefined;
463
+ });
464
+ it('should select another step of the selected walkthrough', () => {
465
+ service.registerWalkthrough(createContribution());
466
+ service.selectWalkthrough(wtId);
467
+ service.selectStep('step2');
468
+ (0, chai_1.expect)(service.selectedStep?.id).to.equal('step2');
469
+ });
470
+ it('should ignore a step that does not belong to the selected walkthrough', () => {
471
+ service.registerWalkthrough(createContribution());
472
+ service.selectWalkthrough(wtId);
473
+ service.selectStep('nonexistent');
474
+ (0, chai_1.expect)(service.selectedStep?.id).to.equal('step1');
475
+ });
476
+ it('should clear the selection', () => {
477
+ service.registerWalkthrough(createContribution());
478
+ service.selectWalkthrough(wtId);
479
+ let fired = 0;
480
+ service.onDidChangeSelection(() => { fired++; });
481
+ service.clearSelection();
482
+ (0, chai_1.expect)(fired).to.equal(1);
483
+ (0, chai_1.expect)(service.selectedWalkthrough).to.be.undefined;
484
+ (0, chai_1.expect)(service.selectedStep).to.be.undefined;
485
+ });
486
+ it('should not fire onDidChangeSelection when clearing an empty selection', () => {
487
+ let fired = false;
488
+ service.onDidChangeSelection(() => { fired = true; });
489
+ service.clearSelection();
490
+ (0, chai_1.expect)(fired).to.be.false;
491
+ });
492
+ it('should reflect step completion in the selected step', async () => {
493
+ service.registerWalkthrough(createContribution());
494
+ service.selectWalkthrough(wtId);
495
+ await service.markStepComplete(wtId, 'step1');
496
+ (0, chai_1.expect)(service.selectedStep?.id).to.equal('step1');
497
+ (0, chai_1.expect)(service.selectedStep?.isComplete).to.be.true;
498
+ });
499
+ });
500
+ describe('when clauses', () => {
501
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
502
+ it('should hide a walkthrough whose when clause does not hold', () => {
503
+ mockContextKeyMatchResult = false;
504
+ service.registerWalkthrough(createContribution({ when: 'someKey' }));
505
+ (0, chai_1.expect)(service.getWalkthroughs()).to.be.empty;
506
+ (0, chai_1.expect)(service.getWalkthrough(wtId)).to.be.undefined;
507
+ });
508
+ it('should list a walkthrough whose when clause holds', () => {
509
+ mockContextKeyMatchResult = true;
510
+ service.registerWalkthrough(createContribution({ when: 'someKey' }));
511
+ (0, chai_1.expect)(service.getWalkthroughs()).to.have.lengthOf(1);
512
+ });
513
+ it('should always list a walkthrough without a when clause', () => {
514
+ mockContextKeyMatchResult = false;
515
+ service.registerWalkthrough(createContribution());
516
+ (0, chai_1.expect)(service.getWalkthroughs()).to.have.lengthOf(1);
517
+ });
518
+ it('should hide the steps whose when clause does not hold', () => {
519
+ mockContextKeyMatchResult = false;
520
+ service.registerWalkthrough(createContribution({
521
+ steps: [
522
+ { id: 'always', title: 'Always', description: 'd' },
523
+ { id: 'never', title: 'Never', description: 'd', when: 'someKey' }
524
+ ]
525
+ }));
526
+ const walkthrough = service.getWalkthrough(wtId);
527
+ (0, chai_1.expect)(walkthrough.steps).to.have.lengthOf(1);
528
+ (0, chai_1.expect)(walkthrough.steps[0].id).to.equal('always');
529
+ });
530
+ it('should exclude hidden steps from the progress', () => {
531
+ mockContextKeyMatchResult = false;
532
+ service.registerWalkthrough(createContribution({
533
+ steps: [
534
+ { id: 'always', title: 'Always', description: 'd' },
535
+ { id: 'never', title: 'Never', description: 'd', when: 'someKey' }
536
+ ]
537
+ }));
538
+ (0, chai_1.expect)(service.getStepProgress(wtId)).to.deep.equal({ completed: 0, total: 1 });
539
+ });
540
+ it('should not select a hidden walkthrough', () => {
541
+ mockContextKeyMatchResult = false;
542
+ service.registerWalkthrough(createContribution({ when: 'someKey' }));
543
+ service.selectWalkthrough(wtId);
544
+ (0, chai_1.expect)(service.selectedWalkthrough).to.be.undefined;
545
+ });
546
+ it('should preselect the first pending visible step', () => {
547
+ mockContextKeyMatchResult = false;
548
+ service.registerWalkthrough(createContribution({
549
+ steps: [
550
+ { id: 'hidden', title: 'Hidden', description: 'd', when: 'someKey' },
551
+ { id: 'visible', title: 'Visible', description: 'd' }
552
+ ]
553
+ }));
554
+ service.selectWalkthrough(wtId);
555
+ (0, chai_1.expect)(service.selectedStep?.id).to.equal('visible');
556
+ });
557
+ it('should re-evaluate visibility whenever it is queried', () => {
558
+ mockContextKeyMatchResult = false;
559
+ service.registerWalkthrough(createContribution({ when: 'someKey' }));
560
+ (0, chai_1.expect)(service.getWalkthroughs()).to.be.empty;
561
+ mockContextKeyMatchResult = true;
562
+ (0, chai_1.expect)(service.getWalkthroughs()).to.have.lengthOf(1);
563
+ });
564
+ it('should collect the context keys of every when clause and onContext event', () => {
565
+ service.registerWalkthrough(createContribution({
566
+ when: 'walkthroughKey',
567
+ steps: [{ id: 's1', title: 'S1', description: 'd', when: 'stepKey', completionEvents: ['onContext:eventKey'] }]
568
+ }));
569
+ const keys = service.getContextKeys();
570
+ (0, chai_1.expect)(Array.from(keys).sort()).to.deep.equal(['eventKey', 'stepKey', 'walkthroughKey']);
571
+ });
572
+ it('should notify when visibility changed', () => {
573
+ service.registerWalkthrough(createContribution({ when: 'someKey' }));
574
+ let fired = false;
575
+ service.onDidChangeWalkthroughs(() => { fired = true; });
576
+ service.handleVisibilityChanged();
577
+ (0, chai_1.expect)(fired).to.be.true;
578
+ });
579
+ it('should close a selected walkthrough that becomes hidden', () => {
580
+ mockContextKeyMatchResult = true;
581
+ service.registerWalkthrough(createContribution({ when: 'someKey' }));
582
+ service.selectWalkthrough(wtId);
583
+ (0, chai_1.expect)(service.selectedWalkthrough?.id).to.equal(wtId);
584
+ mockContextKeyMatchResult = false;
585
+ service.handleVisibilityChanged();
586
+ (0, chai_1.expect)(service.selectedWalkthrough).to.be.undefined;
587
+ });
588
+ it('should mark only the visible steps done', async () => {
589
+ mockContextKeyMatchResult = false;
590
+ service.registerWalkthrough(createContribution({
591
+ steps: [
592
+ { id: 'always', title: 'Always', description: 'd' },
593
+ { id: 'never', title: 'Never', description: 'd', when: 'someKey' }
594
+ ]
595
+ }));
596
+ await service.markAllStepsComplete(wtId);
597
+ (0, chai_1.expect)(storedData['walkthrough-progress']).to.deep.equal({ completedSteps: { [wtId]: ['always'] } });
598
+ });
599
+ });
600
+ describe('getWalkthrough / getWalkthroughs', () => {
601
+ it('should return undefined for non-existent walkthrough', () => {
602
+ (0, chai_1.expect)(service.getWalkthrough('nonexistent')).to.be.undefined;
603
+ });
604
+ it('should return empty array when no walkthroughs registered', () => {
605
+ (0, chai_1.expect)(service.getWalkthroughs()).to.be.empty;
606
+ });
607
+ it('should return all registered walkthroughs', () => {
608
+ service.registerWalkthrough(createContribution({ id: 'wt1' }));
609
+ service.registerWalkthrough(createContribution({ id: 'wt2' }));
610
+ const walkthroughs = service.getWalkthroughs();
611
+ (0, chai_1.expect)(walkthroughs).to.have.lengthOf(2);
612
+ });
613
+ });
614
+ describe('uninstalled and disabled plugins', () => {
615
+ const pluginId = 'test.publisher.test-plugin';
616
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
617
+ beforeEach(() => {
618
+ mockPlugins = [{ model: { id: pluginId, publisher: 'test.publisher', name: 'test-plugin' } }];
619
+ mockDeployedPlugins.set(pluginId, { contributes: { walkthroughs: [createContribution()] } });
620
+ service.syncWalkthroughsFromPlugins();
621
+ });
622
+ it('should offer the walkthrough of an installed plugin', () => {
623
+ (0, chai_1.expect)(service.getWalkthrough(wtId)).to.not.be.undefined;
624
+ });
625
+ it('should replace a walkthrough whose definition changed', () => {
626
+ (0, chai_1.expect)(service.getWalkthrough(wtId).steps).to.have.lengthOf(2);
627
+ // The plugin was updated and now contributes a single step.
628
+ mockDeployedPlugins.set(pluginId, {
629
+ contributes: {
630
+ walkthroughs: [createContribution({
631
+ steps: [{ id: 'only', title: 'Only Step', description: 'd' }]
632
+ })]
633
+ }
634
+ });
635
+ service.syncWalkthroughsFromPlugins();
636
+ const steps = service.getWalkthrough(wtId).steps;
637
+ (0, chai_1.expect)(steps).to.have.lengthOf(1);
638
+ (0, chai_1.expect)(steps[0].id).to.equal('only');
639
+ });
640
+ it('should keep a walkthrough whose definition is unchanged', () => {
641
+ let fired = false;
642
+ service.onDidChangeWalkthroughs(() => { fired = true; });
643
+ service.syncWalkthroughsFromPlugins();
644
+ (0, chai_1.expect)(fired, 'an unchanged sync must not notify').to.be.false;
645
+ });
646
+ it('should not offer the walkthrough of a plugin restricted by workspace trust', () => {
647
+ (0, chai_1.expect)(service.getWalkthrough(wtId), 'offered while the workspace is trusted').to.not.be.undefined;
648
+ mockDisabledByTrust.add(pluginId);
649
+ service.syncWalkthroughsFromPlugins();
650
+ (0, chai_1.expect)(service.getWalkthroughs()).to.be.empty;
651
+ });
652
+ it('should offer it again once the workspace is trusted', () => {
653
+ mockDisabledByTrust.add(pluginId);
654
+ service.syncWalkthroughsFromPlugins();
655
+ (0, chai_1.expect)(service.getWalkthroughs()).to.be.empty;
656
+ mockDisabledByTrust.delete(pluginId);
657
+ service.syncWalkthroughsFromPlugins();
658
+ (0, chai_1.expect)(service.getWalkthrough(wtId)).to.not.be.undefined;
659
+ });
660
+ it('should close a restricted walkthrough that was open', () => {
661
+ service.selectWalkthrough(wtId);
662
+ (0, chai_1.expect)(service.selectedWalkthrough?.id).to.equal(wtId);
663
+ mockDisabledByTrust.add(pluginId);
664
+ service.syncWalkthroughsFromPlugins();
665
+ (0, chai_1.expect)(service.selectedWalkthrough).to.be.undefined;
666
+ });
667
+ it('should drop the walkthrough once the plugin is out of sync', () => {
668
+ // Theia keeps an uninstalled or disabled plugin loaded until the next reload, flagged as out of sync.
669
+ mockPlugins[0].outOfSync = true;
670
+ service.syncWalkthroughsFromPlugins();
671
+ (0, chai_1.expect)(service.getWalkthrough(wtId)).to.be.undefined;
672
+ (0, chai_1.expect)(service.getWalkthroughs()).to.be.empty;
673
+ });
674
+ it('should notify when the walkthrough is dropped', () => {
675
+ let fired = false;
676
+ service.onDidChangeWalkthroughs(() => { fired = true; });
677
+ mockPlugins[0].outOfSync = true;
678
+ service.syncWalkthroughsFromPlugins();
679
+ (0, chai_1.expect)(fired).to.be.true;
680
+ });
681
+ it('should close the walkthrough if it was open', () => {
682
+ service.selectWalkthrough(wtId);
683
+ (0, chai_1.expect)(service.selectedWalkthrough?.id).to.equal(wtId);
684
+ mockPlugins[0].outOfSync = true;
685
+ service.syncWalkthroughsFromPlugins();
686
+ (0, chai_1.expect)(service.selectedWalkthrough).to.be.undefined;
687
+ });
688
+ });
689
+ describe('extensionInstalled completion event', () => {
690
+ const walkthroughContribution = createContribution({
691
+ steps: [
692
+ {
693
+ id: 'install-step',
694
+ title: 'Install Extension',
695
+ description: 'Install the helper extension',
696
+ completionEvents: ['extensionInstalled:some.extension'],
697
+ }
698
+ ]
699
+ });
700
+ function setupPluginWithWalkthrough() {
701
+ const pluginId = 'test.publisher.test-plugin';
702
+ mockPlugins = [{ model: { id: pluginId, publisher: 'test.publisher', name: 'test-plugin' } }];
703
+ mockDeployedPlugins.set(pluginId, {
704
+ contributes: { walkthroughs: [walkthroughContribution] }
705
+ });
706
+ }
707
+ beforeEach(() => {
708
+ // Set up the plugin so the walkthrough is discovered via syncWalkthroughsFromPlugins
709
+ setupPluginWithWalkthrough();
710
+ service.syncWalkthroughsFromPlugins();
711
+ // Wire up the onDidChangePlugins handler manually (simulating @postConstruct)
712
+ service.toDispose.push(service.pluginSupport.onDidChangePlugins(() => {
713
+ const previousIds = new Set(service.knownPluginIds);
714
+ service.syncWalkthroughsFromPlugins();
715
+ const newIds = new Set(service.pluginSupport.plugins.map((p) => p.model.id));
716
+ for (const newId of newIds) {
717
+ if (!previousIds.has(newId)) {
718
+ service.handleCompletionEvent(`extensionInstalled:${newId}`);
719
+ service.handleExtensionInstalledAutoOpen(newId);
720
+ }
721
+ }
722
+ service.knownPluginIds = newIds;
723
+ }));
724
+ // Mark the plugin as known initially so that the walkthrough plugin itself doesn't trigger
725
+ service.knownPluginIds = new Set(['test.publisher.test-plugin']);
726
+ });
727
+ it('should complete step when matching extension is installed', async () => {
728
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
729
+ // Add a new plugin 'some.extension'
730
+ mockPlugins.push({ model: { id: 'some.extension', publisher: 'some', name: 'extension' } });
731
+ onDidChangePluginsEmitter.fire();
732
+ await new Promise(resolve => setTimeout(resolve, 10));
733
+ const walkthrough = service.getWalkthrough(wtId);
734
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.true;
735
+ });
736
+ it('should not fire extensionInstalled for plugins present at startup', async () => {
737
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
738
+ // Mark some.extension as already known
739
+ service.knownPluginIds = new Set(['test.publisher.test-plugin', 'some.extension']);
740
+ mockPlugins.push({ model: { id: 'some.extension', publisher: 'some', name: 'extension' } });
741
+ onDidChangePluginsEmitter.fire();
742
+ await new Promise(resolve => setTimeout(resolve, 10));
743
+ const walkthrough = service.getWalkthrough(wtId);
744
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.false;
745
+ });
746
+ it('should not fire extensionInstalled when a plugin is removed', async () => {
747
+ // Start with some.extension known
748
+ service.knownPluginIds = new Set(['test.publisher.test-plugin', 'some.extension']);
749
+ // Don't add some.extension to mockPlugins (it's removed)
750
+ onDidChangePluginsEmitter.fire();
751
+ await new Promise(resolve => setTimeout(resolve, 10));
752
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
753
+ const walkthrough = service.getWalkthrough(wtId);
754
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.false;
755
+ });
756
+ it('should not complete step when non-matching extension is installed', async () => {
757
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
758
+ mockPlugins.push({ model: { id: 'other.extension', publisher: 'other', name: 'extension' } });
759
+ onDidChangePluginsEmitter.fire();
760
+ await new Promise(resolve => setTimeout(resolve, 10));
761
+ const walkthrough = service.getWalkthrough(wtId);
762
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.false;
763
+ });
764
+ });
765
+ describe('onContext completion event', () => {
766
+ beforeEach(() => {
767
+ service.registerWalkthrough(createContribution({
768
+ steps: [
769
+ {
770
+ id: 'context-step',
771
+ title: 'Context Step',
772
+ description: 'Completes on context key',
773
+ completionEvents: ['onContext:myFeature.enabled'],
774
+ }
775
+ ]
776
+ }));
777
+ // Wire up the context key handler manually
778
+ service.toDispose.push(service.contextKeyService.onDidChange((event) => {
779
+ for (const [walkthroughId, walkthrough] of service.walkthroughs) {
780
+ for (const step of walkthrough.steps) {
781
+ if (step.isComplete || !step.completionEvents) {
782
+ continue;
783
+ }
784
+ for (const completionEvent of step.completionEvents) {
785
+ if (completionEvent.startsWith('onContext:')) {
786
+ const contextKey = completionEvent.substring('onContext:'.length);
787
+ if (event.affects(new Set([contextKey])) && service.contextKeyService.match(contextKey)) {
788
+ service.markStepComplete(walkthroughId, step.id);
789
+ }
790
+ }
791
+ }
792
+ }
793
+ }
794
+ }));
795
+ });
796
+ it('should complete step when context key changes and becomes truthy', async () => {
797
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
798
+ mockContextKeyMatchResult = true;
799
+ onDidChangeContextKeyEmitter.fire({
800
+ affects: (keys) => keys.has('myFeature.enabled')
801
+ });
802
+ await new Promise(resolve => setTimeout(resolve, 10));
803
+ const walkthrough = service.getWalkthrough(wtId);
804
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.true;
805
+ });
806
+ it('should not complete step when context key changes but is not truthy', async () => {
807
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
808
+ mockContextKeyMatchResult = false;
809
+ onDidChangeContextKeyEmitter.fire({
810
+ affects: (keys) => keys.has('myFeature.enabled')
811
+ });
812
+ await new Promise(resolve => setTimeout(resolve, 10));
813
+ const walkthrough = service.getWalkthrough(wtId);
814
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.false;
815
+ });
816
+ it('should not complete step when unrelated context key changes', async () => {
817
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
818
+ mockContextKeyMatchResult = true;
819
+ onDidChangeContextKeyEmitter.fire({
820
+ affects: (keys) => keys.has('unrelated.key')
821
+ });
822
+ await new Promise(resolve => setTimeout(resolve, 10));
823
+ const walkthrough = service.getWalkthrough(wtId);
824
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.false;
825
+ });
826
+ });
827
+ describe('onContext expressions', () => {
828
+ it('should complete a step whose context expression already holds at registration', () => {
829
+ mockContextKeyMatchResult = true;
830
+ service.registerWalkthrough(createContribution({
831
+ steps: [{ id: 'static', title: 'Static', description: 'd', completionEvents: ['onContext:isLinux'] }]
832
+ }));
833
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
834
+ (0, chai_1.expect)(service.getWalkthrough(wtId).steps[0].isComplete).to.be.true;
835
+ });
836
+ it('should react to a context expression rather than treating it as a single key', () => {
837
+ service.registerWalkthrough(createContribution({
838
+ steps: [{ id: 'expr', title: 'Expression', description: 'd', completionEvents: ['onContext:foo && bar'] }]
839
+ }));
840
+ // The keys of the expression have to be recognised, not the expression string itself.
841
+ const keys = service.getContextKeys();
842
+ (0, chai_1.expect)(Array.from(keys).sort()).to.deep.equal(['bar', 'foo']);
843
+ mockContextKeyMatchResult = true;
844
+ service.completeMatchingContextSteps();
845
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
846
+ (0, chai_1.expect)(service.getWalkthrough(wtId).steps[0].isComplete).to.be.true;
847
+ });
848
+ });
849
+ describe('onView completion event', () => {
850
+ beforeEach(() => {
851
+ service.registerWalkthrough(createContribution({
852
+ steps: [
853
+ {
854
+ id: 'view-step',
855
+ title: 'View Step',
856
+ description: 'Completes on view expand',
857
+ completionEvents: ['onView:myExtension.myView'],
858
+ }
859
+ ]
860
+ }));
861
+ // Wire up the onView handler manually
862
+ service.toDispose.push(service.viewEventSource.onDidExpandView((viewId) => {
863
+ service.handleCompletionEvent(`onView:${viewId}`);
864
+ }));
865
+ });
866
+ it('should complete step when matching view is expanded', async () => {
867
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
868
+ onDidExpandViewEmitter.fire('myExtension.myView');
869
+ await new Promise(resolve => setTimeout(resolve, 10));
870
+ const walkthrough = service.getWalkthrough(wtId);
871
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.true;
872
+ });
873
+ it('should not complete step for non-matching view', async () => {
874
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
875
+ onDidExpandViewEmitter.fire('someOther.view');
876
+ await new Promise(resolve => setTimeout(resolve, 10));
877
+ const walkthrough = service.getWalkthrough(wtId);
878
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.false;
879
+ });
880
+ it('should handle multiple onView completion events on different steps', async () => {
881
+ // Register a second walkthrough with a different view event
882
+ service.registerWalkthrough(createContribution({
883
+ id: 'multi-view-walkthrough',
884
+ steps: [
885
+ {
886
+ id: 'view-step-a',
887
+ title: 'View Step A',
888
+ description: 'First view',
889
+ completionEvents: ['onView:view.alpha'],
890
+ },
891
+ {
892
+ id: 'view-step-b',
893
+ title: 'View Step B',
894
+ description: 'Second view',
895
+ completionEvents: ['onView:view.beta'],
896
+ }
897
+ ]
898
+ }));
899
+ onDidExpandViewEmitter.fire('view.alpha');
900
+ await new Promise(resolve => setTimeout(resolve, 10));
901
+ const wtId = 'test.publisher.test-plugin.multi-view-walkthrough';
902
+ let walkthrough = service.getWalkthrough(wtId);
903
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.true;
904
+ (0, chai_1.expect)(walkthrough.steps[1].isComplete).to.be.false;
905
+ onDidExpandViewEmitter.fire('view.beta');
906
+ await new Promise(resolve => setTimeout(resolve, 10));
907
+ walkthrough = service.getWalkthrough(wtId);
908
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.true;
909
+ (0, chai_1.expect)(walkthrough.steps[1].isComplete).to.be.true;
910
+ });
911
+ });
912
+ describe('onLink completion event', () => {
913
+ beforeEach(() => {
914
+ service.registerWalkthrough(createContribution({
915
+ steps: [
916
+ {
917
+ id: 'link-step',
918
+ title: 'Link Step',
919
+ description: 'Completes on link click',
920
+ completionEvents: ['onLink:https://example.com'],
921
+ }
922
+ ]
923
+ }));
924
+ });
925
+ it('should complete step and open link when handleLinkClick is called with matching URL', async () => {
926
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
927
+ service.handleLinkClick('https://example.com');
928
+ await new Promise(resolve => setTimeout(resolve, 10));
929
+ const walkthrough = service.getWalkthrough(wtId);
930
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.true;
931
+ (0, chai_1.expect)(mockOpenerOpenCalls).to.have.lengthOf(1);
932
+ });
933
+ it('should not complete step for non-matching URL but still open link', async () => {
934
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
935
+ service.handleLinkClick('https://other.com');
936
+ await new Promise(resolve => setTimeout(resolve, 10));
937
+ const walkthrough = service.getWalkthrough(wtId);
938
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.false;
939
+ (0, chai_1.expect)(mockOpenerOpenCalls).to.have.lengthOf(1);
940
+ });
941
+ it('should complete step with command: URI', async () => {
942
+ service.registerWalkthrough(createContribution({
943
+ id: 'cmd-link-walkthrough',
944
+ steps: [
945
+ {
946
+ id: 'cmd-link-step',
947
+ title: 'Command Link Step',
948
+ description: 'Completes on command link',
949
+ completionEvents: ['onLink:command:workbench.action.openSettings'],
950
+ }
951
+ ]
952
+ }));
953
+ service.handleLinkClick('command:workbench.action.openSettings');
954
+ await new Promise(resolve => setTimeout(resolve, 10));
955
+ const wtId = 'test.publisher.test-plugin.cmd-link-walkthrough';
956
+ const walkthrough = service.getWalkthrough(wtId);
957
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.true;
958
+ });
959
+ it('should strip the `toSide:` prefix before opening a command link', async () => {
960
+ await service.handleLinkClick('command:toSide:jupyter.createnewnotebook');
961
+ (0, chai_1.expect)(mockOpenerOpenCalls).to.have.lengthOf(1);
962
+ (0, chai_1.expect)(mockOpenerOpenCalls[0]).to.equal('command:jupyter.createnewnotebook');
963
+ (0, chai_1.expect)(reportedErrors).to.be.empty;
964
+ });
965
+ it('should report an error instead of rejecting when the link cannot be opened', async () => {
966
+ mockOpenerGetOpener = () => Promise.reject(new Error("The command 'missing.command' cannot be executed."));
967
+ await service.handleLinkClick('command:missing.command');
968
+ (0, chai_1.expect)(reportedErrors).to.have.lengthOf(1);
969
+ (0, chai_1.expect)(reportedErrors[0]).to.contain('missing.command');
970
+ });
971
+ it('should still complete the step when the link cannot be opened', async () => {
972
+ const wtId = 'test.publisher.test-plugin.test-walkthrough';
973
+ mockOpenerGetOpener = () => Promise.reject(new Error('no opener'));
974
+ await service.handleLinkClick('https://example.com');
975
+ const walkthrough = service.getWalkthrough(wtId);
976
+ (0, chai_1.expect)(walkthrough.steps[0].isComplete).to.be.true;
977
+ });
978
+ });
979
+ describe('openOnInstall preference', () => {
980
+ const contribution = createContribution();
981
+ function setupPluginWithWalkthrough() {
982
+ const pluginId = 'test.publisher.test-plugin';
983
+ mockPlugins = [{ model: { id: pluginId, publisher: 'test.publisher', name: 'test-plugin' } }];
984
+ mockDeployedPlugins.set(pluginId, {
985
+ contributes: { walkthroughs: [contribution] }
986
+ });
987
+ }
988
+ beforeEach(() => {
989
+ // Wire up the onDidChangePlugins handler manually
990
+ service.toDispose.push(service.pluginSupport.onDidChangePlugins(() => {
991
+ const previousIds = new Set(service.knownPluginIds);
992
+ service.syncWalkthroughsFromPlugins();
993
+ const newIds = new Set(service.pluginSupport.plugins.map((p) => p.model.id));
994
+ for (const newId of newIds) {
995
+ if (!previousIds.has(newId)) {
996
+ service.handleCompletionEvent(`extensionInstalled:${newId}`);
997
+ service.handleExtensionInstalledAutoOpen(newId);
998
+ }
999
+ }
1000
+ service.knownPluginIds = newIds;
1001
+ }));
1002
+ });
1003
+ it('should execute walkthrough.open when openOnInstall is true and extension has walkthroughs', () => {
1004
+ mockPreferences['workbench.welcomePage.walkthroughs.openOnInstall'] = true;
1005
+ setupPluginWithWalkthrough();
1006
+ onDidChangePluginsEmitter.fire();
1007
+ (0, chai_1.expect)(executedCommands).to.have.lengthOf(1);
1008
+ (0, chai_1.expect)(executedCommands[0].commandId).to.equal('walkthrough.open');
1009
+ (0, chai_1.expect)(executedCommands[0].args[0]).to.equal('test.publisher.test-plugin.test-walkthrough');
1010
+ });
1011
+ it('should not execute walkthrough.open when openOnInstall is false', () => {
1012
+ mockPreferences['workbench.welcomePage.walkthroughs.openOnInstall'] = false;
1013
+ setupPluginWithWalkthrough();
1014
+ onDidChangePluginsEmitter.fire();
1015
+ (0, chai_1.expect)(executedCommands).to.be.empty;
1016
+ });
1017
+ it('should not execute walkthrough.open when extension has no walkthroughs', () => {
1018
+ mockPreferences['workbench.welcomePage.walkthroughs.openOnInstall'] = true;
1019
+ // This plugin doesn't contribute walkthroughs
1020
+ mockPlugins = [{ model: { id: 'other.publisher.no-walkthroughs', publisher: 'other.publisher', name: 'no-walkthroughs' } }];
1021
+ onDidChangePluginsEmitter.fire();
1022
+ (0, chai_1.expect)(executedCommands).to.be.empty;
1023
+ });
1024
+ });
1025
+ });
1026
+ //# sourceMappingURL=walkthrough-service.spec.js.map