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